This commit is contained in:
Rafal Wisniewski
2026-03-23 20:14:13 +01:00
parent 96cdd056a0
commit 916481e4e3
27 changed files with 960 additions and 154 deletions

View File

@@ -1,6 +1,5 @@
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
@@ -28,19 +27,17 @@ 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
import cc.n0th1ng.tripmoney.utils.colors
@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) }
var color by remember { mutableStateOf(colors[0]) }
AlertDialog(
onDismissRequest = onDismiss, title = { Text("Add new category") }, text = {
AlertDialogFill(
@@ -48,7 +45,7 @@ fun AddCategoryDialog(onDismiss: () -> Unit, onSave: (Category) -> Unit) {
name = newText
},
onIconChange = { newIcon -> icon = newIcon },
onColorChange = {newColor -> color = newColor}
onColorChange = { newColor -> color = newColor }
)
}, confirmButton = {
Button(
@@ -72,10 +69,14 @@ fun AddCategoryDialog(onDismiss: () -> Unit, onSave: (Category) -> Unit) {
}
@Composable
fun AlertDialogFill(onTextChange: (String) -> Unit, onIconChange: (Icons) -> Unit, onColorChange: (String) -> Unit) {
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) }
var colorHex by remember { mutableStateOf(colors[0]) }
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
Row(
verticalAlignment = Alignment.CenterVertically,
@@ -118,16 +119,16 @@ fun AlertDialogFill(onTextChange: (String) -> Unit, onIconChange: (Icons) -> Uni
rememberScrollState()
)
) {
Colors.entries.forEach { color ->
colors.forEach { color ->
Box(
modifier = Modifier
.clickable(onClick = {
colorHex = color.hexString
colorHex = color
onColorChange(colorHex)
})
.size(30.dp)
.aspectRatio(1f)
.background(Color(color.hexString.toColorInt()))
.background(Color(color.toColorInt()))
) {}
}
}

View File

@@ -66,6 +66,7 @@ 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 cc.n0th1ng.tripmoney.viewmodel.TripViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -81,13 +82,13 @@ fun AddExpenseBottomSheet(
onSave: (Expense) -> Unit,
onDismiss: () -> Unit,
expenseDtoToEdit: ExpenseDto?,
state: SheetState,
// categories: List<Category> = emptyList()
state: SheetState
) {
val tripViewModel: TripViewModel = hiltViewModel()
val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel()
val settingsViewModel: SettingsViewModel = hiltViewModel()
val currentTripId by settingsViewModel.currentTrip.collectAsState()
// val currentTripId = 1
val currentTrip = tripViewModel.getTrip(currentTripId)
val categories by expenseAndCategoryViewModel.getCategories().collectAsState(emptyList())
if (categories.isEmpty()) {
return
@@ -103,7 +104,7 @@ fun AddExpenseBottomSheet(
var showDateTimePicker by remember { mutableStateOf(false) }
var currency by remember {
mutableStateOf(
expenseDtoToEdit?.expense?.currency ?: Currencies.PLN.name
expenseDtoToEdit?.expense?.currency ?: currentTrip?.currency ?: Currencies.default().name
)
}
var category by remember { mutableStateOf(expenseDtoToEdit?.category ?: categories[0]) }

View File

@@ -104,7 +104,9 @@ fun DateTimePicker(
if (showDatePicker) {
DatePicker(onDismiss = { showDatePicker = false }, onConfirm = { newDate ->
date = newDate
date = newDate
showDatePicker = false
showTimePicker = true
})
}

View File

@@ -37,8 +37,10 @@ import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.material3.rememberSwipeToDismissBoxState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@@ -53,34 +55,58 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.graphics.toColorInt
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.paging.PagingData
import androidx.paging.compose.collectAsLazyPagingItems
import cc.n0th1ng.tripmoney.R.string
import cc.n0th1ng.tripmoney.data.entity.Category
import cc.n0th1ng.tripmoney.data.entity.Expense
import cc.n0th1ng.tripmoney.data.entity.ExpenseDto
import cc.n0th1ng.tripmoney.data.entity.Trip
import cc.n0th1ng.tripmoney.screens.addexpense.AddExpenseBottomSheet
import cc.n0th1ng.tripmoney.service.ExchangeService
import cc.n0th1ng.tripmoney.theme.TripMoneyTheme
import cc.n0th1ng.tripmoney.utils.AllPreviews
import cc.n0th1ng.tripmoney.utils.Currencies
import cc.n0th1ng.tripmoney.utils.colors
import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel
import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel.ExpenseDtoWithConvertedAmount
import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import javax.inject.Inject
import kotlin.random.Random
@RequiresApi(Build.VERSION_CODES.O)
@Composable
fun ListExpenseScreen() {
val settingsViewModel: SettingsViewModel = hiltViewModel()
val currentTrip by settingsViewModel.currentTrip.collectAsState()
val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel()
val expensesWithConvertedFlow = expenseAndCategoryViewModel
.getExpensesWithConvertedAmountsPaged(currentTrip)
ListExpenseScreen(
expensesWithConvertedFlow = expensesWithConvertedFlow,
onSaveExpense = { expenseAndCategoryViewModel.save(it) },
onDeleteExpense = { expenseAndCategoryViewModel.delete(it) })
}
@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 expenses = expenseAndCategoryViewModel.getExpenses(currentTrip).collectAsLazyPagingItems()
fun ListExpenseScreen(
expensesWithConvertedFlow: Flow<PagingData<ExpenseDtoWithConvertedAmount>>,
onSaveExpense: (Expense) -> Unit, onDeleteExpense: (Expense) -> Unit
) {
val expensesWithConverted = expensesWithConvertedFlow.collectAsLazyPagingItems()
val listState = rememberLazyListState()
var showBottomSheet by remember { mutableStateOf(false) }
var expenseDtoToEdit: ExpenseDto? = null
val sumMap = remember { mutableStateMapOf<LocalDate, Double>() }
Scaffold(floatingActionButtonPosition = FabPosition.EndOverlay, floatingActionButton = {
ExtendedFloatingActionButton(
@@ -90,30 +116,50 @@ fun ListExpenseScreen() {
)
})
{
LaunchedEffect(expensesWithConverted.itemSnapshotList.items) {
val items = expensesWithConverted.itemSnapshotList.items
val newSums = items
.groupBy { LocalDateTime.parse(it.expenseDto.expense.datetime).toLocalDate() }
.mapValues { (_, expensesForDay) ->
expensesForDay.sumOf { it.convertedAmount }
}
sumMap.clear()
sumMap.putAll(newSums)
}
LazyColumn(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
state = listState
) {
items(
count = expenses.itemCount,
key = { index -> expenses[index]?.expense?.id ?: index }
count = expensesWithConverted.itemCount,
key = { index -> expensesWithConverted[index]?.expenseDto?.expense?.id ?: index }
) { index ->
val expenseDto = expenses[index]
if (expenseDto != null) {
val previousExpense = expenses.itemSnapshotList.items.getOrNull(index - 1)
val expenseDtoWithConverted = expensesWithConverted[index]
val expenseDto = expenseDtoWithConverted?.expenseDto
if (expenseDtoWithConverted != null && expenseDto != null) {
val previousExpense =
expensesWithConverted.itemSnapshotList.items.getOrNull(index - 1)?.expenseDto
val showDayDivider =
index == 0 || LocalDateTime.parse(previousExpense?.expense?.datetime)
.toLocalDate() != LocalDateTime.parse(expenseDto.expense.datetime)
.toLocalDate()
Spacer(Modifier.height(5.dp))
Spacer(Modifier
.height(5.dp)
.background(MaterialTheme.colorScheme.onBackground))
if (showDayDivider) {
CustomDivider(expenseDto)
CustomDivider(
expenseDto,
sumMap.getOrDefault(
LocalDateTime.parse(expenseDto.expense.datetime).toLocalDate(), 0.00
)
)
}
Spacer(Modifier.height(5.dp))
SwipeToDeleteExpenseCard(
expenseDto = expenseDto,
onDelete = { expense -> expenseAndCategoryViewModel.delete(expense) },
expenseDtoWithConverted = expenseDtoWithConverted,
onDelete = { expense -> onDeleteExpense(expense) },
onClick = { expenseDto ->
expenseDtoToEdit = expenseDto
showBottomSheet = true
@@ -125,7 +171,7 @@ fun ListExpenseScreen() {
if (showBottomSheet) {
AddExpenseBottomSheet(
onSave = { expense ->
expenseAndCategoryViewModel.save(expense)
onSaveExpense(expense)
showBottomSheet = false
expenseDtoToEdit = null
},
@@ -140,9 +186,10 @@ fun ListExpenseScreen() {
}
}
@RequiresApi(Build.VERSION_CODES.O)
@Composable
fun CustomDivider(expenseDto: ExpenseDto) {
fun CustomDivider(expenseDto: ExpenseDto, sum: Double) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Absolute.Center,
@@ -153,16 +200,32 @@ fun CustomDivider(expenseDto: ExpenseDto) {
LocalDateTime.parse(expenseDto.expense.datetime).format(
DateTimeFormatter.ofPattern("dd EEEE")
).toString(),
modifier = Modifier.background(Color.White.copy(alpha = 0f))
modifier = Modifier.background(Color.White.copy(alpha = 0f)),
style = MaterialTheme.typography.titleMedium
)
HorizontalDivider(modifier = Modifier.weight(1f))
Row(
modifier = Modifier
.fillMaxWidth()
.weight(1f),
horizontalArrangement = Arrangement.Absolute.Center,
verticalAlignment = Alignment.CenterVertically
) {
HorizontalDivider(modifier = Modifier.weight(2f))
Text(
"%.2f %s".format(sum, expenseDto.trip.currency),
modifier = Modifier.background(Color.White.copy(alpha = 0f)),
style = MaterialTheme.typography.bodyMedium
)
HorizontalDivider(modifier = Modifier.weight(1f))
}
}
}
@RequiresApi(Build.VERSION_CODES.O)
@Composable
fun SwipeToDeleteExpenseCard(
expenseDto: ExpenseDto,
expenseDtoWithConverted: ExpenseDtoWithConvertedAmount,
onDelete: (Expense) -> Unit,
onClick: (ExpenseDto) -> Unit
) {
@@ -186,7 +249,7 @@ fun SwipeToDeleteExpenseCard(
onConfirm = {
showDialog = false
dismissed = true
onDelete(expenseDto.expense)
onDelete(expenseDtoWithConverted.expenseDto.expense)
},
onCancel = { showDialog = false }
)
@@ -209,7 +272,7 @@ fun SwipeToDeleteExpenseCard(
}
}
) {
ExpenseCard(expenseDto, onClick = onClick)
ExpenseCard(expenseDtoWithConverted, onClick = onClick)
}
}
}
@@ -226,15 +289,15 @@ fun DeleteConfirmationDialog(
Column(
Modifier
.background(
MaterialTheme.colorScheme.surface,
MaterialTheme.colorScheme.secondaryContainer,
shape = MaterialTheme.shapes.medium
)
.padding(24.dp)
) {
Text(
stringResource(string.delete_confirmation),
fontWeight = FontWeight.Bold,
fontSize = 20.sp
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSecondaryContainer
)
Row(
horizontalArrangement = Arrangement.End,
@@ -244,6 +307,8 @@ fun DeleteConfirmationDialog(
) {
Text(
text = stringResource(string.cancel),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSecondaryContainer,
modifier = Modifier
.padding(end = 24.dp)
.clickable { onCancel() }
@@ -251,7 +316,7 @@ fun DeleteConfirmationDialog(
Text(
text = stringResource(string.delete),
color = MaterialTheme.colorScheme.error,
fontWeight = FontWeight.Bold,
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.clickable { onConfirm() }
)
}
@@ -261,8 +326,14 @@ fun DeleteConfirmationDialog(
@RequiresApi(Build.VERSION_CODES.O)
@Composable
fun ExpenseCard(expenseDto: ExpenseDto, onClick: (ExpenseDto) -> Unit) {
fun ExpenseCard(
expenseDtoWithConverted: ExpenseDtoWithConvertedAmount,
onClick: (ExpenseDto) -> Unit
) {
val expenseDto = expenseDtoWithConverted.expenseDto
ElevatedCard(
colors = CardDefaults.elevatedCardColors()
.copy(containerColor = MaterialTheme.colorScheme.secondaryContainer),
modifier = Modifier
.fillMaxWidth(0.9f)
.height(70.dp)
@@ -299,14 +370,14 @@ fun ExpenseCard(expenseDto: ExpenseDto, onClick: (ExpenseDto) -> Unit) {
{
Text(
text = expenseDto.category.name,
fontWeight = FontWeight.Bold,
lineHeight = 5.sp
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSecondaryContainer
)
Text(
modifier = Modifier.padding(0.dp),
text = expenseDto.expense.note,
fontSize = 11.sp,
lineHeight = 5.sp
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSecondaryContainer
)
}
@@ -314,31 +385,132 @@ fun ExpenseCard(expenseDto: ExpenseDto, onClick: (ExpenseDto) -> Unit) {
text = LocalDateTime.parse(expenseDto.expense.datetime).format(
DateTimeFormatter.ofPattern("dd MMM HH:mm")
),
fontSize = 12.sp,
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSecondaryContainer
)
}
}
Column {
Text(
text = "- %.2f ${expenseDto.expense.currency}".format(expenseDto.expense.amount),
fontWeight = FontWeight.Bold
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSecondaryContainer
)
if (expenseDto.expense.currency.lowercase() != expenseDto.trip.currency.lowercase()) {
val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel()
val amount by
expenseAndCategoryViewModel.convertAmount(
amount = expenseDto.expense.amount,
base = Currencies.valueOf(expenseDto.expense.currency),
target = Currencies.valueOf(expenseDto.trip.currency),
date = LocalDateTime.parse(expenseDto.expense.datetime).toLocalDate()
).collectAsState(initial = 0.0)
Text(
text = "≈ %.2f ${expenseDto.trip.currency}".format(amount),
fontSize = 12.sp
text = "≈ %.2f ${expenseDto.trip.currency}".format(expenseDtoWithConverted.convertedAmount),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSecondaryContainer
)
}
}
}
}
}
}
@RequiresApi(Build.VERSION_CODES.O)
@AllPreviews
@Composable
fun PreviewListExpenseScreen() {
TripMoneyTheme() {
val pagingData = PagingData.from(sampleExpenseDtoWithConvertedAmountList())
ListExpenseScreen(
expensesWithConvertedFlow = MutableStateFlow(pagingData),
onSaveExpense = {},
onDeleteExpense = {}
)
}
}
@AllPreviews
@Composable
fun PreviewDeleteConfirmationDialog() {
TripMoneyTheme() {
DeleteConfirmationDialog(
onConfirm = {},
onCancel = {})
}
}
@RequiresApi(Build.VERSION_CODES.O)
private fun sampleExpenseDtoWithConvertedAmountList(): List<ExpenseDtoWithConvertedAmount> {
val sampleCategories = listOf(
Category(
name = "Hotel",
icon = cc.n0th1ng.tripmoney.utils.Icons.HOTEL,
color = colors.random()
),
Category(
name = "Jedzenie",
icon = cc.n0th1ng.tripmoney.utils.Icons.RESTAURANT,
color = colors.random()
),
Category(
name = "Transport",
icon = cc.n0th1ng.tripmoney.utils.Icons.FLIGHT,
color = colors.random()
),
Category(
name = "Rozrywka",
icon = cc.n0th1ng.tripmoney.utils.Icons.ATTRACTION,
color = colors.random()
),
Category(
name = "Zakupy",
icon = cc.n0th1ng.tripmoney.utils.Icons.GROCERIES,
color = colors.random()
),
)
val trip = Trip(
id = 1,
name = "Vacation",
currency = "USD",
startDate = "2026-01-01"
)
val startLong = LocalDateTime.now().minusDays(10).toEpochMilli()
val endLong = LocalDateTime.now().toEpochMilli()
val result: MutableList<ExpenseDtoWithConvertedAmount> = mutableListOf()
for (i in 0..15) {
val category = sampleCategories.random()
val datetime = if (i > 4) {
LocalDateTime.ofEpochSecond(
Random.nextLong(startLong, endLong),
0,
ZoneOffset.UTC
).toString()
} else LocalDateTime.now().toString()
val expense = Expense(
id = i,
categoryId = category.id,
tripId = 1,
amount = Random.nextDouble(0.1, 300.0),
currency = Currencies.entries.random().name,
note = if (i % 3 == 0) "Some note" else "",
datetime = datetime
)
val expenseDto = ExpenseDto(
expense = expense,
category = category,
trip = trip
)
result.add(
ExpenseDtoWithConvertedAmount(
expenseDto,
convertedAmount = if (Random.nextBoolean()) Random.nextDouble(
0.1,
300.0
) else expense.amount
)
)
}
return result
}

View File

@@ -33,6 +33,8 @@ 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.screens.listexpense.CurrencySelectionDialog
import cc.n0th1ng.tripmoney.utils.Currencies
import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel
@RequiresApi(Build.VERSION_CODES.S)
@@ -40,8 +42,9 @@ import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel
fun SettingsScreen() {
val settingsViewModel: SettingsViewModel = hiltViewModel()
val currentTheme by settingsViewModel.theme.collectAsState()
var showDialog by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
val currentDefaultCurrency by settingsViewModel.defaultCurrency.collectAsState()
var showThemeDialog by remember { mutableStateOf(false) }
var showCurrencyDialog by remember { mutableStateOf(false) }
Column(
modifier = Modifier
.fillMaxWidth()
@@ -49,7 +52,7 @@ fun SettingsScreen() {
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
Card {
SettingsListItem(onClick = { showDialog = true }, stringResource(string.theme)) {
SettingsListItem(onClick = { showThemeDialog = true }, stringResource(string.theme)) {
Text(
if (isSystemInDarkTheme()) stringResource(string.dark_theme) else stringResource(
string.light_theme
@@ -58,16 +61,33 @@ fun SettingsScreen() {
}
}
if (showDialog) {
Card {
SettingsListItem(
onClick = { showCurrencyDialog = true },
stringResource(string.default_currency)
) {
Text(currentDefaultCurrency.name)
}
}
if (showThemeDialog) {
ThemeSelectionDialog(
onDismiss = { showDialog = false },
onDismiss = { showThemeDialog = false },
onThemeSelected = { theme ->
settingsViewModel.setTheme(theme)
showDialog = false
showThemeDialog = false
},
selected = currentTheme
)
}
if (showCurrencyDialog) {
CurrencySelectionDialog(onDismiss = {showCurrencyDialog = false}, onCurrencySelected = {
currencyString ->
settingsViewModel.setDefaultCurrency(Currencies.valueOf(currencyString))
showCurrencyDialog = false
}, currentDefaultCurrency.name)
}
}
}

View File

@@ -1,9 +1,150 @@
package cc.n0th1ng.tripmoney.screens.statistics
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
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.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.graphics.toColorInt
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import cc.n0th1ng.tripmoney.data.dto.SummaryPerCategory
import cc.n0th1ng.tripmoney.data.entity.Category
import cc.n0th1ng.tripmoney.theme.TripMoneyTheme
import cc.n0th1ng.tripmoney.utils.Currencies
import cc.n0th1ng.tripmoney.utils.Icons
import cc.n0th1ng.tripmoney.utils.colors
import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel
import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel
@RequiresApi(Build.VERSION_CODES.O)
@Composable
fun StatisticsScreen() {
Text("TODO")
}
val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel()
val settingsViewModel: SettingsViewModel = hiltViewModel()
val currentTrip by settingsViewModel.currentTrip.collectAsState()
val summaryPerCategoryList by expenseAndCategoryViewModel.getSummaryPerCategory(currentTrip)
.collectAsState(emptyList())
Column(modifier = Modifier.padding(10.dp)) {
SummaryPerCategoryCard(summaryPerCategoryList)
}
}
@Composable
fun SummaryPerCategoryCard(summaryPerCategoryList: List<SummaryPerCategory>) {
Card(modifier = Modifier.fillMaxWidth()) {
Column(
modifier = Modifier.padding(15.dp),
verticalArrangement = Arrangement.spacedBy(5.dp)
) {
// Text(text = "Summary", fontWeight = FontWeight.Bold, fontSize = 25.sp)
summaryPerCategoryList.forEach {
CategoryCard(
summaryPerCategory = it, modifier = Modifier
.fillMaxWidth()
)
}
}
}
}
@Composable
fun CategoryCard(modifier: Modifier = Modifier, summaryPerCategory: SummaryPerCategory) {
Column(modifier = modifier) {
Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(5.dp)) {
Icon(
painter = painterResource(summaryPerCategory.category.icon.resource),
contentDescription = null,
modifier = Modifier.size(MaterialTheme.typography.bodyLarge.fontSize.value.dp),
tint = Color(summaryPerCategory.category.color.toColorInt())
)
Text("%s".format(summaryPerCategory.category.name, (summaryPerCategory.percent * 100).toInt()),
style = MaterialTheme.typography.bodyLarge, color = Color(summaryPerCategory.category.color.toColorInt()))
}
Text("%.2f ${summaryPerCategory.currency}".format(summaryPerCategory.amount),
style = MaterialTheme.typography.bodyMedium)
}
Row(horizontalArrangement = Arrangement.spacedBy(5.dp), verticalAlignment = Alignment.CenterVertically){
Box(
modifier = Modifier
.height(40.dp)
.fillMaxWidth(0.12f + (0.90f - 0.12f) * summaryPerCategory.percent)
.clip(RoundedCornerShape(16.dp))
.background(MaterialTheme.colorScheme.primary)
) {
Column(verticalArrangement = Arrangement.Center, modifier = Modifier.fillMaxSize().padding(11.dp)) {
Text("%d%%".format((summaryPerCategory.percent * 100).toInt()),
style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onPrimary)
}
}
// Text("%d%%".format((summaryPerCategory.percent * 100).toInt()),
// style = MaterialTheme.typography.labelSmall)
}
}
}
@Preview
@Composable
fun previewLight() {
TripMoneyTheme {
SummaryPerCategoryCard(summaryPerCategoryList)
}
}
@Preview
@Composable
fun previewDark() {
TripMoneyTheme(darkTheme = true) {
SummaryPerCategoryCard(summaryPerCategoryList)
}
}
val categories = listOf(
Category(name = "Jedzenie", icon = Icons.RESTAURANT, color = colors.random()),
Category(name = "Transport", icon = Icons.FLIGHT, color = colors.random()),
Category(name = "Rozrywka", icon = Icons.ATTRACTION, color = colors.random()),
Category(name = "Zakupy", icon = Icons.GROCERIES, color = colors.random()),
Category(name = "Zakupy1", icon = Icons.GROCERIES, color = colors.random()),
Category(name = "Zakupy2", icon = Icons.GROCERIES, color = colors.random()),
Category(name = "Zakupy3", icon = Icons.GROCERIES, color = colors.random())
)
val summaryPerCategoryList = listOf(
SummaryPerCategory(categories[0], 50.0, 1f, Currencies.PLN),
SummaryPerCategory(categories[1], 120.0, 0.3f, Currencies.PLN),
SummaryPerCategory(categories[2], 80.0, 0.2f, Currencies.PLN),
SummaryPerCategory(categories[3], 50.0, 0.1f, Currencies.PLN),
SummaryPerCategory(categories[4], 120.0, 0.3f, Currencies.PLN),
SummaryPerCategory(categories[5], 50.0, 0.0001f, Currencies.PLN),
)

View File

@@ -24,6 +24,7 @@ import androidx.compose.material3.Shapes
import androidx.compose.material3.SheetState
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
@@ -37,12 +38,15 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import cc.n0th1ng.tripmoney.R
import cc.n0th1ng.tripmoney.data.entity.Trip
import cc.n0th1ng.tripmoney.screens.addexpense.CurrencyButton
import cc.n0th1ng.tripmoney.screens.listexpense.CurrencySelectionDialog
import cc.n0th1ng.tripmoney.screens.listexpense.DatePicker
import cc.n0th1ng.tripmoney.utils.Currencies
import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel
import io.ktor.http.hostIsIp
import java.time.LocalDate
import java.time.format.DateTimeFormatter
@@ -62,9 +66,11 @@ fun AddTripBottomSheet(
LocalDate.parse(tripToEdit?.startDate ?: LocalDate.now().toString())
)
}
val settingsViewModel: SettingsViewModel = hiltViewModel()
val defaultCurrency by settingsViewModel.defaultCurrency.collectAsState()
var showCurrencyDialog by remember { mutableStateOf(false) }
var showDatePicker by remember { mutableStateOf(false) }
var currency by remember { mutableStateOf(tripToEdit?.currency ?: Currencies.default().name) }
var currency by remember { mutableStateOf(tripToEdit?.currency ?: defaultCurrency.name) }
var enableSave by remember { mutableStateOf(tripToEdit != null) }
ModalBottomSheet(

View File

@@ -182,6 +182,13 @@ fun TripCard(
) {
val haptics = LocalHapticFeedback.current
ElevatedCard(
colors = CardDefaults.elevatedCardColors(
containerColor = if (isSelected) {
MaterialTheme.colorScheme.primary
} else {
MaterialTheme.colorScheme.secondary
}
),
modifier = Modifier
.height(100.dp)
.combinedClickable(enabled = true, onLongClick = {