This commit is contained in:
Rafal Wisniewski
2026-03-19 21:02:10 +01:00
parent b074c98f7d
commit f625a6975c
13 changed files with 408 additions and 123 deletions

View File

@@ -2,6 +2,7 @@ package cc.n0th1ng.tripmoney.data.dao
import androidx.paging.PagingSource import androidx.paging.PagingSource
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert import androidx.room.Insert
import androidx.room.Query import androidx.room.Query
import androidx.room.Upsert import androidx.room.Upsert
@@ -19,4 +20,6 @@ interface TripDao {
) )
fun tripsPaged(): PagingSource<Int, Trip> fun tripsPaged(): PagingSource<Int, Trip>
@Delete
suspend fun delete(trip: Trip)
} }

View File

@@ -22,4 +22,9 @@ class TripRepository @Inject constructor(private val tripDao: TripDao) {
pagingSourceFactory = { tripDao.tripsPaged() } pagingSourceFactory = { tripDao.tripsPaged() }
).flow ).flow
} }
@WorkerThread
suspend fun delete(trip: Trip) {
tripDao.delete(trip)
}
} }

View File

@@ -38,6 +38,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.core.graphics.toColorInt import androidx.core.graphics.toColorInt
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import cc.n0th1ng.tripmoney.R import cc.n0th1ng.tripmoney.R
import cc.n0th1ng.tripmoney.data.entity.Category import cc.n0th1ng.tripmoney.data.entity.Category
import cc.n0th1ng.tripmoney.data.entity.Expense import cc.n0th1ng.tripmoney.data.entity.Expense
@@ -46,9 +47,11 @@ import cc.n0th1ng.tripmoney.screens.listexpense.CategorySelectionDialog
import cc.n0th1ng.tripmoney.screens.listexpense.CurrencySelectionDialog import cc.n0th1ng.tripmoney.screens.listexpense.CurrencySelectionDialog
import cc.n0th1ng.tripmoney.screens.listexpense.DateTimePicker import cc.n0th1ng.tripmoney.screens.listexpense.DateTimePicker
import cc.n0th1ng.tripmoney.theme.TripMoneyTheme import cc.n0th1ng.tripmoney.theme.TripMoneyTheme
import cc.n0th1ng.tripmoney.utils.Currencies
import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel
import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@@ -57,11 +60,10 @@ import java.time.LocalDateTime
fun AddExpenseBottomSheet( fun AddExpenseBottomSheet(
onSave: (Expense) -> Unit, onSave: (Expense) -> Unit,
onDismiss: () -> Unit, onDismiss: () -> Unit,
settingsViewModel: SettingsViewModel,
categories: List<Category>, categories: List<Category>,
expenseAndCategoryViewModel: ExpenseAndCategoryViewModel,
expenseDtoToEdit: ExpenseDto? expenseDtoToEdit: ExpenseDto?
) { ) {
val settingsViewModel: SettingsViewModel = hiltViewModel()
val currentTripId by settingsViewModel.currentTrip.collectAsState() val currentTripId by settingsViewModel.currentTrip.collectAsState()
var amount by remember { var amount by remember {
mutableStateOf( mutableStateOf(
@@ -70,11 +72,18 @@ fun AddExpenseBottomSheet(
} }
var showCurrencyDialog by remember { mutableStateOf(false) } var showCurrencyDialog by remember { mutableStateOf(false) }
var showCategoryDialog by remember { mutableStateOf(false) } var showCategoryDialog by remember { mutableStateOf(false) }
var 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 category by remember { mutableStateOf(expenseDtoToEdit?.category ?: categories[0]) }
var datetime by remember { var datetime by remember {
mutableStateOf( 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 ?: "") } var note by remember { mutableStateOf(expenseDtoToEdit?.expense?.note ?: "") }
@@ -103,10 +112,12 @@ fun AddExpenseBottomSheet(
CurrencyButton(onClick = { showCurrencyDialog = true }, text = currency) CurrencyButton(onClick = { showCurrencyDialog = true }, text = currency)
} }
Spacer(Modifier.height(14.dp)) Spacer(Modifier.height(14.dp))
DateTimePicker( OutlinedButton(onClick = { showDateTimePicker = true }) {
dateTime = datetime, Text(
onChange = { datetime = it } text = datetime.format(DateTimeFormatter.ofPattern("dd.MM HH:mm")),
) fontSize = 17.sp
)
}
Spacer(Modifier.height(14.dp)) Spacer(Modifier.height(14.dp))
CategoryButton(onClick = { showCategoryDialog = true }, category = category) CategoryButton(onClick = { showCategoryDialog = true }, category = category)
Spacer(Modifier.height(14.dp)) Spacer(Modifier.height(14.dp))
@@ -155,7 +166,12 @@ fun AddExpenseBottomSheet(
} }
} }
if (showDateTimePicker) {
DateTimePicker(datetime, onChange = { newDateTime ->
datetime = newDateTime
showDateTimePicker = false
})
}
if (showCurrencyDialog) { if (showCurrencyDialog) {
CurrencySelectionDialog( CurrencySelectionDialog(
@@ -164,8 +180,7 @@ fun AddExpenseBottomSheet(
showCurrencyDialog = false showCurrencyDialog = false
currency = selectedCurrency currency = selectedCurrency
}, },
selected = currency, selected = currency
listOfCurrencies = listOf("PLN", "EUR", "USD")
) )
} }
@@ -177,8 +192,7 @@ fun AddExpenseBottomSheet(
category = selectedCategory category = selectedCategory
}, },
selected = category, selected = category,
categories = categories, categories = categories
settingsAndCategoryViewModel = expenseAndCategoryViewModel
) )
} }
} }

View File

@@ -24,6 +24,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.graphics.toColorInt import androidx.core.graphics.toColorInt
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import cc.n0th1ng.tripmoney.R.* import cc.n0th1ng.tripmoney.R.*
import cc.n0th1ng.tripmoney.data.entity.Category import cc.n0th1ng.tripmoney.data.entity.Category
import cc.n0th1ng.tripmoney.screens.AddCategoryDialog import cc.n0th1ng.tripmoney.screens.AddCategoryDialog
@@ -36,9 +37,9 @@ fun CategorySelectionDialog(
onDismiss: () -> Unit, onDismiss: () -> Unit,
onCategorySelected: (Category) -> Unit, onCategorySelected: (Category) -> Unit,
selected: Category, selected: Category,
categories: List<Category>, categories: List<Category>
settingsAndCategoryViewModel: ExpenseAndCategoryViewModel
) { ) {
val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel()
val listState = rememberLazyListState() val listState = rememberLazyListState()
var showAddCategoryDialog by remember { mutableStateOf(false) } var showAddCategoryDialog by remember { mutableStateOf(false) }
AlertDialog( AlertDialog(
@@ -99,7 +100,7 @@ fun CategorySelectionDialog(
AddCategoryDialog(onDismiss = { AddCategoryDialog(onDismiss = {
showAddCategoryDialog = false showAddCategoryDialog = false
}, onSave = { category -> }, onSave = { category ->
settingsAndCategoryViewModel.save(category) expenseAndCategoryViewModel.save(category)
showAddCategoryDialog = false showAddCategoryDialog = false
}) })
} }

View File

@@ -14,20 +14,20 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import cc.n0th1ng.tripmoney.R import cc.n0th1ng.tripmoney.R
import cc.n0th1ng.tripmoney.utils.Currencies
@Composable @Composable
fun CurrencySelectionDialog( fun CurrencySelectionDialog(
onDismiss: () -> Unit, onDismiss: () -> Unit,
onCurrencySelected: (String) -> Unit, onCurrencySelected: (String) -> Unit,
selected: String, selected: String
listOfCurrencies: List<String>
) { ) {
AlertDialog( AlertDialog(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
title = { Text(stringResource(R.string.pick_currency)) }, title = { Text(stringResource(R.string.pick_currency)) },
text = { text = {
Column { Column {
listOfCurrencies.forEach { currency -> Currencies.names().forEach { currency ->
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()

View File

@@ -5,11 +5,13 @@ import androidx.annotation.RequiresApi
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.DatePicker import androidx.compose.material3.DatePicker
import androidx.compose.material3.DatePickerDialog import androidx.compose.material3.DatePickerDialog
import androidx.compose.material3.DatePickerState
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.TimePicker import androidx.compose.material3.TimePicker
import androidx.compose.material3.TimePickerState
import androidx.compose.material3.rememberDatePickerState import androidx.compose.material3.rememberDatePickerState
import androidx.compose.material3.rememberTimePickerState import androidx.compose.material3.rememberTimePickerState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -20,11 +22,73 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import cc.n0th1ng.tripmoney.R.* import cc.n0th1ng.tripmoney.R.*
import java.sql.Time
import java.time.Instant import java.time.Instant
import java.time.LocalDate
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.LocalTime import java.time.LocalTime
import java.time.ZoneId import java.time.ZoneId
import java.time.format.DateTimeFormatter 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 @Composable
@RequiresApi(Build.VERSION_CODES.O) @RequiresApi(Build.VERSION_CODES.O)
@@ -33,76 +97,34 @@ fun DateTimePicker(
dateTime: LocalDateTime = LocalDateTime.now(), dateTime: LocalDateTime = LocalDateTime.now(),
onChange: (LocalDateTime) -> Unit 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) } var showTimePicker by remember { mutableStateOf(false) }
var date by remember { mutableStateOf(dateTime.toLocalDate()) }
val formatter = DateTimeFormatter.ofPattern("dd.MM HH:mm")
OutlinedButton(onClick = { showDatePicker = true }) {
Text(text = dateTime.format(formatter), fontSize = 17.sp)
}
if (showDatePicker) { if (showDatePicker) {
DatePickerDialog( DatePicker(onDismiss = { showDatePicker = false }, onConfirm = { newDate ->
onDismissRequest = { showDatePicker = false }, date = newDate
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)
}
} }
if (showTimePicker) { if (showTimePicker) {
AlertDialog( TimePicker(onDismiss = {
onDismissRequest = { showTimePicker = false }, showTimePicker = false
confirmButton = { showDatePicker = true
TextButton(onClick = { }, onConfirm = { timePickerState ->
showTimePicker = false showTimePicker = false
val newTime = LocalTime.of(timePickerState.hour, timePickerState.minute) showDatePicker = true
onChange(LocalDateTime.of(dateTime.toLocalDate(), newTime)) val newTime = LocalTime.of(timePickerState.hour, timePickerState.minute)
}) { onChange(LocalDateTime.of(date, newTime))
Text("OK") })
}
},
dismissButton = {
TextButton(onClick = { showTimePicker = false }) { Text(stringResource(string.cancel)) }
},
text = { TimePicker(state = timePickerState) }
)
} }
} }
@RequiresApi(Build.VERSION_CODES.O) @RequiresApi(Build.VERSION_CODES.O)
fun LocalDateTime.toEpochMilli(): Long = fun LocalDateTime.toEpochMilli(): Long =
this.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli() this.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()
@RequiresApi(Build.VERSION_CODES.O)
fun LocalDate.toEpochMilli(): Long =
this.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()

View File

@@ -2,7 +2,6 @@ package cc.n0th1ng.tripmoney.screens.listexpense
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.os.Build import android.os.Build
import androidx.activity.viewModels
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
@@ -53,7 +52,7 @@ import androidx.compose.ui.unit.sp
import androidx.core.graphics.toColorInt import androidx.core.graphics.toColorInt
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.paging.compose.collectAsLazyPagingItems 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.Expense
import cc.n0th1ng.tripmoney.data.entity.ExpenseDto import cc.n0th1ng.tripmoney.data.entity.ExpenseDto
import cc.n0th1ng.tripmoney.screens.addexpense.AddExpenseBottomSheet import cc.n0th1ng.tripmoney.screens.addexpense.AddExpenseBottomSheet
@@ -61,7 +60,6 @@ import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel
import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import kotlin.getValue
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@@ -145,9 +143,7 @@ fun ListExpenseScreen() {
expenseDtoToEdit = null expenseDtoToEdit = null
showBottomSheet = false showBottomSheet = false
}, },
settingsViewModel = settingsViewModel,
categories = categories, categories = categories,
expenseAndCategoryViewModel = expenseAndCategoryViewModel,
expenseDtoToEdit = expenseDtoToEdit expenseDtoToEdit = expenseDtoToEdit
) )
} }
@@ -287,8 +283,8 @@ fun ExpenseCard(expenseDto: ExpenseDto, onClick: (ExpenseDto) -> Unit) {
.fillMaxHeight() .fillMaxHeight()
.padding(vertical = 8.dp) .padding(vertical = 8.dp)
) { ) {
Column( Column()
) { {
Text( Text(
text = expenseDto.category.name, text = expenseDto.category.name,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,

View File

@@ -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)
)
}

View File

@@ -1,5 +1,9 @@
package cc.n0th1ng.tripmoney.screens.trippicker 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.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box 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.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.runtime.Composable import androidx.compose.material.icons.Icons
import androidx.paging.compose.LazyPagingItems import androidx.compose.material.icons.filled.Add
import androidx.paging.compose.collectAsLazyPagingItems import androidx.compose.material.icons.filled.Delete
import androidx.paging.compose.itemKey
import cc.n0th1ng.tripmoney.data.entity.Trip
import cc.n0th1ng.tripmoney.viewmodel.TripViewModel
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ElevatedCard 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.Text
import androidx.compose.material3.rememberSwipeToDismissBoxState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
import androidx.compose.ui.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.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.NavController 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.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.SettingsViewModel
import cc.n0th1ng.tripmoney.viewmodel.TripViewModel
@RequiresApi(Build.VERSION_CODES.O)
@Composable @Composable
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
fun TripPickerScreen( fun TripPickerScreen(
navController: NavController navController: NavController
) { ) {
val settingsViewModel: SettingsViewModel = hiltViewModel() val settingsViewModel: SettingsViewModel = hiltViewModel()
val tripViewModel: TripViewModel = hiltViewModel() val tripViewModel: TripViewModel = hiltViewModel()
var showBottomSheet by remember { mutableStateOf(false) }
val trips: LazyPagingItems<Trip> = tripViewModel.getTrips().collectAsLazyPagingItems() val trips: LazyPagingItems<Trip> = tripViewModel.getTrips().collectAsLazyPagingItems()
val currentTripId by settingsViewModel.currentTrip.collectAsState() val currentTripId by settingsViewModel.currentTrip.collectAsState()
Scaffold(floatingActionButtonPosition = FabPosition.EndOverlay, floatingActionButton = {
LazyColumn( FloatingActionButton(
modifier = Modifier onClick = { showBottomSheet = true }) {
.padding(horizontal = 15.dp) Icon(Icons.Filled.Add, stringResource(string.add_trip))
.fillMaxSize(), }
verticalArrangement = Arrangement.Center }) { paddingValues ->
) { LazyColumn(
modifier = Modifier
items(trips.itemCount, trips.itemKey { it.id }) { i -> .padding(horizontal = 15.dp)
Spacer(Modifier.height(10.dp)) .fillMaxSize(),
val trip = trips[i] verticalArrangement = Arrangement.Center
if (trip != null) { ) {
TripCard(trip, currentTripId == trip.id, onClick = { items(trips.itemCount, trips.itemKey { it.id }) { i ->
settingsViewModel.setCurrentTrip(trip.id) Spacer(Modifier.height(10.dp))
navController.navigate(Screens.LIST_EXPENSE) 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 @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( ElevatedCard(
modifier = Modifier modifier = Modifier
.height(100.dp) .height(100.dp)
.clickable(true, onClick = onClick) .clickable(true, onClick = { onClick(trip) }),
.alpha(if (isSelected) 1.0f else 0.7f),
elevation = CardDefaults.cardElevation(defaultElevation = if (isSelected) 7.dp else 0.dp) elevation = CardDefaults.cardElevation(defaultElevation = if (isSelected) 7.dp else 0.dp)
) { ) {
Row( Row(
@@ -83,8 +171,7 @@ fun TripCard(trip: Trip, isSelected: Boolean, onClick: () -> Unit) {
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier.padding(16.dp)
.padding(16.dp)
) { ) {
Text(fontSize = 25.sp, fontWeight = FontWeight.SemiBold, text = trip.name) Text(fontSize = 25.sp, fontWeight = FontWeight.SemiBold, text = trip.name)
Text(trip.startDate) Text(trip.startDate)
@@ -96,6 +183,5 @@ fun TripCard(trip: Trip, isSelected: Boolean, onClick: () -> Unit) {
fontWeight = FontWeight.SemiBold fontWeight = FontWeight.SemiBold
) )
} }
} }
} }

View File

@@ -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<String> {
return Currencies.entries.map { it.name }
}
}
}

View File

@@ -5,10 +5,12 @@ import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.paging.PagingData import androidx.paging.PagingData
import androidx.paging.cachedIn import androidx.paging.cachedIn
import cc.n0th1ng.tripmoney.data.entity.Expense
import cc.n0th1ng.tripmoney.data.entity.Trip import cc.n0th1ng.tripmoney.data.entity.Trip
import cc.n0th1ng.tripmoney.data.repository.TripRepository import cc.n0th1ng.tripmoney.data.repository.TripRepository
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
@@ -16,4 +18,16 @@ class TripViewModel @Inject constructor(private val repository: TripRepository)
fun getTrips(): Flow<PagingData<Trip>> = repository.getTrips().cachedIn(viewModelScope) fun getTrips(): Flow<PagingData<Trip>> = repository.getTrips().cachedIn(viewModelScope)
fun delete(trip: Trip) {
viewModelScope.launch {
repository.delete(trip)
}
}
fun save(trip: Trip) {
viewModelScope.launch {
repository.save(trip)
}
}
} }

View File

@@ -16,4 +16,6 @@
<string name="system_settings">Zgodnie z systemem</string> <string name="system_settings">Zgodnie z systemem</string>
<string name="theme">Motyw</string> <string name="theme">Motyw</string>
<string name="dark_theme">Ciemny motyw</string> <string name="dark_theme">Ciemny motyw</string>
<string name="add_trip">Dodaj Wycieczkę</string>
<string name="name">Nazwa</string>
</resources> </resources>

View File

@@ -16,5 +16,6 @@
<string name="light_theme">Light theme</string> <string name="light_theme">Light theme</string>
<string name="pick_theme">Pick a theme</string> <string name="pick_theme">Pick a theme</string>
<string name="system_settings">System settings</string> <string name="system_settings">System settings</string>
<!-- <string name="theme">Theme</string>--> <string name="add_trip">Add Trip</string>
<string name="name">Name</string>
</resources> </resources>