diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/data/dao/TripDao.kt b/app/src/main/java/cc/n0th1ng/tripmoney/data/dao/TripDao.kt index 3ed8e32..89098e6 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/data/dao/TripDao.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/data/dao/TripDao.kt @@ -2,6 +2,7 @@ package cc.n0th1ng.tripmoney.data.dao import androidx.paging.PagingSource import androidx.room.Dao +import androidx.room.Delete import androidx.room.Insert import androidx.room.Query import androidx.room.Upsert @@ -19,4 +20,6 @@ interface TripDao { ) fun tripsPaged(): PagingSource + @Delete + suspend fun delete(trip: Trip) } diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/data/repository/TripRepository.kt b/app/src/main/java/cc/n0th1ng/tripmoney/data/repository/TripRepository.kt index 423c630..d0109ea 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/data/repository/TripRepository.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/data/repository/TripRepository.kt @@ -22,4 +22,9 @@ class TripRepository @Inject constructor(private val tripDao: TripDao) { pagingSourceFactory = { tripDao.tripsPaged() } ).flow } + + @WorkerThread + suspend fun delete(trip: Trip) { + tripDao.delete(trip) + } } \ No newline at end of file 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 7fd4d38..9efca80 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 @@ -38,6 +38,7 @@ import androidx.compose.ui.tooling.preview.Preview 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.R import cc.n0th1ng.tripmoney.data.entity.Category import cc.n0th1ng.tripmoney.data.entity.Expense @@ -46,9 +47,11 @@ 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.Currencies import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel import java.time.LocalDateTime +import java.time.format.DateTimeFormatter @OptIn(ExperimentalMaterial3Api::class) @@ -57,11 +60,10 @@ import java.time.LocalDateTime fun AddExpenseBottomSheet( onSave: (Expense) -> Unit, onDismiss: () -> Unit, - settingsViewModel: SettingsViewModel, categories: List, - expenseAndCategoryViewModel: ExpenseAndCategoryViewModel, expenseDtoToEdit: ExpenseDto? ) { + val settingsViewModel: SettingsViewModel = hiltViewModel() val currentTripId by settingsViewModel.currentTrip.collectAsState() var amount by remember { mutableStateOf( @@ -70,11 +72,18 @@ fun AddExpenseBottomSheet( } var showCurrencyDialog by remember { mutableStateOf(false) } var showCategoryDialog by remember { mutableStateOf(false) } - var currency by remember { mutableStateOf(expenseDtoToEdit?.expense?.currency ?: "PLN") } + var showDateTimePicker by remember { mutableStateOf(false) } + var currency by remember { + mutableStateOf( + expenseDtoToEdit?.expense?.currency ?: Currencies.PLN.name + ) + } var category by remember { mutableStateOf(expenseDtoToEdit?.category ?: categories[0]) } var datetime by remember { mutableStateOf( - LocalDateTime.parse(expenseDtoToEdit?.expense?.datetime ?: LocalDateTime.now().toString()) + LocalDateTime.parse( + expenseDtoToEdit?.expense?.datetime ?: LocalDateTime.now().toString() + ) ) } var note by remember { mutableStateOf(expenseDtoToEdit?.expense?.note ?: "") } @@ -103,10 +112,12 @@ fun AddExpenseBottomSheet( CurrencyButton(onClick = { showCurrencyDialog = true }, text = currency) } Spacer(Modifier.height(14.dp)) - DateTimePicker( - dateTime = datetime, - onChange = { datetime = it } - ) + 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)) @@ -155,7 +166,12 @@ fun AddExpenseBottomSheet( } } - + if (showDateTimePicker) { + DateTimePicker(datetime, onChange = { newDateTime -> + datetime = newDateTime + showDateTimePicker = false + }) + } if (showCurrencyDialog) { CurrencySelectionDialog( @@ -164,8 +180,7 @@ fun AddExpenseBottomSheet( showCurrencyDialog = false currency = selectedCurrency }, - selected = currency, - listOfCurrencies = listOf("PLN", "EUR", "USD") + selected = currency ) } @@ -177,8 +192,7 @@ fun AddExpenseBottomSheet( category = selectedCategory }, selected = category, - categories = categories, - settingsAndCategoryViewModel = expenseAndCategoryViewModel + categories = categories ) } } diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/screens/listexpense/CategorySelectionDialog.kt b/app/src/main/java/cc/n0th1ng/tripmoney/screens/listexpense/CategorySelectionDialog.kt index a2f1c09..40dbfc4 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/screens/listexpense/CategorySelectionDialog.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/screens/listexpense/CategorySelectionDialog.kt @@ -24,6 +24,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.core.graphics.toColorInt +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import cc.n0th1ng.tripmoney.R.* import cc.n0th1ng.tripmoney.data.entity.Category import cc.n0th1ng.tripmoney.screens.AddCategoryDialog @@ -36,9 +37,9 @@ fun CategorySelectionDialog( onDismiss: () -> Unit, onCategorySelected: (Category) -> Unit, selected: Category, - categories: List, - settingsAndCategoryViewModel: ExpenseAndCategoryViewModel + categories: List ) { + val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel() val listState = rememberLazyListState() var showAddCategoryDialog by remember { mutableStateOf(false) } AlertDialog( @@ -99,7 +100,7 @@ fun CategorySelectionDialog( AddCategoryDialog(onDismiss = { showAddCategoryDialog = false }, onSave = { category -> - settingsAndCategoryViewModel.save(category) + expenseAndCategoryViewModel.save(category) showAddCategoryDialog = false }) } diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/screens/listexpense/CurrencySelectionDialog.kt b/app/src/main/java/cc/n0th1ng/tripmoney/screens/listexpense/CurrencySelectionDialog.kt index 9492974..3d7a6fb 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/screens/listexpense/CurrencySelectionDialog.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/screens/listexpense/CurrencySelectionDialog.kt @@ -14,20 +14,20 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import cc.n0th1ng.tripmoney.R +import cc.n0th1ng.tripmoney.utils.Currencies @Composable fun CurrencySelectionDialog( onDismiss: () -> Unit, onCurrencySelected: (String) -> Unit, - selected: String, - listOfCurrencies: List + selected: String ) { AlertDialog( onDismissRequest = onDismiss, title = { Text(stringResource(R.string.pick_currency)) }, text = { Column { - listOfCurrencies.forEach { currency -> + Currencies.names().forEach { currency -> Row( modifier = Modifier .fillMaxWidth() diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/screens/listexpense/DateTimePicker.kt b/app/src/main/java/cc/n0th1ng/tripmoney/screens/listexpense/DateTimePicker.kt index eacd587..f4f57c9 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/screens/listexpense/DateTimePicker.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/screens/listexpense/DateTimePicker.kt @@ -5,11 +5,13 @@ import androidx.annotation.RequiresApi import androidx.compose.material3.AlertDialog import androidx.compose.material3.DatePicker import androidx.compose.material3.DatePickerDialog +import androidx.compose.material3.DatePickerState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TimePicker +import androidx.compose.material3.TimePickerState import androidx.compose.material3.rememberDatePickerState import androidx.compose.material3.rememberTimePickerState import androidx.compose.runtime.Composable @@ -20,11 +22,73 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.sp import cc.n0th1ng.tripmoney.R.* +import java.sql.Time import java.time.Instant +import java.time.LocalDate import java.time.LocalDateTime import java.time.LocalTime import java.time.ZoneId import java.time.format.DateTimeFormatter +import java.util.Calendar + +@RequiresApi(Build.VERSION_CODES.O) +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DatePicker( + dateTime: LocalDate = LocalDate.now(), + onDismiss: () -> Unit, + onConfirm: (LocalDate) -> Unit +) { + val datePickerState = + rememberDatePickerState(initialSelectedDateMillis = dateTime.toEpochMilli()) + + DatePickerDialog( + onDismissRequest = onDismiss, + confirmButton = { + TextButton(onClick = { + val selectedMillis = datePickerState.selectedDateMillis + if (selectedMillis != null) { + val selectedDate = Instant.ofEpochMilli(selectedMillis) + .atZone(ZoneId.systemDefault()) + .toLocalDate() + onConfirm(selectedDate) + } + }) { + Text("OK") + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { Text(stringResource(string.cancel)) } + } + ) { + DatePicker(state = datePickerState) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TimePicker(onDismiss: () -> Unit, onConfirm: (TimePickerState) -> Unit) { + val currentTime = Calendar.getInstance() + val timePickerState = rememberTimePickerState( + initialHour = currentTime.get(Calendar.HOUR_OF_DAY), + initialMinute = currentTime.get(Calendar.MINUTE), + is24Hour = true + ) + + AlertDialog( + onDismissRequest = onDismiss, + confirmButton = { + TextButton(onClick = { onConfirm(timePickerState) }) { + Text("OK") + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { Text(stringResource(string.cancel)) } + }, + text = { TimePicker(state = timePickerState) } + ) + +} @Composable @RequiresApi(Build.VERSION_CODES.O) @@ -33,76 +97,34 @@ fun DateTimePicker( dateTime: LocalDateTime = LocalDateTime.now(), onChange: (LocalDateTime) -> Unit ) { - val datePickerState = - rememberDatePickerState(initialSelectedDateMillis = dateTime.toEpochMilli()) - val timePickerState = rememberTimePickerState( - initialHour = dateTime.hour, - initialMinute = dateTime.minute - ) - var showDatePicker by remember { mutableStateOf(false) } + var showDatePicker by remember { mutableStateOf(true) } var showTimePicker by remember { mutableStateOf(false) } - - - val formatter = DateTimeFormatter.ofPattern("dd.MM HH:mm") - OutlinedButton(onClick = { showDatePicker = true }) { - Text(text = dateTime.format(formatter), fontSize = 17.sp) - } + var date by remember { mutableStateOf(dateTime.toLocalDate()) } if (showDatePicker) { - DatePickerDialog( - onDismissRequest = { showDatePicker = false }, - confirmButton = { - TextButton(onClick = { - showDatePicker = false - val selectedMillis = datePickerState.selectedDateMillis - if (selectedMillis != null) { - val selectedDate = Instant.ofEpochMilli(selectedMillis) - .atZone(ZoneId.systemDefault()) - .toLocalDate() - // open time picker next - showTimePicker = true - onChange( - LocalDateTime.of( - selectedDate, - dateTime.toLocalTime() - ) - ) - } - }) { - Text("OK") - } - }, - dismissButton = { - TextButton(onClick = { - showDatePicker = false - }) { Text(stringResource(string.cancel)) } - } - ) { - DatePicker(state = datePickerState) - } + DatePicker(onDismiss = { showDatePicker = false }, onConfirm = { newDate -> + date = newDate + }) } if (showTimePicker) { - AlertDialog( - onDismissRequest = { showTimePicker = false }, - confirmButton = { - TextButton(onClick = { - showTimePicker = false - val newTime = LocalTime.of(timePickerState.hour, timePickerState.minute) - onChange(LocalDateTime.of(dateTime.toLocalDate(), newTime)) - }) { - Text("OK") - } - }, - dismissButton = { - TextButton(onClick = { showTimePicker = false }) { Text(stringResource(string.cancel)) } - }, - text = { TimePicker(state = timePickerState) } - ) + TimePicker(onDismiss = { + showTimePicker = false + showDatePicker = true + }, onConfirm = { timePickerState -> + showTimePicker = false + showDatePicker = true + val newTime = LocalTime.of(timePickerState.hour, timePickerState.minute) + onChange(LocalDateTime.of(date, newTime)) + }) } } @RequiresApi(Build.VERSION_CODES.O) fun LocalDateTime.toEpochMilli(): Long = - this.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli() \ No newline at end of file + this.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli() + +@RequiresApi(Build.VERSION_CODES.O) +fun LocalDate.toEpochMilli(): Long = + this.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli() \ 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 161e7e4..3f3f864 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 @@ -2,7 +2,6 @@ package cc.n0th1ng.tripmoney.screens.listexpense import android.annotation.SuppressLint import android.os.Build -import androidx.activity.viewModels import androidx.annotation.RequiresApi import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -53,7 +52,7 @@ import androidx.compose.ui.unit.sp import androidx.core.graphics.toColorInt import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.paging.compose.collectAsLazyPagingItems -import cc.n0th1ng.tripmoney.R.* +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 @@ -61,7 +60,6 @@ import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel import java.time.LocalDateTime import java.time.format.DateTimeFormatter -import kotlin.getValue @OptIn(ExperimentalMaterial3Api::class) @@ -145,9 +143,7 @@ fun ListExpenseScreen() { expenseDtoToEdit = null showBottomSheet = false }, - settingsViewModel = settingsViewModel, categories = categories, - expenseAndCategoryViewModel = expenseAndCategoryViewModel, expenseDtoToEdit = expenseDtoToEdit ) } @@ -287,8 +283,8 @@ fun ExpenseCard(expenseDto: ExpenseDto, onClick: (ExpenseDto) -> Unit) { .fillMaxHeight() .padding(vertical = 8.dp) ) { - Column( - ) { + Column() + { Text( text = expenseDto.category.name, fontWeight = FontWeight.Bold, 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 new file mode 100644 index 0000000..d0103f1 --- /dev/null +++ b/app/src/main/java/cc/n0th1ng/tripmoney/screens/trippicker/AddTripBottomSheet.kt @@ -0,0 +1,125 @@ +package cc.n0th1ng.tripmoney.screens.trippicker + +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +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.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.OutlinedTextField +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 +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardType +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?) { + + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + var name by remember { mutableStateOf(tripToEdit?.name ?: "") } + var startDate by remember { + mutableStateOf( + LocalDate.parse(tripToEdit?.startDate ?: LocalDate.now().toString()) + ) + } + var showCurrencyDialog by remember { mutableStateOf(false) } + var showDatePicker by remember { mutableStateOf(false) } + var currency by remember { mutableStateOf(tripToEdit?.currency ?: Currencies.default().name) } + var enableSave by remember { mutableStateOf(tripToEdit != null) } + + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = sheetState, + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalAlignment = Alignment.Start + ) { + 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 + ) + } + OutlinedButton( + enabled = enableSave, + onClick = { + onSave(Trip(name = name, startDate = startDate.toString(), currency = currency)) + }) { + Icon( + imageVector = Icons.Filled.Check, + contentDescription = stringResource(R.string.save) + ) + } + } + } + + if (showCurrencyDialog) { + CurrencySelectionDialog( + onDismiss = { showCurrencyDialog = false }, + onCurrencySelected = { selectedCurrency -> + showCurrencyDialog = false + currency = selectedCurrency + }, + selected = currency + ) + } + + if (showDatePicker) { + DatePicker(startDate, onDismiss = {showDatePicker = false}, onConfirm = { newDate -> + startDate = newDate + showDatePicker = false + }) + } +} + +@Composable +fun NameInput(name: String, onTextChange: (String) -> Unit) { + var text by remember { mutableStateOf(name) } + OutlinedTextField( + label = { Text(stringResource(R.string.name)) }, value = name, onValueChange = { newText -> + text = newText + onTextChange(text) + }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text) + ) +} + diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/screens/trippicker/TripPickerScreen.kt b/app/src/main/java/cc/n0th1ng/tripmoney/screens/trippicker/TripPickerScreen.kt index 00b7b75..4f3450e 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/screens/trippicker/TripPickerScreen.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/screens/trippicker/TripPickerScreen.kt @@ -1,5 +1,9 @@ package cc.n0th1ng.tripmoney.screens.trippicker +import android.annotation.SuppressLint +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -7,74 +11,158 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.runtime.Composable -import androidx.paging.compose.LazyPagingItems -import androidx.paging.compose.collectAsLazyPagingItems -import androidx.paging.compose.itemKey -import cc.n0th1ng.tripmoney.data.entity.Trip -import cc.n0th1ng.tripmoney.viewmodel.TripViewModel +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Delete import androidx.compose.material3.CardDefaults -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.ExtendedFloatingActionButton +import androidx.compose.material3.FabPosition +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SwipeToDismissBox +import androidx.compose.material3.SwipeToDismissBoxValue import androidx.compose.material3.Text +import androidx.compose.material3.rememberSwipeToDismissBoxState +import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha -import androidx.compose.ui.layout.ModifierLocalBeyondBoundsLayout +import androidx.compose.ui.draw.clip +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.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.navigation.NavController +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.collectAsLazyPagingItems +import androidx.paging.compose.itemKey +import cc.n0th1ng.tripmoney.R.string +import cc.n0th1ng.tripmoney.data.entity.Trip import cc.n0th1ng.tripmoney.navigation.Screens +import cc.n0th1ng.tripmoney.screens.addexpense.AddExpenseBottomSheet +import cc.n0th1ng.tripmoney.screens.listexpense.DeleteConfirmationDialog import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel +import cc.n0th1ng.tripmoney.viewmodel.TripViewModel +@RequiresApi(Build.VERSION_CODES.O) @Composable +@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") fun TripPickerScreen( navController: NavController ) { - val settingsViewModel: SettingsViewModel = hiltViewModel() val tripViewModel: TripViewModel = hiltViewModel() + var showBottomSheet by remember { mutableStateOf(false) } val trips: LazyPagingItems = tripViewModel.getTrips().collectAsLazyPagingItems() val currentTripId by settingsViewModel.currentTrip.collectAsState() - - LazyColumn( - modifier = Modifier - .padding(horizontal = 15.dp) - .fillMaxSize(), - verticalArrangement = Arrangement.Center - ) { - - items(trips.itemCount, trips.itemKey { it.id }) { i -> - Spacer(Modifier.height(10.dp)) - val trip = trips[i] - if (trip != null) { - TripCard(trip, currentTripId == trip.id, onClick = { - settingsViewModel.setCurrentTrip(trip.id) - navController.navigate(Screens.LIST_EXPENSE) - }) + Scaffold(floatingActionButtonPosition = FabPosition.EndOverlay, floatingActionButton = { + FloatingActionButton( + onClick = { showBottomSheet = true }) { + Icon(Icons.Filled.Add, stringResource(string.add_trip)) + } + }) { paddingValues -> + LazyColumn( + modifier = Modifier + .padding(horizontal = 15.dp) + .fillMaxSize(), + verticalArrangement = Arrangement.Center + ) { + items(trips.itemCount, trips.itemKey { it.id }) { i -> + Spacer(Modifier.height(10.dp)) + val trip = trips[i] + if (trip != null) { + SwipeToDeleteTripCard(trip, onDelete = { + tripViewModel.delete(trip) + }, onClick = { + settingsViewModel.setCurrentTrip(trip.id) + navController.navigate(Screens.LIST_EXPENSE) + }, isSelected = currentTripId == trip.id) + } + Spacer(Modifier.height(10.dp)) } - Spacer(Modifier.height(10.dp)) + } + + if (showBottomSheet) { + AddTripBottomSheet( + onDismiss = { + showBottomSheet = false + }, + onSave = { trip -> + tripViewModel.save(trip) + showBottomSheet = false + }, + null + ) } } - - } +@RequiresApi(Build.VERSION_CODES.O) @Composable -fun TripCard(trip: Trip, isSelected: Boolean, onClick: () -> Unit) { +fun SwipeToDeleteTripCard( + trip: Trip, onDelete: (Trip) -> Unit, onClick: (Trip) -> Unit, isSelected: Boolean +) { + var dismissed by remember { mutableStateOf(false) } + var showDialog by remember { mutableStateOf(false) } + + if (!dismissed) { + val dismissState = rememberSwipeToDismissBoxState( + confirmValueChange = { dismissValue -> + if (dismissValue == SwipeToDismissBoxValue.EndToStart) { + showDialog = true + false + } else { + false + } + }) + if (showDialog) { + DeleteConfirmationDialog(onConfirm = { + showDialog = false + dismissed = true + onDelete(trip) + }, onCancel = { showDialog = false }) + } + + SwipeToDismissBox( + modifier = Modifier.alpha(if (isSelected) 1.0f else 0.7f), + state = dismissState, + enableDismissFromStartToEnd = false, + backgroundContent = { + Box( + Modifier + .clip(CardDefaults.elevatedShape) + .fillMaxSize() + .background(MaterialTheme.colorScheme.onError) + .padding(horizontal = 20.dp), + contentAlignment = Alignment.CenterEnd + ) { + Icon(Icons.Default.Delete, contentDescription = stringResource(string.delete)) + } + }) { + TripCard(trip, isSelected, onClick = onClick) + } + } +} + + +@Composable +fun TripCard(trip: Trip, isSelected: Boolean, onClick: (Trip) -> Unit) { ElevatedCard( modifier = Modifier .height(100.dp) - .clickable(true, onClick = onClick) - .alpha(if (isSelected) 1.0f else 0.7f), + .clickable(true, onClick = { onClick(trip) }), elevation = CardDefaults.cardElevation(defaultElevation = if (isSelected) 7.dp else 0.dp) ) { Row( @@ -83,8 +171,7 @@ fun TripCard(trip: Trip, isSelected: Boolean, onClick: () -> Unit) { verticalAlignment = Alignment.CenterVertically ) { Column( - modifier = Modifier - .padding(16.dp) + modifier = Modifier.padding(16.dp) ) { Text(fontSize = 25.sp, fontWeight = FontWeight.SemiBold, text = trip.name) Text(trip.startDate) @@ -96,6 +183,5 @@ fun TripCard(trip: Trip, isSelected: Boolean, onClick: () -> Unit) { fontWeight = FontWeight.SemiBold ) } - } } \ No newline at end of file diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/utils/Currencies.kt b/app/src/main/java/cc/n0th1ng/tripmoney/utils/Currencies.kt new file mode 100644 index 0000000..4963490 --- /dev/null +++ b/app/src/main/java/cc/n0th1ng/tripmoney/utils/Currencies.kt @@ -0,0 +1,16 @@ +package cc.n0th1ng.tripmoney.utils + +enum class Currencies { + PLN, + EUR, + USD; + + companion object { + fun default(): Currencies { + return PLN + } + fun names(): List { + return Currencies.entries.map { it.name } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/viewmodel/TripViewModel.kt b/app/src/main/java/cc/n0th1ng/tripmoney/viewmodel/TripViewModel.kt index 5f4f9d3..3686396 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/viewmodel/TripViewModel.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/viewmodel/TripViewModel.kt @@ -5,10 +5,12 @@ import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import androidx.paging.PagingData import androidx.paging.cachedIn +import cc.n0th1ng.tripmoney.data.entity.Expense import cc.n0th1ng.tripmoney.data.entity.Trip import cc.n0th1ng.tripmoney.data.repository.TripRepository import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel @@ -16,4 +18,16 @@ class TripViewModel @Inject constructor(private val repository: TripRepository) fun getTrips(): Flow> = repository.getTrips().cachedIn(viewModelScope) + fun delete(trip: Trip) { + viewModelScope.launch { + repository.delete(trip) + } + } + + fun save(trip: Trip) { + viewModelScope.launch { + repository.save(trip) + } + } + } \ No newline at end of file diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index e1a5d91..2a0a4c3 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -16,4 +16,6 @@ Zgodnie z systemem Motyw Ciemny motyw + Dodaj WycieczkÄ™ + Nazwa \ 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 bf677c9..c5216a9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -16,5 +16,6 @@ Light theme Pick a theme System settings - + Add Trip + Name \ No newline at end of file