init
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
package cc.n0th1ng.tripmoney.screens
|
||||
|
||||
import android.graphics.drawable.Icon
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
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.aspectRatio
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
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.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.graphics.toColorInt
|
||||
import cc.n0th1ng.tripmoney.data.entity.Category
|
||||
import cc.n0th1ng.tripmoney.theme.TripMoneyTheme
|
||||
import cc.n0th1ng.tripmoney.utils.Colors
|
||||
import cc.n0th1ng.tripmoney.utils.Icons
|
||||
|
||||
@Composable
|
||||
fun AddCategoryDialog(onDismiss: () -> Unit, onSave: (Category) -> Unit) {
|
||||
var name by remember { mutableStateOf("") }
|
||||
var icon by remember { mutableStateOf(Icons.entries[0]) }
|
||||
var color by remember { mutableStateOf(Colors.entries[0].hexString) }
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss, title = { Text("Add new category") }, text = {
|
||||
AlertDialogFill(
|
||||
onTextChange = { newText ->
|
||||
name = newText
|
||||
},
|
||||
onIconChange = { newIcon -> icon = newIcon },
|
||||
onColorChange = {newColor -> color = newColor}
|
||||
)
|
||||
}, confirmButton = {
|
||||
Button(
|
||||
enabled = !name.isEmpty(),
|
||||
onClick = {
|
||||
onSave(
|
||||
Category(
|
||||
name = name,
|
||||
icon = icon,
|
||||
color = color
|
||||
)
|
||||
)
|
||||
}) { Text("Save") }
|
||||
},
|
||||
dismissButton = {
|
||||
Button(
|
||||
colors = ButtonDefaults.buttonColors(MaterialTheme.colorScheme.error),
|
||||
onClick = onDismiss
|
||||
) { Text("close") }
|
||||
})
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AlertDialogFill(onTextChange: (String) -> Unit, onIconChange: (Icons) -> Unit, onColorChange: (String) -> Unit) {
|
||||
var text by remember { mutableStateOf("") }
|
||||
var iconId by remember { mutableIntStateOf(Icons.entries[0].resource) }
|
||||
var colorHex by remember { mutableStateOf(Colors.entries[0].hexString) }
|
||||
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(30.dp),
|
||||
painter = painterResource(iconId), contentDescription = null,
|
||||
tint = Color(colorHex.toColorInt())
|
||||
)
|
||||
OutlinedTextField(label = { Text("Name") }, value = text, onValueChange = { newText ->
|
||||
text = newText
|
||||
onTextChange(text)
|
||||
})
|
||||
}
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
modifier = Modifier.horizontalScroll(
|
||||
rememberScrollState()
|
||||
)
|
||||
) {
|
||||
Icons.entries.forEach { icon ->
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.size(30.dp)
|
||||
.clickable(onClick = {
|
||||
iconId = icon.resource
|
||||
onIconChange(icon)
|
||||
}),
|
||||
painter = painterResource(icon.resource),
|
||||
contentDescription = null,
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
modifier = Modifier.horizontalScroll(
|
||||
rememberScrollState()
|
||||
)
|
||||
) {
|
||||
Colors.entries.forEach { color ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clickable(onClick = {
|
||||
colorHex = color.hexString
|
||||
onColorChange(colorHex)
|
||||
})
|
||||
.size(30.dp)
|
||||
.aspectRatio(1f)
|
||||
.background(Color(color.hexString.toColorInt()))
|
||||
) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
package cc.n0th1ng.tripmoney.screens.addexpense
|
||||
|
||||
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.aspectRatio
|
||||
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.automirrored.filled.ArrowBack
|
||||
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.rememberModalBottomSheetState
|
||||
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.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
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 cc.n0th1ng.tripmoney.R
|
||||
import cc.n0th1ng.tripmoney.data.entity.Category
|
||||
import cc.n0th1ng.tripmoney.data.entity.Expense
|
||||
import cc.n0th1ng.tripmoney.data.entity.ExpenseDto
|
||||
import cc.n0th1ng.tripmoney.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.viewmodel.ExpenseAndCategoryViewModel
|
||||
import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel
|
||||
import java.time.LocalDateTime
|
||||
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
@Composable
|
||||
fun AddExpenseBottomSheet(
|
||||
onSave: (Expense) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
settingsViewModel: SettingsViewModel,
|
||||
categories: List<Category>,
|
||||
expenseAndCategoryViewModel: ExpenseAndCategoryViewModel,
|
||||
expenseDtoToEdit: ExpenseDto?
|
||||
) {
|
||||
val currentTripId by settingsViewModel.currentTrip.collectAsState()
|
||||
var amount by remember {
|
||||
mutableStateOf(
|
||||
expenseDtoToEdit?.expense?.amount?.toString() ?: "0.00"
|
||||
)
|
||||
}
|
||||
var showCurrencyDialog by remember { mutableStateOf(false) }
|
||||
var showCategoryDialog by remember { mutableStateOf(false) }
|
||||
var currency by remember { mutableStateOf(expenseDtoToEdit?.expense?.currency ?: "PLN") }
|
||||
var category by remember { mutableStateOf(expenseDtoToEdit?.category ?: categories[0]) }
|
||||
var datetime by remember {
|
||||
mutableStateOf(
|
||||
LocalDateTime.parse(expenseDtoToEdit?.expense?.datetime ?: LocalDateTime.now().toString())
|
||||
)
|
||||
}
|
||||
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,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(9.dp)
|
||||
) {
|
||||
Text(
|
||||
text = amount.ifEmpty { "0.00" },
|
||||
fontSize = 25.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
CurrencyButton(onClick = { showCurrencyDialog = true }, text = currency)
|
||||
}
|
||||
Spacer(Modifier.height(14.dp))
|
||||
DateTimePicker(
|
||||
dateTime = datetime,
|
||||
onChange = { datetime = it }
|
||||
)
|
||||
Spacer(Modifier.height(14.dp))
|
||||
CategoryButton(onClick = { showCategoryDialog = true }, category = category)
|
||||
Spacer(Modifier.height(14.dp))
|
||||
Row(
|
||||
modifier = Modifier.height(50.dp),
|
||||
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)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(14.dp))
|
||||
NumberKeyboard(
|
||||
onNumberClick = { number ->
|
||||
val newText = (if (amount == "0.00") "" else amount) + number
|
||||
if (newText.isDoubleTwoDigitsAboveZero()) {
|
||||
amount = newText
|
||||
enableSave = true
|
||||
} else if (amount == "0.00") {
|
||||
enableSave = false
|
||||
}
|
||||
|
||||
},
|
||||
onBackspaceClick = {
|
||||
if (amount == "0.00") return@NumberKeyboard
|
||||
amount = amount.safeSubstring(0, amount.length - 1)
|
||||
enableSave = amount.isDoubleTwoDigitsAboveZero()
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (showCurrencyDialog) {
|
||||
CurrencySelectionDialog(
|
||||
onDismiss = { showCurrencyDialog = false },
|
||||
onCurrencySelected = { selectedCurrency ->
|
||||
showCurrencyDialog = false
|
||||
currency = selectedCurrency
|
||||
},
|
||||
selected = currency,
|
||||
listOfCurrencies = listOf("PLN", "EUR", "USD")
|
||||
)
|
||||
}
|
||||
|
||||
if (showCategoryDialog) {
|
||||
CategorySelectionDialog(
|
||||
onDismiss = { showCategoryDialog = false },
|
||||
onCategorySelected = { selectedCategory ->
|
||||
showCategoryDialog = false
|
||||
category = selectedCategory
|
||||
},
|
||||
selected = category,
|
||||
categories = categories,
|
||||
settingsAndCategoryViewModel = expenseAndCategoryViewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun String.safeSubstring(start: Int, end: Int): String {
|
||||
return try {
|
||||
this.substring(start, end)
|
||||
} catch (e: Exception) {
|
||||
"0.00"
|
||||
}
|
||||
}
|
||||
|
||||
fun String.isDoubleTwoDigitsAboveZero(): Boolean {
|
||||
return this.toDoubleOrNull() != null && this.matches(Regex("^\\d*(\\.\\d{0,2})?$")) && this.toDouble() > 0
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NoteInput(note: String, onTextChange: (String) -> Unit) {
|
||||
var text by remember { mutableStateOf(note) }
|
||||
|
||||
OutlinedTextField(
|
||||
label = { Text(stringResource(R.string.note)) }, value = note, onValueChange = { newText ->
|
||||
text = newText
|
||||
onTextChange(text)
|
||||
}, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CurrencyButton(onClick: () -> Unit, text: String) {
|
||||
OutlinedButton(onClick = onClick) {
|
||||
Text(text)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CategoryButton(onClick: () -> Unit, category: Category) {
|
||||
OutlinedButton(
|
||||
onClick = onClick,
|
||||
modifier = Modifier.fillMaxWidth(0.5f)
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.padding(end = 10.dp),
|
||||
painter = painterResource(category.icon.resource),
|
||||
contentDescription = stringResource(R.string.category),
|
||||
tint = Color(category.color.toColorInt())
|
||||
)
|
||||
Text(category.name, color = Color(category.color.toColorInt()))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SaveButton(enabled: Boolean, onClick: () -> Unit) {
|
||||
OutlinedButton(
|
||||
onClick = onClick,
|
||||
enabled = enabled,
|
||||
modifier = Modifier
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Check,
|
||||
contentDescription = stringResource(R.string.save)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun Preview() {
|
||||
TripMoneyTheme(darkTheme = true) {
|
||||
NumberKeyboard(onNumberClick = {}, onBackspaceClick = {})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun NumberKeyboard(
|
||||
modifier: Modifier = Modifier,
|
||||
onNumberClick: (String) -> Unit,
|
||||
onBackspaceClick: () -> Unit
|
||||
) {
|
||||
val buttonModifier = Modifier
|
||||
.padding(4.dp)
|
||||
.aspectRatio(2f)
|
||||
|
||||
Column(
|
||||
modifier = modifier,
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
OutlinedButton(
|
||||
onClick = { onNumberClick("1") },
|
||||
modifier = buttonModifier.weight(1f)
|
||||
) {
|
||||
Text("1", fontSize = 20.sp)
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = { onNumberClick("2") },
|
||||
modifier = buttonModifier.weight(1f)
|
||||
) {
|
||||
Text("2", fontSize = 20.sp)
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = { onNumberClick("3") },
|
||||
modifier = buttonModifier.weight(1f)
|
||||
) {
|
||||
Text("3", fontSize = 20.sp)
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
OutlinedButton(
|
||||
onClick = { onNumberClick("4") },
|
||||
modifier = buttonModifier.weight(1f)
|
||||
) {
|
||||
Text("4", fontSize = 20.sp)
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = { onNumberClick("5") },
|
||||
modifier = buttonModifier.weight(1f)
|
||||
) {
|
||||
Text("5", fontSize = 20.sp)
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = { onNumberClick("6") },
|
||||
modifier = buttonModifier.weight(1f)
|
||||
) {
|
||||
Text("6", fontSize = 20.sp)
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
OutlinedButton(
|
||||
onClick = { onNumberClick("7") },
|
||||
modifier = buttonModifier.weight(1f)
|
||||
) {
|
||||
Text("7", fontSize = 20.sp)
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = { onNumberClick("8") },
|
||||
modifier = buttonModifier.weight(1f)
|
||||
) {
|
||||
Text("8", fontSize = 20.sp)
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = { onNumberClick("9") },
|
||||
modifier = buttonModifier.weight(1f)
|
||||
) {
|
||||
Text("9", fontSize = 20.sp)
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
OutlinedButton(
|
||||
onClick = { onNumberClick(".") },
|
||||
modifier = buttonModifier.weight(1f)
|
||||
) {
|
||||
Text(".", fontSize = 20.sp)
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = { onNumberClick("0") },
|
||||
modifier = buttonModifier.weight(1f)
|
||||
) {
|
||||
Text("0", fontSize = 20.sp)
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = onBackspaceClick,
|
||||
modifier = buttonModifier.weight(1f)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = stringResource(R.string.backspace)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package cc.n0th1ng.tripmoney.screens.listexpense
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Text
|
||||
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.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.graphics.toColorInt
|
||||
import cc.n0th1ng.tripmoney.R.*
|
||||
import cc.n0th1ng.tripmoney.data.entity.Category
|
||||
import cc.n0th1ng.tripmoney.screens.AddCategoryDialog
|
||||
import cc.n0th1ng.tripmoney.utils.Icons
|
||||
import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel
|
||||
import com.composables.icons.materialsymbols.outlined.R
|
||||
|
||||
@Composable
|
||||
fun CategorySelectionDialog(
|
||||
onDismiss: () -> Unit,
|
||||
onCategorySelected: (Category) -> Unit,
|
||||
selected: Category,
|
||||
categories: List<Category>,
|
||||
settingsAndCategoryViewModel: ExpenseAndCategoryViewModel
|
||||
) {
|
||||
val listState = rememberLazyListState()
|
||||
var showAddCategoryDialog by remember { mutableStateOf(false) }
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss, title = { Text(stringResource(string.pick_category)) }, text = {
|
||||
Column {
|
||||
LazyColumn(
|
||||
modifier = Modifier.heightIn(max = 300.dp),
|
||||
state = listState,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
items(
|
||||
count = categories.size,
|
||||
key = { index -> categories[index].id }) { index ->
|
||||
val category = categories[index]
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
onCategorySelected(category)
|
||||
}
|
||||
.padding(vertical = 0.dp),
|
||||
verticalAlignment = Alignment.CenterVertically) {
|
||||
RadioButton(
|
||||
selected = selected == category, onClick = {
|
||||
onCategorySelected(category)
|
||||
})
|
||||
Icon(
|
||||
painter = painterResource(category.icon.resource),
|
||||
contentDescription = stringResource(string.category),
|
||||
tint = Color(category.color.toColorInt())
|
||||
)
|
||||
Text(
|
||||
text = category.name, modifier = Modifier.padding(start = 8.dp),
|
||||
color = Color(category.color.toColorInt())
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
showAddCategoryDialog = true
|
||||
}
|
||||
.padding(top = 15.dp),
|
||||
verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.materialsymbols_ic_add_outlined),
|
||||
contentDescription = stringResource(string.category)
|
||||
)
|
||||
Text(
|
||||
text = stringResource(string.add_new_category), modifier = Modifier.padding(start = 8.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}, confirmButton = {})
|
||||
if (showAddCategoryDialog) {
|
||||
AddCategoryDialog(onDismiss = {
|
||||
showAddCategoryDialog = false
|
||||
}, onSave = { category ->
|
||||
settingsAndCategoryViewModel.save(category)
|
||||
showAddCategoryDialog = false
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package cc.n0th1ng.tripmoney.screens.listexpense
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import cc.n0th1ng.tripmoney.R
|
||||
|
||||
@Composable
|
||||
fun CurrencySelectionDialog(
|
||||
onDismiss: () -> Unit,
|
||||
onCurrencySelected: (String) -> Unit,
|
||||
selected: String,
|
||||
listOfCurrencies: List<String>
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(stringResource(R.string.pick_currency)) },
|
||||
text = {
|
||||
Column {
|
||||
listOfCurrencies.forEach { currency ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
onCurrencySelected(currency)
|
||||
}
|
||||
.padding(vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically) {
|
||||
RadioButton(
|
||||
selected = selected == currency, onClick = {
|
||||
onCurrencySelected(currency)
|
||||
})
|
||||
Text(
|
||||
text = currency, modifier = Modifier.padding(start = 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {})
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package cc.n0th1ng.tripmoney.screens.listexpense
|
||||
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.DatePicker
|
||||
import androidx.compose.material3.DatePickerDialog
|
||||
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.rememberDatePickerState
|
||||
import androidx.compose.material3.rememberTimePickerState
|
||||
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.res.stringResource
|
||||
import androidx.compose.ui.unit.sp
|
||||
import cc.n0th1ng.tripmoney.R.*
|
||||
import java.time.Instant
|
||||
import java.time.LocalDateTime
|
||||
import java.time.LocalTime
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
@Composable
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
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 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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
fun LocalDateTime.toEpochMilli(): Long =
|
||||
this.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()
|
||||
@@ -0,0 +1,328 @@
|
||||
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
|
||||
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
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
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.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material3.BasicAlertDialog
|
||||
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.HorizontalDivider
|
||||
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.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.graphics.toColorInt
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import cc.n0th1ng.tripmoney.R.*
|
||||
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.viewmodel.ExpenseAndCategoryViewModel
|
||||
import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import kotlin.getValue
|
||||
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
@Composable
|
||||
fun ListExpenseScreen() {
|
||||
val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel()
|
||||
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) }
|
||||
var expenseDtoToEdit: ExpenseDto? = null
|
||||
|
||||
Scaffold(floatingActionButtonPosition = FabPosition.EndOverlay, floatingActionButton = {
|
||||
ExtendedFloatingActionButton(
|
||||
onClick = { showBottomSheet = true },
|
||||
icon = { Icon(Icons.Filled.Add, stringResource(string.add_expense)) },
|
||||
text = { Text(text = stringResource(string.add_expense)) },
|
||||
)
|
||||
})
|
||||
{
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
state = listState
|
||||
) {
|
||||
items(
|
||||
count = expenses.itemCount,
|
||||
key = { index -> expenses[index]?.expense?.id ?: index }
|
||||
) { index ->
|
||||
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))
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(5.dp))
|
||||
SwipeToDeleteExpenseCard(
|
||||
expenseDto = expenseDto,
|
||||
onDelete = { expense -> expenseAndCategoryViewModel.delete(expense) },
|
||||
onClick = { expenseDto ->
|
||||
expenseDtoToEdit = expenseDto
|
||||
showBottomSheet = true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
if (showBottomSheet) {
|
||||
AddExpenseBottomSheet(
|
||||
onSave = { expense ->
|
||||
expenseAndCategoryViewModel.save(expense)
|
||||
showBottomSheet = false
|
||||
expenseDtoToEdit = null
|
||||
},
|
||||
onDismiss = {
|
||||
expenseDtoToEdit = null
|
||||
showBottomSheet = false
|
||||
},
|
||||
settingsViewModel = settingsViewModel,
|
||||
categories = categories,
|
||||
expenseAndCategoryViewModel = expenseAndCategoryViewModel,
|
||||
expenseDtoToEdit = expenseDtoToEdit
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
@Composable
|
||||
fun SwipeToDeleteExpenseCard(
|
||||
expenseDto: ExpenseDto,
|
||||
onDelete: (Expense) -> Unit,
|
||||
onClick: (ExpenseDto) -> Unit
|
||||
) {
|
||||
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(expenseDto.expense)
|
||||
},
|
||||
onCancel = { showDialog = false }
|
||||
)
|
||||
}
|
||||
|
||||
SwipeToDismissBox(
|
||||
modifier = Modifier,
|
||||
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))
|
||||
}
|
||||
}
|
||||
) {
|
||||
ExpenseCard(expenseDto, onClick = onClick)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun DeleteConfirmationDialog(
|
||||
onConfirm: () -> Unit,
|
||||
onCancel: () -> Unit
|
||||
) {
|
||||
BasicAlertDialog(
|
||||
onDismissRequest = { onCancel() }
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
.background(
|
||||
MaterialTheme.colorScheme.surface,
|
||||
shape = MaterialTheme.shapes.medium
|
||||
)
|
||||
.padding(24.dp)
|
||||
) {
|
||||
Text(
|
||||
stringResource(string.delete_confirmation),
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 20.sp
|
||||
)
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.End,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 24.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(string.cancel),
|
||||
modifier = Modifier
|
||||
.padding(end = 24.dp)
|
||||
.clickable { onCancel() }
|
||||
)
|
||||
Text(
|
||||
text = stringResource(string.delete),
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.clickable { onConfirm() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
@Composable
|
||||
fun ExpenseCard(expenseDto: ExpenseDto, onClick: (ExpenseDto) -> Unit) {
|
||||
ElevatedCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(0.9f)
|
||||
.height(70.dp)
|
||||
.clickable { onClick(expenseDto) },
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 7.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 16.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(15.dp),
|
||||
modifier = Modifier.fillMaxHeight()
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(expenseDto.category.icon.resource),
|
||||
contentDescription = "Category",
|
||||
tint = Color(expenseDto.category.color.toColorInt())
|
||||
)
|
||||
Column(
|
||||
verticalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.padding(vertical = 8.dp)
|
||||
) {
|
||||
Column(
|
||||
) {
|
||||
Text(
|
||||
text = expenseDto.category.name,
|
||||
fontWeight = FontWeight.Bold,
|
||||
lineHeight = 5.sp
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.padding(0.dp),
|
||||
text = expenseDto.expense.note,
|
||||
fontSize = 11.sp,
|
||||
lineHeight = 5.sp
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = LocalDateTime.parse(expenseDto.expense.datetime).format(
|
||||
DateTimeFormatter.ofPattern("dd MMM HH:mm")
|
||||
),
|
||||
fontSize = 12.sp,
|
||||
)
|
||||
}
|
||||
}
|
||||
Column {
|
||||
Text(
|
||||
text = "- %.2f ${expenseDto.expense.currency}".format(expenseDto.expense.amount),
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
if (expenseDto.expense.currency.lowercase() != expenseDto.trip.currency.lowercase()) {
|
||||
Text(
|
||||
text = "≈ %.2f ${expenseDto.trip.currency}".format(expenseDto.expense.amount),
|
||||
fontSize = 12.sp
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package cc.n0th1ng.tripmoney.screens.settings
|
||||
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.ListItemDefaults
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Text
|
||||
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.rememberCoroutineScope
|
||||
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.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import cc.n0th1ng.tripmoney.R.*
|
||||
import cc.n0th1ng.tripmoney.data.repository.AppTheme
|
||||
import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.S)
|
||||
@Composable
|
||||
fun SettingsScreen() {
|
||||
val settingsViewModel: SettingsViewModel = hiltViewModel()
|
||||
val currentTheme by settingsViewModel.theme.collectAsState()
|
||||
var showDialog by remember { mutableStateOf(false) }
|
||||
val scope = rememberCoroutineScope()
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(15.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
Card {
|
||||
SettingsListItem(onClick = { showDialog = true }, stringResource(string.theme)) {
|
||||
Text(
|
||||
if (isSystemInDarkTheme()) stringResource(string.dark_theme) else stringResource(
|
||||
string.light_theme
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (showDialog) {
|
||||
ThemeSelectionDialog(
|
||||
onDismiss = { showDialog = false },
|
||||
onThemeSelected = { theme ->
|
||||
settingsViewModel.setTheme(theme)
|
||||
showDialog = false
|
||||
},
|
||||
selected = currentTheme
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingsCard(@StringRes title: Int = -1, content: @Composable () -> Unit) {
|
||||
Card {
|
||||
if (title != -1) {
|
||||
Text(
|
||||
text = stringResource(title),
|
||||
fontSize = 13.sp,
|
||||
modifier = Modifier
|
||||
.padding(start = 15.dp, top = 15.dp, end = 15.dp)
|
||||
.alpha(0.6f)
|
||||
)
|
||||
}
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingsListItem(
|
||||
onClick: () -> Unit,
|
||||
headlineText: String,
|
||||
trailingContent: @Composable () -> Unit = {},
|
||||
supportingContent: @Composable () -> Unit
|
||||
) {
|
||||
ListItem(
|
||||
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
|
||||
headlineContent = { Text(headlineText) },
|
||||
supportingContent = supportingContent,
|
||||
trailingContent = trailingContent,
|
||||
modifier = Modifier
|
||||
.clickable(true, onClick = onClick)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ThemeSelectionDialog(
|
||||
onDismiss: () -> Unit,
|
||||
onThemeSelected: (AppTheme) -> Unit,
|
||||
selected: AppTheme
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(stringResource(string.pick_theme)) },
|
||||
text = {
|
||||
Column {
|
||||
AppTheme.entries.forEach { theme ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
onThemeSelected(theme)
|
||||
}
|
||||
.padding(vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
RadioButton(
|
||||
selected = selected == theme,
|
||||
onClick = {
|
||||
onThemeSelected(theme)
|
||||
}
|
||||
)
|
||||
Text(
|
||||
text = when (theme) {
|
||||
AppTheme.LIGHT -> stringResource(string.light_theme)
|
||||
AppTheme.DARK -> stringResource(string.dark_theme)
|
||||
AppTheme.SYSTEM -> stringResource(string.system_settings)
|
||||
},
|
||||
modifier = Modifier.padding(start = 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {}
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package cc.n0th1ng.tripmoney.screens.statistics
|
||||
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
@Composable
|
||||
fun StatisticsScreen() {
|
||||
Text("TODO")
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package cc.n0th1ng.tripmoney.screens.trippicker
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
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
|
||||
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.material3.CardDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
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.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 cc.n0th1ng.tripmoney.navigation.Screens
|
||||
import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel
|
||||
|
||||
@Composable
|
||||
fun TripPickerScreen(
|
||||
navController: NavController
|
||||
) {
|
||||
|
||||
val settingsViewModel: SettingsViewModel = hiltViewModel()
|
||||
val tripViewModel: TripViewModel = hiltViewModel()
|
||||
val trips: LazyPagingItems<Trip> = 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)
|
||||
})
|
||||
}
|
||||
Spacer(Modifier.height(10.dp))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TripCard(trip: Trip, isSelected: Boolean, onClick: () -> Unit) {
|
||||
ElevatedCard(
|
||||
modifier = Modifier
|
||||
.height(100.dp)
|
||||
.clickable(true, onClick = onClick)
|
||||
.alpha(if (isSelected) 1.0f else 0.7f),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = if (isSelected) 7.dp else 0.dp)
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Text(fontSize = 25.sp, fontWeight = FontWeight.SemiBold, text = trip.name)
|
||||
Text(trip.startDate)
|
||||
}
|
||||
Text(
|
||||
trip.currency.uppercase(),
|
||||
modifier = Modifier.padding(20.dp),
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user