diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 4ec2c5c..c485d7d 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -102,4 +102,5 @@ dependencies {
implementation("io.ktor:ktor-client-core: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.apache.commons:commons-csv:1.5")
}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 0ad5229..e16262c 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -21,6 +21,16 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/navigation/CustomNavigationDrawer.kt b/app/src/main/java/cc/n0th1ng/tripmoney/navigation/CustomNavigationDrawer.kt
index fe912c8..3ff77a1 100644
--- a/app/src/main/java/cc/n0th1ng/tripmoney/navigation/CustomNavigationDrawer.kt
+++ b/app/src/main/java/cc/n0th1ng/tripmoney/navigation/CustomNavigationDrawer.kt
@@ -18,6 +18,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
+import cc.n0th1ng.tripmoney.R.string
import com.composables.icons.materialsymbols.outlined.R
import kotlinx.coroutines.launch
@@ -32,7 +33,7 @@ fun CustomNavigationDrawer(
Text("Trip Money", modifier = Modifier.padding(16.dp))
HorizontalDivider()
NavigationDrawerItem(
- label = { Text(text = "Pick trip") },
+ label = { Text(text = stringResource(string.pick_trip)) },
selected = false,
onClick = {
navController.navigate(Screens.TRIP_PICKER)
@@ -48,7 +49,7 @@ fun CustomNavigationDrawer(
)
})
NavigationDrawerItem(
- label = { Text(text = "List of expenses") },
+ label = { Text(text = stringResource(string.list_of_expenses)) },
selected = false,
onClick = {
navController.navigate(Screens.LIST_EXPENSE)
@@ -64,7 +65,7 @@ fun CustomNavigationDrawer(
)
})
NavigationDrawerItem(
- label = { Text(text = "Statistics") },
+ label = { Text(text = stringResource(string.statistics)) },
selected = false,
onClick = {
navController.navigate(Screens.STATISTICS)
@@ -80,7 +81,7 @@ fun CustomNavigationDrawer(
)
})
NavigationDrawerItem(
- label = { Text(text = "Settings") },
+ label = { Text(text = stringResource(string.settings)) },
selected = false,
onClick = {
navController.navigate(Screens.SETTINGS)
@@ -88,7 +89,7 @@ fun CustomNavigationDrawer(
drawerState.close()
}
},
- icon = { Icon(Icons.Default.Settings, contentDescription = "settings") }
+ icon = { Icon(Icons.Default.Settings, contentDescription = stringResource(string.settings)) }
)
}
}) { content() }
diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/navigation/TopBar.kt b/app/src/main/java/cc/n0th1ng/tripmoney/navigation/TopBar.kt
index 5c2a69a..fe8eb9e 100644
--- a/app/src/main/java/cc/n0th1ng/tripmoney/navigation/TopBar.kt
+++ b/app/src/main/java/cc/n0th1ng/tripmoney/navigation/TopBar.kt
@@ -11,6 +11,7 @@ import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import androidx.navigation.NavHostController
+import cc.n0th1ng.tripmoney.R
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -31,7 +32,7 @@ fun TopBar(onClick: () -> Unit, title: String = "") {
fun TopBarSettings(navController: NavHostController) {
TopAppBar(
- title = { Text("Settings") },
+ title = { Text(stringResource(R.string.settings)) },
navigationIcon = {
IconButton(onClick = { navController.popBackStack() }) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/screens/addexpense/AddExpenseBottomSheet.kt b/app/src/main/java/cc/n0th1ng/tripmoney/screens/addexpense/AddExpenseBottomSheet.kt
index 7e0e350..c1802d6 100644
--- a/app/src/main/java/cc/n0th1ng/tripmoney/screens/addexpense/AddExpenseBottomSheet.kt
+++ b/app/src/main/java/cc/n0th1ng/tripmoney/screens/addexpense/AddExpenseBottomSheet.kt
@@ -1,6 +1,7 @@
package cc.n0th1ng.tripmoney.screens.addexpense
import android.annotation.SuppressLint
+import android.graphics.drawable.PaintDrawable
import android.os.Build
import androidx.annotation.RequiresApi
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.automirrored.filled.ArrowBack
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.HorizontalDivider
import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
@@ -34,6 +40,7 @@ import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableDoubleStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
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.graphics.Color
+import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.ModifierLocalBeyondBoundsLayout
+import androidx.compose.ui.modifier.modifierLocalMapOf
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.sensitiveContent
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
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.Expense
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.CurrencySelectionDialog
import cc.n0th1ng.tripmoney.screens.listexpense.DateTimePicker
import cc.n0th1ng.tripmoney.theme.TripMoneyTheme
+import cc.n0th1ng.tripmoney.utils.AllPreviews
import cc.n0th1ng.tripmoney.utils.Currencies
+import cc.n0th1ng.tripmoney.utils.colors
import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel
import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel
import cc.n0th1ng.tripmoney.viewmodel.TripViewModel
+import com.composables.icons.materialsymbols.outlined.R.drawable
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
-
-
+import kotlin.collections.listOf
@OptIn(ExperimentalMaterial3Api::class)
@RequiresApi(Build.VERSION_CODES.O)
@@ -88,23 +101,48 @@ fun AddExpenseBottomSheet(
val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel()
val settingsViewModel: SettingsViewModel = hiltViewModel()
val currentTripId by settingsViewModel.currentTrip.collectAsState()
- val currentTrip = tripViewModel.getTrip(currentTripId)
+ val currentTrip = tripViewModel.getTrip(currentTripId)!!
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
+) {
+ val currentTripId = currentTrip.id
+
if (categories.isEmpty()) {
return
}
+
var amount by remember {
mutableStateOf(
expenseDtoToEdit?.expense?.amount?.toString() ?: "0.00"
)
}
+ var equationResult by remember { mutableDoubleStateOf(0.0) }
val dummyFocusRequester = remember { FocusRequester() }
var showCurrencyDialog by remember { mutableStateOf(false) }
var showCategoryDialog by remember { mutableStateOf(false) }
var showDateTimePicker by remember { mutableStateOf(false) }
var currency by remember {
mutableStateOf(
- expenseDtoToEdit?.expense?.currency ?: currentTrip?.currency ?: Currencies.default().name
+ expenseDtoToEdit?.expense?.currency ?: currentTrip.currency
)
}
var category by remember { mutableStateOf(expenseDtoToEdit?.category ?: categories[0]) }
@@ -122,6 +160,8 @@ fun AddExpenseBottomSheet(
ModalBottomSheet(
onDismissRequest = onDismiss,
sheetState = state,
+ containerColor = MaterialTheme.colorScheme.surfaceContainer,
+ contentColor = MaterialTheme.colorScheme.onSecondaryContainer
) {
Column(
modifier = Modifier
@@ -135,96 +175,127 @@ fun AddExpenseBottomSheet(
.fillMaxWidth()
.padding(start = 15.dp),
text = stringResource(if (expenseDtoToEdit == null) R.string.add_expense else R.string.edit_expense),
- fontWeight = FontWeight.Bold,
- fontSize = 35.sp,
+ style = MaterialTheme.typography.displaySmall,
textAlign = TextAlign.Start
)
- HorizontalDivider(modifier = Modifier.fillMaxWidth())
- Row(
- modifier = Modifier.fillMaxWidth(0.9f),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.SpaceBetween
+ HorizontalDivider(
+ modifier = Modifier.fillMaxWidth(),
+ color = MaterialTheme.colorScheme.onSecondaryContainer
+ )
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(10.dp),
+ modifier = Modifier.padding(10.dp)
) {
- Text(
- text = amount.ifEmpty { "0.00" },
- fontSize = 25.sp,
- fontWeight = FontWeight.Bold
- )
- CurrencyButton(onClick = { showCurrencyDialog = true }, text = currency)
- }
- Row(
- modifier = Modifier.fillMaxWidth(0.9f),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(10.dp)
- ) {
- OutlinedButton(
- onClick = { showDateTimePicker = true },
- modifier = Modifier.weight(1f)
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween
) {
- Text(
- text = datetime.format(DateTimeFormatter.ofPattern("dd.MM HH:mm")),
- fontSize = 17.sp
- )
+ Column{
+ Text(
+ text = amount.ifEmpty { "0.00" },
+ fontSize = 25.sp,
+ fontWeight = FontWeight.Bold
+ )
+ Text(
+ text = if(amount.contains(Regex("[+\\/*-]\\d+"))) "%.2f".format(equationResult) else "",
+ fontSize = 14.sp,
+ )
+ }
+ CurrencyButton(onClick = { showCurrencyDialog = true }, text = currency)
}
- CategoryButton(
- onClick = { showCategoryDialog = true },
- category = category,
- modifier = Modifier.weight(1f)
+ Box(
+ modifier = Modifier
+ .size(0.dp)
+ .focusRequester(dummyFocusRequester)
+ .focusable()
)
-
- }
- Row(
- verticalAlignment = Alignment.CenterVertically,
- ) {
NoteInput(
note = note,
onTextChange = { newNote -> note = newNote },
- modifier = Modifier.fillMaxWidth(0.9f),
+ modifier = Modifier.fillMaxWidth(),
focusRequester = dummyFocusRequester
)
- }
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(10.dp)
+ ) {
+ Button(
+ onClick = { showDateTimePicker = true },
+ modifier = Modifier.weight(1f),
+ shape = MaterialTheme.shapes.medium,
- Box(
- modifier = Modifier
- .size(0.dp)
- .focusRequester(dummyFocusRequester)
- .focusable()
- )
- NumberKeyboard(
- onNumberClick = { number ->
- val newText = (if (amount == "0.00") "" else amount) + number
- if (newText.isDoubleTwoDigitsAboveZero()) {
- amount = newText
- enableSave = true
- } else if (amount == "0.00") {
- enableSave = false
+ ) {
+ Text(
+ text = datetime.format(DateTimeFormatter.ofPattern("dd.MM HH:mm")),
+ fontSize = 17.sp
+ )
}
- dummyFocusRequester.requestFocus()
- },
- onBackspaceClick = {
- if (amount == "0.00") return@NumberKeyboard
- amount = amount.safeSubstring(0, amount.length - 1)
- enableSave = amount.isDoubleTwoDigitsAboveZero()
- },
- onSave = {
- val expenseToSave = Expense(
- amount = amount.toDouble(),
- currency = currency,
- note = note,
- datetime = datetime.toString(),
- categoryId = category.id,
- tripId = currentTripId
+ CategoryButton(
+ onClick = { showCategoryDialog = true },
+ category = category,
+ modifier = Modifier.weight(1f)
)
- onSave(
- if (expenseDtoToEdit == null) expenseToSave
- else expenseToSave.copy(id = expenseDtoToEdit.expense.id)
- )
- }, enableSave = enableSave
- )
+ }
+
+ 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 ->
+ val newText = (if (amount == "0.00") "" else amount) + number
+ if (newText.isDoubleTwoDigitsOrEquation()) {
+ amount = newText
+ equationResult = evaluate(amount)
+ enableSave = equationResult > 0
+ } else if (amount == "0.00") {
+ enableSave = false
+ }
+ dummyFocusRequester.requestFocus()
+ },
+ onBackspaceClick = {
+ if (amount == "0.00") return@NumberKeyboard
+ amount = amount.safeSubstring(0, amount.length - 1)
+ enableSave = amount.isDoubleTwoDigitsOrEquation()
+ equationResult = evaluate(amount)
+ enableSave = amount.isDoubleTwoDigitsOrEquation() && equationResult > 0
+ },
+ )
+
+ SaveButton(
+ modifier = Modifier.fillMaxWidth(),
+ enabled = enableSave,
+ onClick = {
+ val expenseToSave = Expense(
+ amount = equationResult,
+ currency = currency,
+ note = note,
+ datetime = datetime.toString(),
+ categoryId = category.id,
+ tripId = currentTripId
+ )
+ onSave(
+ if (expenseDtoToEdit == null) expenseToSave
+ else expenseToSave.copy(id = expenseDtoToEdit.expense.id)
+ )
+ })
+ }
}
}
+
if (showDateTimePicker) {
DateTimePicker(datetime, onChange = { newDateTime ->
datetime = newDateTime
@@ -264,12 +335,50 @@ fun String.safeSubstring(start: Int, end: Int): String {
}
}
-fun String.isDoubleTwoDigitsAboveZero(): Boolean {
- return this.toDoubleOrNull() != null && this.matches(Regex("^\\d*(\\.\\d{0,2})?$")) && this.toDouble() > 0
+private fun evaluate(equation: String): Double {
+ 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
-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) }
OutlinedTextField(
@@ -292,33 +401,36 @@ fun NoteInput(note: String, onTextChange: (String) -> Unit, modifier: Modifier =
@Composable
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)
}
}
@Composable
fun CategoryButton(onClick: () -> Unit, category: Category, modifier: Modifier = Modifier) {
- OutlinedButton(
+ Button(
onClick = onClick,
- modifier = modifier
+ modifier = modifier,
+ shape = MaterialTheme.shapes.medium,
+ colors = ButtonDefaults.buttonColors()
+ .copy(containerColor = Color(category.color.toColorInt()), contentColor = Color.Black)
) {
Icon(
modifier = Modifier.padding(end = 10.dp),
painter = painterResource(category.icon.resource),
contentDescription = stringResource(R.string.category),
- tint = Color(category.color.toColorInt())
)
- Text(category.name, color = Color(category.color.toColorInt()))
+ Text(category.name)
}
}
@Composable
-fun SaveButton(enabled: Boolean, onClick: () -> Unit) {
- OutlinedButton(
+fun SaveButton(modifier: Modifier = Modifier, enabled: Boolean, onClick: () -> Unit) {
+ Button(
onClick = onClick,
enabled = enabled,
- modifier = Modifier
+ modifier = modifier,
+ shape = MaterialTheme.shapes.medium
) {
Icon(
imageVector = Icons.Filled.Check,
@@ -332,273 +444,171 @@ fun NumberKeyboard(
modifier: Modifier = Modifier,
onNumberClick: (String) -> Unit,
onBackspaceClick: () -> Unit,
- onSave: () -> Unit,
- enableSave: Boolean
+ onOperatorClick: (String) -> Unit
) {
- val buttonModifier = Modifier
- .padding(4.dp)
- .aspectRatio(2f)
-
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.spacedBy(4.dp)
- ) {
- TextButton(
- onClick = { onNumberClick("1") },
- modifier = buttonModifier.weight(1f)
+ keyboard.forEach { row ->
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
- 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.forEach { key ->
+ when (key) {
+ "backspace" -> KeyboardButton(
+ icon = painterResource(drawable.materialsymbols_ic_arrow_left_alt_outlined),
+ onClick = onBackspaceClick,
+ modifier = Modifier.weight(1f),
+ containerColor = MaterialTheme.colorScheme.primary
+ )
- 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)
- }
- }
+ "+", "/", "-", "*" -> KeyboardButton(
+ text = key,
+ onClick = { onOperatorClick(key) },
+ modifier = Modifier.weight(1f),
+ containerColor = MaterialTheme.colorScheme.secondaryContainer,
+ contentColor = MaterialTheme.colorScheme.onSecondaryContainer
+ )
- 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,
- modifier = buttonModifier.weight(1f)
- ) {
- Icon(
- imageVector = Icons.AutoMirrored.Filled.ArrowBack,
- contentDescription = stringResource(R.string.backspace)
- )
- }
- TextButton(
- onClick = onSave,
- modifier = buttonModifier.weight(1f),
- enabled = enableSave
- ) {
- Icon(
- imageVector = Icons.Default.Check,
- contentDescription = stringResource(R.string.backspace)
- )
+ else -> KeyboardButton(
+ text = key,
+ onClick = { onNumberClick(key) },
+ modifier = Modifier.weight(1f),
+ containerColor = MaterialTheme.colorScheme.secondary,
+ contentColor = MaterialTheme.colorScheme.onSecondary
+ )
+ }
+ }
}
}
}
}
+@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")
-//@RequiresApi(Build.VERSION_CODES.O)
-//@OptIn(ExperimentalMaterial3Api::class)
-//@Preview
-//@Composable
-//fun PreviewLight() {
-// val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
-// CoroutineScope(Dispatchers.IO).launch {
-// sheetState.show()
-// }
-//
-// TripMoneyTheme {
-// AddExpenseBottomSheet(
-// {}, {}, null, sheetState,
-// categories = listOf(
-// Category(
-// name = "Hotel",
-// icon = cc.n0th1ng.tripmoney.utils.Icons.HOTEL,
-// color = "#B3E5FC"
-// ),
-// Category(
-// name = "Jedzenie",
-// icon = cc.n0th1ng.tripmoney.utils.Icons.RESTAURANT,
-// color = "#C8E6C9"
-// ),
-// Category(
-// name = "Transport",
-// icon = cc.n0th1ng.tripmoney.utils.Icons.FLIGHT,
-// color = "#FFCDD2"
-// ),
-// Category(
-// 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"
-// ),
-// )
-// )
-// }
-//}
-//
-//@SuppressLint("CoroutineCreationDuringComposition")
-//@RequiresApi(Build.VERSION_CODES.O)
-//@OptIn(ExperimentalMaterial3Api::class)
-//@Preview
-//@Composable
-//fun PreviewDark() {
-// val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
-// CoroutineScope(Dispatchers.IO).launch {
-// sheetState.show()
-// }
-//
-// TripMoneyTheme(darkTheme = true) {
-// AddExpenseBottomSheet(
-// {}, {}, null, sheetState,
-// categories = listOf(
-// Category(
-// name = "Hotel",
-// icon = cc.n0th1ng.tripmoney.utils.Icons.HOTEL,
-// color = "#B3E5FC"
-// ),
-// Category(
-// name = "Jedzenie",
-// icon = cc.n0th1ng.tripmoney.utils.Icons.RESTAURANT,
-// color = "#C8E6C9"
-// ),
-// Category(
-// name = "Transport",
-// icon = cc.n0th1ng.tripmoney.utils.Icons.FLIGHT,
-// color = "#FFCDD2"
-// ),
-// Category(
-// 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"
-// ),
-// )
-// )
-// }
-//}
\ No newline at end of file
+ icon != null -> Icon(painter = icon, contentDescription = null)
+ }
+ }
+}
+
+val keyboard = listOf(
+ listOf("+", "-", "*", "/"),
+ listOf("1", "2", "3"),
+ listOf("4", "5", "6"),
+ listOf("7", "8", "9"),
+ listOf(".", "0", "backspace")
+)
+
+
+@SuppressLint("CoroutineCreationDuringComposition")
+@RequiresApi(Build.VERSION_CODES.O)
+@OptIn(ExperimentalMaterial3Api::class)
+@AllPreviews
+@Composable
+fun PreviewAddExpenseDisabled() {
+ val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
+ CoroutineScope(Dispatchers.IO).launch {
+ sheetState.show()
+ }
+
+ TripMoneyTheme {
+ AddExpenseBottomSheet(
+ onSave = {},
+ onDismiss = {},
+ expenseDtoToEdit = null,
+ state = sheetState,
+ currentTrip = Trip(1, "Trip", "2020-01-01", Currencies.entries.random().name),
+ categories = categoriesToPreview
+ )
+ }
+
+}
+
+@SuppressLint("CoroutineCreationDuringComposition")
+@RequiresApi(Build.VERSION_CODES.O)
+@OptIn(ExperimentalMaterial3Api::class)
+@AllPreviews
+@Composable
+fun PreviewAddExpenseEnabled() {
+ val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
+ CoroutineScope(Dispatchers.IO).launch {
+ sheetState.show()
+ }
+
+ TripMoneyTheme {
+ AddExpenseBottomSheet(
+ onSave = {},
+ onDismiss = {},
+ expenseDtoToEdit = ExpenseDto(
+ Expense(
+ amount = 10.31,
+ currency = "PLN",
+ note = "some note",
+ 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),
+ categories = categoriesToPreview
+ )
+ }
+
+}
+
+val categoriesToPreview = listOf(
+ Category(
+ name = "Hotel",
+ icon = cc.n0th1ng.tripmoney.utils.Icons.HOTEL,
+ color = colors.random()
+ ),
+ Category(
+ name = "Jedzenie",
+ icon = cc.n0th1ng.tripmoney.utils.Icons.RESTAURANT,
+ color = colors.random()
+ ),
+ Category(
+ name = "Transport",
+ icon = cc.n0th1ng.tripmoney.utils.Icons.FLIGHT,
+ color = colors.random()
+ ),
+ Category(
+ name = "Rozrywka",
+ icon = cc.n0th1ng.tripmoney.utils.Icons.ATTRACTION,
+ color = colors.random()
+ ),
+ Category(
+ name = "Zakupy",
+ icon = cc.n0th1ng.tripmoney.utils.Icons.GROCERIES,
+ color = colors.random()
+ ),
+)
\ No newline at end of file
diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/screens/listexpense/ListExpenseScreen.kt b/app/src/main/java/cc/n0th1ng/tripmoney/screens/listexpense/ListExpenseScreen.kt
index a875eb1..d126310 100644
--- a/app/src/main/java/cc/n0th1ng/tripmoney/screens/listexpense/ListExpenseScreen.kt
+++ b/app/src/main/java/cc/n0th1ng/tripmoney/screens/listexpense/ListExpenseScreen.kt
@@ -50,9 +50,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
import androidx.core.graphics.toColorInt
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.paging.PagingData
@@ -126,7 +124,6 @@ fun ListExpenseScreen(
sumMap.clear()
sumMap.putAll(newSums)
}
-
LazyColumn(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
@@ -513,4 +510,4 @@ private fun sampleExpenseDtoWithConvertedAmountList(): List Unit,
+ onCurrencySave: (Currencies) -> Unit,
+ tripName: String,
+ onExportToCsv: () -> Unit,
+) {
+
+ Scaffold { padding ->
+ var showThemeDialog by remember { mutableStateOf(false) }
+ var showCurrencyDialog by remember { mutableStateOf(false) }
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(15.dp),
+ verticalArrangement = Arrangement.spacedBy(10.dp)
+ ) {
SettingsListItem(
onClick = { showCurrencyDialog = true },
- stringResource(string.default_currency)
- ) {
- Text(currentDefaultCurrency.name)
- }
- }
-
- if (showThemeDialog) {
- ThemeSelectionDialog(
- onDismiss = { showThemeDialog = false },
- onThemeSelected = { theme ->
- settingsViewModel.setTheme(theme)
- showThemeDialog = false
- },
- selected = currentTheme
+ headlineText = stringResource(string.default_currency),
+ supportingText = currentDefaultCurrency.name,
+ iconResource = R.drawable.materialsymbols_ic_currency_yen_outlined
)
- }
- if (showCurrencyDialog) {
- CurrencySelectionDialog(onDismiss = {showCurrencyDialog = false}, onCurrencySelected = {
- currencyString ->
- settingsViewModel.setDefaultCurrency(Currencies.valueOf(currencyString))
- showCurrencyDialog = false
- }, currentDefaultCurrency.name)
+ 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) {
+ ThemeSelectionDialog(
+ onDismiss = { showThemeDialog = false },
+ onThemeSelected = { theme ->
+ onThemeSave(theme)
+ showThemeDialog = false
+ },
+ selected = currentTheme
+ )
+ }
+
+ if (showCurrencyDialog) {
+ CurrencySelectionDialog(
+ onDismiss = { showCurrencyDialog = false },
+ onCurrencySelected = { currencyString ->
+ onCurrencySave(Currencies.valueOf(currencyString))
+ showCurrencyDialog = false
+ },
+ currentDefaultCurrency.name
+ )
+ }
}
}
}
@@ -97,7 +176,7 @@ fun SettingsCard(@StringRes title: Int = -1, content: @Composable () -> Unit) {
if (title != -1) {
Text(
text = stringResource(title),
- fontSize = 13.sp,
+ style = MaterialTheme.typography.titleSmall,
modifier = Modifier
.padding(start = 15.dp, top = 15.dp, end = 15.dp)
.alpha(0.6f)
@@ -112,16 +191,22 @@ fun SettingsListItem(
onClick: () -> Unit,
headlineText: String,
trailingContent: @Composable () -> Unit = {},
- supportingContent: @Composable () -> Unit
+ supportingText: String,
+ iconResource: Int
) {
- ListItem(
- colors = ListItemDefaults.colors(containerColor = Color.Transparent),
- headlineContent = { Text(headlineText) },
- supportingContent = supportingContent,
- trailingContent = trailingContent,
- modifier = Modifier
- .clickable(true, onClick = onClick)
- )
+ Card {
+ ListItem(
+ leadingContent = {
+ Icon(painter = painterResource(iconResource), contentDescription = null)
+ },
+ colors = ListItemDefaults.colors(containerColor = Color.Transparent),
+ headlineContent = { Text(headlineText) },
+ supportingContent = { Text(supportingText) },
+ trailingContent = trailingContent,
+ modifier = Modifier
+ .clickable(true, onClick = onClick)
+ )
+ }
}
@Composable
@@ -165,4 +250,35 @@ fun ThemeSelectionDialog(
},
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
+ )
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/screens/statistics/StatisticsScreen.kt b/app/src/main/java/cc/n0th1ng/tripmoney/screens/statistics/StatisticsScreen.kt
index 6fb4ebe..cdca6da 100644
--- a/app/src/main/java/cc/n0th1ng/tripmoney/screens/statistics/StatisticsScreen.kt
+++ b/app/src/main/java/cc/n0th1ng/tripmoney/screens/statistics/StatisticsScreen.kt
@@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@@ -25,38 +24,97 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.ModifierLocalBeyondBoundsLayout
import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.text.TextStyle
-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.res.stringResource
import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
import androidx.core.graphics.toColorInt
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import cc.n0th1ng.tripmoney.data.dto.SummaryPerCategory
import cc.n0th1ng.tripmoney.data.entity.Category
+import cc.n0th1ng.tripmoney.data.entity.Trip
import cc.n0th1ng.tripmoney.theme.TripMoneyTheme
+import cc.n0th1ng.tripmoney.utils.AllPreviews
import cc.n0th1ng.tripmoney.utils.Currencies
import cc.n0th1ng.tripmoney.utils.Icons
import cc.n0th1ng.tripmoney.utils.colors
import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel
import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel
+import cc.n0th1ng.tripmoney.viewmodel.TripViewModel
+import com.composables.icons.materialsymbols.outlined.R
@RequiresApi(Build.VERSION_CODES.O)
@Composable
fun StatisticsScreen() {
val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel()
val settingsViewModel: SettingsViewModel = hiltViewModel()
- val currentTrip by settingsViewModel.currentTrip.collectAsState()
- val summaryPerCategoryList by expenseAndCategoryViewModel.getSummaryPerCategory(currentTrip)
+ val tripViewModel: TripViewModel = hiltViewModel()
+ val currentTripId by settingsViewModel.currentTrip.collectAsState()
+ val currentTrip = tripViewModel.getTrip(currentTripId)
+ val summaryPerCategoryList by expenseAndCategoryViewModel.getSummaryPerCategory(currentTripId)
.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,
+ summaryAmount: Double,
+ tripCurrency: Currencies
+) {
+ Column(
+ modifier = Modifier
+ .padding(10.dp)
+ .fillMaxSize(),
+ verticalArrangement = Arrangement.spacedBy(10.dp)
+ ) {
+ Summary(summaryAmount, tripCurrency.name)
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
fun SummaryPerCategoryCard(summaryPerCategoryList: List) {
@@ -65,7 +123,6 @@ fun SummaryPerCategoryCard(summaryPerCategoryList: List) {
modifier = Modifier.padding(15.dp),
verticalArrangement = Arrangement.spacedBy(5.dp)
) {
-// Text(text = "Summary", fontWeight = FontWeight.Bold, fontSize = 25.sp)
summaryPerCategoryList.forEach {
CategoryCard(
summaryPerCategory = it, modifier = Modifier
@@ -80,21 +137,35 @@ fun SummaryPerCategoryCard(summaryPerCategoryList: List) {
fun CategoryCard(modifier: Modifier = Modifier, summaryPerCategory: SummaryPerCategory) {
Column(modifier = modifier) {
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(
painter = painterResource(summaryPerCategory.category.icon.resource),
contentDescription = null,
modifier = Modifier.size(MaterialTheme.typography.bodyLarge.fontSize.value.dp),
tint = Color(summaryPerCategory.category.color.toColorInt())
)
- Text("%s".format(summaryPerCategory.category.name, (summaryPerCategory.percent * 100).toInt()),
- style = MaterialTheme.typography.bodyLarge, color = Color(summaryPerCategory.category.color.toColorInt()))
+ Text(
+ "%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),
- style = MaterialTheme.typography.bodyMedium)
+ Text(
+ "%.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(
modifier = Modifier
.height(40.dp)
@@ -102,31 +173,34 @@ fun CategoryCard(modifier: Modifier = Modifier, summaryPerCategory: SummaryPerCa
.clip(RoundedCornerShape(16.dp))
.background(MaterialTheme.colorScheme.primary)
) {
- Column(verticalArrangement = Arrangement.Center, modifier = Modifier.fillMaxSize().padding(11.dp)) {
- Text("%d%%".format((summaryPerCategory.percent * 100).toInt()),
- style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onPrimary)
+ Column(
+ verticalArrangement = Arrangement.Center,
+ 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
-fun previewLight() {
+fun Preview() {
TripMoneyTheme {
- SummaryPerCategoryCard(summaryPerCategoryList)
- }
-}
-
-@Preview
-@Composable
-fun previewDark() {
- TripMoneyTheme(darkTheme = true) {
- SummaryPerCategoryCard(summaryPerCategoryList)
+ StatisticsScreen(
+ summaryPerCategoryList,
+ summaryAmount = 125.24,
+ Currencies.entries.random()
+ )
}
}
@@ -143,8 +217,8 @@ val categories = listOf(
val summaryPerCategoryList = listOf(
SummaryPerCategory(categories[0], 50.0, 1f, 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[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),
)
\ No newline at end of file
diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/screens/trippicker/AddTripBottomSheet.kt b/app/src/main/java/cc/n0th1ng/tripmoney/screens/trippicker/AddTripBottomSheet.kt
index 204e6a1..e4474f1 100644
--- a/app/src/main/java/cc/n0th1ng/tripmoney/screens/trippicker/AddTripBottomSheet.kt
+++ b/app/src/main/java/cc/n0th1ng/tripmoney/screens/trippicker/AddTripBottomSheet.kt
@@ -1,5 +1,6 @@
package cc.n0th1ng.tripmoney.screens.trippicker
+import android.annotation.SuppressLint
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.compose.foundation.layout.Arrangement
@@ -17,12 +18,14 @@ import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Shapes
import androidx.compose.material3.SheetState
import androidx.compose.material3.Text
+import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
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.listexpense.CurrencySelectionDialog
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.viewmodel.SettingsViewModel
import io.ktor.http.hostIsIp
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
import java.time.LocalDate
import java.time.format.DateTimeFormatter
@@ -59,6 +67,29 @@ fun AddTripBottomSheet(
tripToEdit: Trip?,
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 startDate by remember {
@@ -66,8 +97,7 @@ fun AddTripBottomSheet(
LocalDate.parse(tripToEdit?.startDate ?: LocalDate.now().toString())
)
}
- val settingsViewModel: SettingsViewModel = hiltViewModel()
- val defaultCurrency by settingsViewModel.defaultCurrency.collectAsState()
+
var showCurrencyDialog by remember { mutableStateOf(false) }
var showDatePicker by remember { mutableStateOf(false) }
var currency by remember { mutableStateOf(tripToEdit?.currency ?: defaultCurrency.name) }
@@ -87,7 +117,7 @@ fun AddTripBottomSheet(
modifier = Modifier
.fillMaxWidth()
.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,
fontSize = 35.sp,
textAlign = TextAlign.Start
@@ -101,32 +131,35 @@ fun AddTripBottomSheet(
modifier = Modifier.fillMaxWidth(0.9f),
horizontalArrangement = Arrangement.spacedBy(10.dp)
) {
- CurrencyButton(
- modifier = Modifier
- .weight(1f)
- .fillMaxWidth(1f),
- onClick = { showCurrencyDialog = true }, text = currency
- )
- OutlinedButton(
+ Button(
modifier = Modifier
.fillMaxWidth(1f)
.weight(1f),
+ shape = MaterialTheme.shapes.medium,
onClick = { showDatePicker = true }) {
Text(
text = startDate.format(DateTimeFormatter.ofPattern("dd.MM.yyyy")),
fontSize = 17.sp
)
}
+ CurrencyButton(
+ modifier = Modifier
+ .weight(1f)
+ .fillMaxWidth(1f),
+ onClick = { showCurrencyDialog = true }, text = currency
+ )
}
Button(
modifier = Modifier.fillMaxWidth(0.9f),
enabled = enableSave,
+ shape = MaterialTheme.shapes.medium,
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(
imageVector = Icons.Filled.Check,
@@ -166,4 +199,41 @@ fun NameInput(name: String, onTextChange: (String) -> Unit) {
onTextChange(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()
+ )
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/utils/AllPreviews.kt b/app/src/main/java/cc/n0th1ng/tripmoney/utils/AllPreviews.kt
index 7449ef9..b94c7d2 100644
--- a/app/src/main/java/cc/n0th1ng/tripmoney/utils/AllPreviews.kt
+++ b/app/src/main/java/cc/n0th1ng/tripmoney/utils/AllPreviews.kt
@@ -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_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.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 = "Dark", uiMode = UI_MODE_NIGHT_YES or UI_MODE_TYPE_NORMAL)
diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/utils/CSVUtils.kt b/app/src/main/java/cc/n0th1ng/tripmoney/utils/CSVUtils.kt
new file mode 100644
index 0000000..e3e03f5
--- /dev/null
+++ b/app/src/main/java/cc/n0th1ng/tripmoney/utils/CSVUtils.kt
@@ -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")
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/viewmodel/ExpenseAndCategoryViewModel.kt b/app/src/main/java/cc/n0th1ng/tripmoney/viewmodel/ExpenseAndCategoryViewModel.kt
index 6153d1f..2ab88d2 100644
--- a/app/src/main/java/cc/n0th1ng/tripmoney/viewmodel/ExpenseAndCategoryViewModel.kt
+++ b/app/src/main/java/cc/n0th1ng/tripmoney/viewmodel/ExpenseAndCategoryViewModel.kt
@@ -18,12 +18,17 @@ import cc.n0th1ng.tripmoney.data.repository.TripRepository
import cc.n0th1ng.tripmoney.utils.Currencies
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
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 javax.inject.Inject
+
@HiltViewModel
open class ExpenseAndCategoryViewModel @Inject constructor(
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 {
+ return getExpensesWithConvertedAmounts(tripId).map { list ->
+ list.sumOf { it.convertedAmount }
+ }
+ }
+
@RequiresApi(Build.VERSION_CODES.O)
fun getSummaryPerCategory(tripId: Int): Flow> {
val tripCurrency = tripRepo.getTrip(tripId)?.currency ?: Currencies.default().name
return getExpensesWithConvertedAmounts(tripId)
.map { list ->
- // Compute summary
val sumOfAll = list.sumOf { it.convertedAmount }
list.groupBy { it.expenseDto.category }
.map { (category, expenses) ->
@@ -84,7 +116,7 @@ open class ExpenseAndCategoryViewModel @Inject constructor(
val convertedAmount =
if (expenseDto.expense.currency != expenseDto.trip.currency) {
runBlocking {
- expenseDto.toExpenseDtoWithConvertedAmount()
+ expenseDto.convertedAmount()
}
} else {
expenseDto.expense.amount
@@ -102,7 +134,7 @@ open class ExpenseAndCategoryViewModel @Inject constructor(
val convertedAmount =
if (expenseDto.expense.currency != expenseDto.trip.currency) {
runBlocking {
- expenseDto.toExpenseDtoWithConvertedAmount()
+ expenseDto.convertedAmount()
}
} else {
expenseDto.expense.amount
@@ -120,7 +152,7 @@ open class ExpenseAndCategoryViewModel @Inject constructor(
}
@RequiresApi(Build.VERSION_CODES.O)
- suspend fun ExpenseDto.toExpenseDtoWithConvertedAmount(): Double {
+ suspend fun ExpenseDto.convertedAmount(): Double {
return exchangeRateRepository.getRate(
Currencies.valueOf(this.expense.currency),
Currencies.valueOf(this.trip.currency),
diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index 508d84a..1fd2749 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -21,4 +21,10 @@
Edytuj wycieczkę
Edytuj wydatek
Domyślna waluta
+ Suma wydatków
+ Ustawienia
+ Wybierz wycieczkę
+ Lista wydatków
+ Statystyki
+ Eksport do CSV
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index f487f89..68bde61 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -21,4 +21,10 @@
Edit trip
Edit expense
Default currency
+ Total expenses
+ Settings
+ Pick trip
+ List of expenses
+ Statistics
+ Export to CSV
\ No newline at end of file
diff --git a/app/src/main/res/xml/file_paths.xml b/app/src/main/res/xml/file_paths.xml
new file mode 100644
index 0000000..af7c742
--- /dev/null
+++ b/app/src/main/res/xml/file_paths.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file