This commit is contained in:
Rafal Wisniewski
2026-03-20 14:32:47 +01:00
parent f625a6975c
commit 96cdd056a0
16 changed files with 596 additions and 124 deletions

View File

@@ -1,8 +1,13 @@
package cc.n0th1ng.tripmoney.screens.addexpense
import android.annotation.SuppressLint
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.compose.foundation.clickable
import androidx.compose.foundation.focusable
import androidx.compose.foundation.interaction.MutableInteractionSource
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.Spacer
@@ -10,16 +15,21 @@ import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.text.KeyboardActions
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.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.SheetState
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
@@ -29,11 +39,17 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
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.layout.ModifierLocalBeyondBoundsLayout
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -50,26 +66,38 @@ import cc.n0th1ng.tripmoney.theme.TripMoneyTheme
import cc.n0th1ng.tripmoney.utils.Currencies
import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel
import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
@OptIn(ExperimentalMaterial3Api::class)
@RequiresApi(Build.VERSION_CODES.O)
@Composable
fun AddExpenseBottomSheet(
onSave: (Expense) -> Unit,
onDismiss: () -> Unit,
categories: List<Category>,
expenseDtoToEdit: ExpenseDto?
expenseDtoToEdit: ExpenseDto?,
state: SheetState,
// categories: List<Category> = emptyList()
) {
val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel()
val settingsViewModel: SettingsViewModel = hiltViewModel()
val currentTripId by settingsViewModel.currentTrip.collectAsState()
// val currentTripId = 1
val categories by expenseAndCategoryViewModel.getCategories().collectAsState(emptyList())
if (categories.isEmpty()) {
return
}
var amount by remember {
mutableStateOf(
expenseDtoToEdit?.expense?.amount?.toString() ?: "0.00"
)
}
val dummyFocusRequester = remember { FocusRequester() }
var showCurrencyDialog by remember { mutableStateOf(false) }
var showCategoryDialog by remember { mutableStateOf(false) }
var showDateTimePicker by remember { mutableStateOf(false) }
@@ -88,21 +116,33 @@ fun AddExpenseBottomSheet(
}
var note by remember { mutableStateOf(expenseDtoToEdit?.expense?.note ?: "") }
var enableSave by remember { mutableStateOf(expenseDtoToEdit != null) }
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
ModalBottomSheet(
onDismissRequest = onDismiss,
sheetState = sheetState,
sheetState = state,
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
.padding(0.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
Text(
modifier = Modifier
.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,
textAlign = TextAlign.Start
)
HorizontalDivider(modifier = Modifier.fillMaxWidth())
Row(
modifier = Modifier.fillMaxWidth(0.9f),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(9.dp)
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = amount.ifEmpty { "0.00" },
@@ -111,41 +151,44 @@ fun AddExpenseBottomSheet(
)
CurrencyButton(onClick = { showCurrencyDialog = true }, text = currency)
}
Spacer(Modifier.height(14.dp))
OutlinedButton(onClick = { showDateTimePicker = true }) {
Text(
text = datetime.format(DateTimeFormatter.ofPattern("dd.MM HH:mm")),
fontSize = 17.sp
)
}
Spacer(Modifier.height(14.dp))
CategoryButton(onClick = { showCategoryDialog = true }, category = category)
Spacer(Modifier.height(14.dp))
Row(
modifier = Modifier.height(50.dp),
modifier = Modifier.fillMaxWidth(0.9f),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(10.dp)
) {
NoteInput(note = note) { newNote -> note = newNote }
SaveButton(
enabled = enableSave,
onClick = {
val expenseToSave = Expense(
amount = amount.toDouble(),
currency = currency,
note = note,
datetime = datetime.toString(),
categoryId = category.id,
tripId = currentTripId
)
onSave(
if (expenseDtoToEdit == null) expenseToSave
else expenseToSave.copy(id = expenseDtoToEdit.expense.id)
)
}
OutlinedButton(
onClick = { showDateTimePicker = true },
modifier = Modifier.weight(1f)
) {
Text(
text = datetime.format(DateTimeFormatter.ofPattern("dd.MM HH:mm")),
fontSize = 17.sp
)
}
CategoryButton(
onClick = { showCategoryDialog = true },
category = category,
modifier = Modifier.weight(1f)
)
}
Row(
verticalAlignment = Alignment.CenterVertically,
) {
NoteInput(
note = note,
onTextChange = { newNote -> note = newNote },
modifier = Modifier.fillMaxWidth(0.9f),
focusRequester = dummyFocusRequester
)
}
Spacer(Modifier.height(14.dp))
Box(
modifier = Modifier
.size(0.dp)
.focusRequester(dummyFocusRequester)
.focusable()
)
NumberKeyboard(
onNumberClick = { number ->
val newText = (if (amount == "0.00") "" else amount) + number
@@ -155,13 +198,28 @@ fun AddExpenseBottomSheet(
} 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.isDoubleTwoDigitsAboveZero()
})
},
onSave = {
val expenseToSave = Expense(
amount = amount.toDouble(),
currency = currency,
note = note,
datetime = datetime.toString(),
categoryId = category.id,
tripId = currentTripId
)
onSave(
if (expenseDtoToEdit == null) expenseToSave
else expenseToSave.copy(id = expenseDtoToEdit.expense.id)
)
}, enableSave = enableSave
)
}
}
@@ -210,29 +268,39 @@ fun String.isDoubleTwoDigitsAboveZero(): Boolean {
}
@Composable
fun NoteInput(note: String, onTextChange: (String) -> Unit) {
fun NoteInput(note: String, onTextChange: (String) -> Unit, modifier: Modifier = Modifier, focusRequester: FocusRequester) {
var text by remember { mutableStateOf(note) }
OutlinedTextField(
modifier = modifier,
label = { Text(stringResource(R.string.note)) }, value = note, onValueChange = { newText ->
text = newText
onTextChange(text)
}, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
},
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Text,
imeAction = ImeAction.Done
),
keyboardActions = KeyboardActions(
onDone = {
focusRequester.requestFocus()
}
)
)
}
@Composable
fun CurrencyButton(onClick: () -> Unit, text: String) {
OutlinedButton(onClick = onClick) {
fun CurrencyButton(modifier: Modifier = Modifier, onClick: () -> Unit, text: String) {
OutlinedButton(onClick = onClick, modifier = modifier) {
Text(text)
}
}
@Composable
fun CategoryButton(onClick: () -> Unit, category: Category) {
fun CategoryButton(onClick: () -> Unit, category: Category, modifier: Modifier = Modifier) {
OutlinedButton(
onClick = onClick,
modifier = Modifier.fillMaxWidth(0.5f)
modifier = modifier
) {
Icon(
modifier = Modifier.padding(end = 10.dp),
@@ -258,20 +326,13 @@ fun SaveButton(enabled: Boolean, onClick: () -> Unit) {
}
}
@Preview
@Composable
fun Preview() {
TripMoneyTheme(darkTheme = true) {
NumberKeyboard(onNumberClick = {}, onBackspaceClick = {})
}
}
@Composable
fun NumberKeyboard(
modifier: Modifier = Modifier,
onNumberClick: (String) -> Unit,
onBackspaceClick: () -> Unit
onBackspaceClick: () -> Unit,
onSave: () -> Unit,
enableSave: Boolean
) {
val buttonModifier = Modifier
.padding(4.dp)
@@ -285,91 +346,109 @@ fun NumberKeyboard(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
OutlinedButton(
TextButton(
onClick = { onNumberClick("1") },
modifier = buttonModifier.weight(1f)
) {
Text("1", fontSize = 20.sp)
}
OutlinedButton(
TextButton(
onClick = { onNumberClick("2") },
modifier = buttonModifier.weight(1f)
) {
Text("2", fontSize = 20.sp)
}
OutlinedButton(
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)
) {
OutlinedButton(
TextButton(
onClick = { onNumberClick("4") },
modifier = buttonModifier.weight(1f)
) {
Text("4", fontSize = 20.sp)
}
OutlinedButton(
TextButton(
onClick = { onNumberClick("5") },
modifier = buttonModifier.weight(1f)
) {
Text("5", fontSize = 20.sp)
}
OutlinedButton(
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)
) {
OutlinedButton(
TextButton(
onClick = { onNumberClick("7") },
modifier = buttonModifier.weight(1f)
) {
Text("7", fontSize = 20.sp)
}
OutlinedButton(
TextButton(
onClick = { onNumberClick("8") },
modifier = buttonModifier.weight(1f)
) {
Text("8", fontSize = 20.sp)
}
OutlinedButton(
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)
) {
OutlinedButton(
TextButton(
onClick = { onNumberClick(".") },
modifier = buttonModifier.weight(1f)
) {
Text(".", fontSize = 20.sp)
}
OutlinedButton(
TextButton(
onClick = { onNumberClick("0") },
modifier = buttonModifier.weight(1f)
) {
Text("0", fontSize = 20.sp)
}
OutlinedButton(
TextButton(
onClick = onBackspaceClick,
modifier = buttonModifier.weight(1f)
) {
@@ -378,6 +457,147 @@ fun NumberKeyboard(
contentDescription = stringResource(R.string.backspace)
)
}
TextButton(
onClick = onSave,
modifier = buttonModifier.weight(1f),
enabled = enableSave
) {
Icon(
imageVector = Icons.Default.Check,
contentDescription = stringResource(R.string.backspace)
)
}
}
}
}
}
//@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"
// ),
// )
// )
// }
//}

View File

@@ -5,6 +5,7 @@ import android.os.Build
import androidx.annotation.RequiresApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -33,6 +34,7 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.SwipeToDismissBox
import androidx.compose.material3.SwipeToDismissBoxValue
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.material3.rememberSwipeToDismissBoxState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
@@ -56,10 +58,14 @@ import cc.n0th1ng.tripmoney.R.string
import cc.n0th1ng.tripmoney.data.entity.Expense
import cc.n0th1ng.tripmoney.data.entity.ExpenseDto
import cc.n0th1ng.tripmoney.screens.addexpense.AddExpenseBottomSheet
import cc.n0th1ng.tripmoney.service.ExchangeService
import cc.n0th1ng.tripmoney.utils.Currencies
import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel
import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import javax.inject.Inject
@OptIn(ExperimentalMaterial3Api::class)
@@ -71,8 +77,6 @@ fun ListExpenseScreen() {
val settingsViewModel: SettingsViewModel = hiltViewModel()
val currentTrip by settingsViewModel.currentTrip.collectAsState()
val categories by expenseAndCategoryViewModel.getCategories()
.collectAsState(initial = emptyList())
val expenses = expenseAndCategoryViewModel.getExpenses(currentTrip).collectAsLazyPagingItems()
val listState = rememberLazyListState()
var showBottomSheet by remember { mutableStateOf(false) }
@@ -98,27 +102,13 @@ fun ListExpenseScreen() {
val expenseDto = expenses[index]
if (expenseDto != null) {
val previousExpense = expenses.itemSnapshotList.items.getOrNull(index - 1)
val showDayDivider =
index == 0 || LocalDateTime.parse(previousExpense?.expense?.datetime)
.toLocalDate() != LocalDateTime.parse(expenseDto.expense.datetime)
.toLocalDate()
Spacer(Modifier.height(5.dp))
if (showDayDivider) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Absolute.Center,
verticalAlignment = Alignment.CenterVertically
) {
HorizontalDivider(modifier = Modifier.weight(1f))
Text(
LocalDateTime.parse(expenseDto.expense.datetime).format(
DateTimeFormatter.ofPattern("dd EEEE")
).toString(),
modifier = Modifier.background(Color.White.copy(alpha = 0f))
)
HorizontalDivider(modifier = Modifier.weight(1f))
}
CustomDivider(expenseDto)
}
Spacer(Modifier.height(5.dp))
SwipeToDeleteExpenseCard(
@@ -143,13 +133,32 @@ fun ListExpenseScreen() {
expenseDtoToEdit = null
showBottomSheet = false
},
categories = categories,
expenseDtoToEdit = expenseDtoToEdit
expenseDtoToEdit = expenseDtoToEdit,
state = rememberModalBottomSheetState(skipPartiallyExpanded = true)
)
}
}
}
@RequiresApi(Build.VERSION_CODES.O)
@Composable
fun CustomDivider(expenseDto: ExpenseDto) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Absolute.Center,
verticalAlignment = Alignment.CenterVertically
) {
HorizontalDivider(modifier = Modifier.weight(1f))
Text(
LocalDateTime.parse(expenseDto.expense.datetime).format(
DateTimeFormatter.ofPattern("dd EEEE")
).toString(),
modifier = Modifier.background(Color.White.copy(alpha = 0f))
)
HorizontalDivider(modifier = Modifier.weight(1f))
}
}
@RequiresApi(Build.VERSION_CODES.O)
@Composable
fun SwipeToDeleteExpenseCard(
@@ -257,7 +266,10 @@ fun ExpenseCard(expenseDto: ExpenseDto, onClick: (ExpenseDto) -> Unit) {
modifier = Modifier
.fillMaxWidth(0.9f)
.height(70.dp)
.clickable { onClick(expenseDto) },
.combinedClickable(
enabled = true,
onClick = { onClick(expenseDto) },
onLongClick = { onClick(expenseDto) }),
elevation = CardDefaults.cardElevation(defaultElevation = 7.dp)
) {
Row(
@@ -312,8 +324,16 @@ fun ExpenseCard(expenseDto: ExpenseDto, onClick: (ExpenseDto) -> Unit) {
fontWeight = FontWeight.Bold
)
if (expenseDto.expense.currency.lowercase() != expenseDto.trip.currency.lowercase()) {
val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel()
val amount by
expenseAndCategoryViewModel.convertAmount(
amount = expenseDto.expense.amount,
base = Currencies.valueOf(expenseDto.expense.currency),
target = Currencies.valueOf(expenseDto.trip.currency),
date = LocalDateTime.parse(expenseDto.expense.datetime).toLocalDate()
).collectAsState(initial = 0.0)
Text(
text = "≈ %.2f ${expenseDto.trip.currency}".format(expenseDto.expense.amount),
text = "≈ %.2f ${expenseDto.trip.currency}".format(amount),
fontSize = 12.sp
)
}

View File

@@ -2,20 +2,27 @@ package cc.n0th1ng.tripmoney.screens.trippicker
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
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.TextButton
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -23,31 +30,32 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import cc.n0th1ng.tripmoney.R
import cc.n0th1ng.tripmoney.R.string
import cc.n0th1ng.tripmoney.data.entity.Trip
import cc.n0th1ng.tripmoney.screens.addexpense.CurrencyButton
import cc.n0th1ng.tripmoney.screens.addexpense.isDoubleTwoDigitsAboveZero
import cc.n0th1ng.tripmoney.screens.listexpense.CurrencySelectionDialog
import cc.n0th1ng.tripmoney.screens.listexpense.DatePicker
import cc.n0th1ng.tripmoney.screens.listexpense.DateTimePicker
import cc.n0th1ng.tripmoney.utils.Currencies
import java.time.Instant
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
@RequiresApi(Build.VERSION_CODES.O)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AddTripBottomSheet(onDismiss: () -> Unit, onSave: (Trip) -> Unit, tripToEdit: Trip?) {
fun AddTripBottomSheet(
onDismiss: () -> Unit,
onSave: (Trip) -> Unit,
tripToEdit: Trip?,
sheetState: SheetState
) {
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
var name by remember { mutableStateOf(tripToEdit?.name ?: "") }
var startDate by remember {
mutableStateOf(
@@ -65,31 +73,61 @@ fun AddTripBottomSheet(onDismiss: () -> Unit, onSave: (Trip) -> Unit, tripToEdit
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalAlignment = Alignment.Start
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
Text(
modifier = Modifier
.fillMaxWidth()
.padding(start = 15.dp),
text = stringResource(if(tripToEdit == null) R.string.add_trip else R.string.edit_trip),
fontWeight = FontWeight.Bold,
fontSize = 35.sp,
textAlign = TextAlign.Start
)
HorizontalDivider(modifier = Modifier.fillMaxWidth())
NameInput(name = name, onTextChange = { newText ->
name = newText
enableSave = !name.isEmpty()
})
CurrencyButton(onClick = {showCurrencyDialog = true}, currency)
OutlinedButton(onClick = { showDatePicker = true }) {
Text(
text = startDate.format(DateTimeFormatter.ofPattern("dd.MM.yyyy")),
fontSize = 17.sp
Row(
modifier = Modifier.fillMaxWidth(0.9f),
horizontalArrangement = Arrangement.spacedBy(10.dp)
) {
CurrencyButton(
modifier = Modifier
.weight(1f)
.fillMaxWidth(1f),
onClick = { showCurrencyDialog = true }, text = currency
)
OutlinedButton(
modifier = Modifier
.fillMaxWidth(1f)
.weight(1f),
onClick = { showDatePicker = true }) {
Text(
text = startDate.format(DateTimeFormatter.ofPattern("dd.MM.yyyy")),
fontSize = 17.sp
)
}
}
OutlinedButton(
Button(
modifier = Modifier.fillMaxWidth(0.9f),
enabled = enableSave,
onClick = {
onSave(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))
}) {
Icon(
imageVector = Icons.Filled.Check,
contentDescription = stringResource(R.string.save)
)
}
Spacer(Modifier.height(5.dp))
}
}
@@ -105,7 +143,7 @@ fun AddTripBottomSheet(onDismiss: () -> Unit, onSave: (Trip) -> Unit, tripToEdit
}
if (showDatePicker) {
DatePicker(startDate, onDismiss = {showDatePicker = false}, onConfirm = { newDate ->
DatePicker(startDate, onDismiss = { showDatePicker = false }, onConfirm = { newDate ->
startDate = newDate
showDatePicker = false
})
@@ -116,10 +154,10 @@ fun AddTripBottomSheet(onDismiss: () -> Unit, onSave: (Trip) -> Unit, tripToEdit
fun NameInput(name: String, onTextChange: (String) -> Unit) {
var text by remember { mutableStateOf(name) }
OutlinedTextField(
modifier = Modifier.fillMaxWidth(0.9f),
label = { Text(stringResource(R.string.name)) }, value = name, onValueChange = { newText ->
text = newText
onTextChange(text)
}, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
)
}
}

View File

@@ -5,6 +5,7 @@ import android.os.Build
import androidx.annotation.RequiresApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -19,6 +20,7 @@ import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.FabPosition
import androidx.compose.material3.FloatingActionButton
@@ -28,6 +30,7 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.SwipeToDismissBox
import androidx.compose.material3.SwipeToDismissBoxValue
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.material3.rememberSwipeToDismissBoxState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
@@ -39,6 +42,8 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
@@ -56,6 +61,7 @@ import cc.n0th1ng.tripmoney.screens.listexpense.DeleteConfirmationDialog
import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel
import cc.n0th1ng.tripmoney.viewmodel.TripViewModel
@OptIn(ExperimentalMaterial3Api::class)
@RequiresApi(Build.VERSION_CODES.O)
@Composable
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@@ -67,6 +73,7 @@ fun TripPickerScreen(
var showBottomSheet by remember { mutableStateOf(false) }
val trips: LazyPagingItems<Trip> = tripViewModel.getTrips().collectAsLazyPagingItems()
val currentTripId by settingsViewModel.currentTrip.collectAsState()
var tripToEdit by remember { mutableStateOf<Trip?>(null) }
Scaffold(floatingActionButtonPosition = FabPosition.EndOverlay, floatingActionButton = {
FloatingActionButton(
onClick = { showBottomSheet = true }) {
@@ -83,12 +90,17 @@ fun TripPickerScreen(
Spacer(Modifier.height(10.dp))
val trip = trips[i]
if (trip != null) {
SwipeToDeleteTripCard(trip, onDelete = {
SwipeToDeleteTripCard(
trip, onDelete = {
tripViewModel.delete(trip)
}, onClick = {
settingsViewModel.setCurrentTrip(trip.id)
navController.navigate(Screens.LIST_EXPENSE)
}, isSelected = currentTripId == trip.id)
}, isSelected = currentTripId == trip.id,
onLongClick = { trip ->
tripToEdit = trip
showBottomSheet = true
})
}
Spacer(Modifier.height(10.dp))
}
@@ -98,12 +110,15 @@ fun TripPickerScreen(
AddTripBottomSheet(
onDismiss = {
showBottomSheet = false
tripToEdit = null
},
onSave = { trip ->
tripViewModel.save(trip)
showBottomSheet = false
tripToEdit = null
},
null
tripToEdit = tripToEdit,
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
)
}
}
@@ -112,7 +127,8 @@ fun TripPickerScreen(
@RequiresApi(Build.VERSION_CODES.O)
@Composable
fun SwipeToDeleteTripCard(
trip: Trip, onDelete: (Trip) -> Unit, onClick: (Trip) -> Unit, isSelected: Boolean
trip: Trip, onDelete: (Trip) -> Unit, onClick: (Trip) -> Unit, isSelected: Boolean,
onLongClick: (Trip) -> Unit
) {
var dismissed by remember { mutableStateOf(false) }
var showDialog by remember { mutableStateOf(false) }
@@ -151,18 +167,27 @@ fun SwipeToDeleteTripCard(
Icon(Icons.Default.Delete, contentDescription = stringResource(string.delete))
}
}) {
TripCard(trip, isSelected, onClick = onClick)
TripCard(trip, isSelected, onClick = onClick, onLongClick = onLongClick)
}
}
}
@Composable
fun TripCard(trip: Trip, isSelected: Boolean, onClick: (Trip) -> Unit) {
fun TripCard(
trip: Trip,
isSelected: Boolean,
onClick: (Trip) -> Unit,
onLongClick: (Trip) -> Unit
) {
val haptics = LocalHapticFeedback.current
ElevatedCard(
modifier = Modifier
.height(100.dp)
.clickable(true, onClick = { onClick(trip) }),
.combinedClickable(enabled = true, onLongClick = {
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
onLongClick(trip)
}, onClick = { onClick(trip) }),
elevation = CardDefaults.cardElevation(defaultElevation = if (isSelected) 7.dp else 0.dp)
) {
Row(