Compare commits
5 Commits
79551ab69d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae5394aa59 | ||
|
|
dae0212cf9 | ||
|
|
270ff4fa07 | ||
|
|
f83bf62655 | ||
|
|
6c067f64ce |
@@ -15,7 +15,7 @@ android {
|
||||
}
|
||||
defaultConfig {
|
||||
applicationId = "cc.n0th1ng.tripmoney"
|
||||
minSdk = 24
|
||||
minSdk = 26
|
||||
targetSdk = 36
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
|
||||
@@ -44,6 +44,7 @@ import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel
|
||||
import cc.n0th1ng.tripmoney.viewmodel.TripViewModel
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.launch
|
||||
import java.time.LocalDate
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : ComponentActivity() {
|
||||
@@ -57,7 +58,6 @@ class MainActivity : ComponentActivity() {
|
||||
NavigationDrawer()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,14 +111,16 @@ fun NavigationDrawer() {
|
||||
startDestination = if (currentTripId == -1) Screens.TRIP_PICKER else Screens.LIST_EXPENSE,
|
||||
modifier = Modifier.padding(innerPadding)
|
||||
) {
|
||||
composable(Screens.LIST_EXPENSE+"?dateToScroll={dateToScroll}",
|
||||
arguments = listOf(navArgument("dateToScroll"){defaultValue = ""})) {
|
||||
backStackEntry ->
|
||||
composable(
|
||||
Screens.LIST_EXPENSE + "?dateToScroll={dateToScroll}",
|
||||
arguments = listOf(navArgument("dateToScroll") { defaultValue = "" })
|
||||
) { backStackEntry ->
|
||||
ListExpenseScreen(
|
||||
filter = filter, search = search,
|
||||
initialAutoOpen = shouldTriggerAutoOpen,
|
||||
onAutoOpenConsumed = { hasHandledStartupOpen = true },
|
||||
dateToScroll = backStackEntry.arguments?.getString("dateToScroll")?: "")
|
||||
dateToScroll = backStackEntry.arguments?.getString("dateToScroll") ?: ""
|
||||
)
|
||||
}
|
||||
composable(Screens.TRIP_PICKER) {
|
||||
TripPickerScreen(navController)
|
||||
@@ -139,7 +141,9 @@ fun NavigationDrawer() {
|
||||
|
||||
data class Filter(
|
||||
val categories: List<Category> = emptyList(), val startAmount: Double = 0.0,
|
||||
val endAmount: Double = Double.MAX_VALUE
|
||||
val endAmount: Double = Double.MAX_VALUE,
|
||||
val startDate: LocalDate = LocalDate.MIN,
|
||||
val endDate: LocalDate = LocalDate.MAX
|
||||
) {
|
||||
fun with(category: Category): Filter {
|
||||
return this.copy(categories = categories + category)
|
||||
@@ -153,11 +157,19 @@ data class Filter(
|
||||
return this.copy(endAmount = amount)
|
||||
}
|
||||
|
||||
fun withStartDate(date: LocalDate): Filter {
|
||||
return this.copy(startDate = date)
|
||||
}
|
||||
|
||||
fun withEndDate(date: LocalDate): Filter {
|
||||
return this.copy(endDate = date)
|
||||
}
|
||||
|
||||
fun without(category: Category): Filter {
|
||||
return this.copy(categories = categories - category)
|
||||
}
|
||||
|
||||
fun isDefault(): Boolean {
|
||||
return this.categories.isEmpty() && startAmount == 0.0 && endAmount == Double.MAX_VALUE
|
||||
return this.categories.isEmpty() && startAmount == 0.0 && endAmount == Double.MAX_VALUE && startDate == LocalDate.MIN && endDate == LocalDate.MAX
|
||||
}
|
||||
}
|
||||
@@ -153,16 +153,16 @@ private class DatabasePrepopulator(
|
||||
name = "Hotel", icon = Icons.HOTEL, color = colors.random()
|
||||
),
|
||||
Category(
|
||||
name = "Jedzenie", icon = Icons.RESTAURANT, color = colors.random()
|
||||
name = "Restaurants", icon = Icons.RESTAURANT, color = colors.random()
|
||||
),
|
||||
Category(
|
||||
name = "Transport", icon = Icons.FLIGHT, color = colors.random()
|
||||
),
|
||||
Category(
|
||||
name = "Rozrywka", icon = Icons.ATTRACTION, color = colors.random()
|
||||
name = "Entertainment", icon = Icons.ATTRACTION, color = colors.random()
|
||||
),
|
||||
Category(
|
||||
name = "Zakupy", icon = Icons.GROCERIES, color = colors.random()
|
||||
name = "Groceries", icon = Icons.GROCERIES, color = colors.random()
|
||||
),
|
||||
Category(
|
||||
name = "Zakupy1", icon = Icons.GROCERIES, color = colors.random()
|
||||
|
||||
@@ -2,7 +2,6 @@ package cc.n0th1ng.tripmoney.data.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import androidx.room.Upsert
|
||||
@@ -21,7 +20,7 @@ interface CategoryDao {
|
||||
@Transaction
|
||||
@Query(
|
||||
"""
|
||||
SELECT * FROM category WHERE archived is 0
|
||||
SELECT * FROM category WHERE archived is 0 ORDER BY name
|
||||
"""
|
||||
)
|
||||
fun categories(): Flow<List<Category>>
|
||||
|
||||
@@ -40,6 +40,12 @@ interface ExpenseDao {
|
||||
AND (
|
||||
:endAmount IS NULL OR expense.amount <= :endAmount
|
||||
)
|
||||
AND (
|
||||
:startDate IS NULL OR expense.datetime >= :startDate
|
||||
)
|
||||
AND (
|
||||
:endDate IS NULL OR expense.datetime <= :endDate
|
||||
)
|
||||
|
||||
ORDER BY expense.datetime DESC
|
||||
"""
|
||||
@@ -50,7 +56,9 @@ interface ExpenseDao {
|
||||
categoryIds: List<Int>,
|
||||
categoriesEmpty: Boolean,
|
||||
startAmount: Double?,
|
||||
endAmount: Double?
|
||||
endAmount: Double?,
|
||||
startDate: Long?,
|
||||
endDate: Long?
|
||||
): PagingSource<Int, ExpenseDto>
|
||||
|
||||
@Transaction
|
||||
@@ -74,7 +82,12 @@ interface ExpenseDao {
|
||||
AND (
|
||||
:endAmount IS NULL OR expense.amount <= :endAmount
|
||||
)
|
||||
|
||||
AND (
|
||||
:startDate IS NULL OR expense.datetime >= :startDate
|
||||
)
|
||||
AND (
|
||||
:endDate IS NULL OR expense.datetime <= :endDate
|
||||
)
|
||||
ORDER BY expense.datetime DESC
|
||||
"""
|
||||
)
|
||||
@@ -84,7 +97,9 @@ interface ExpenseDao {
|
||||
categoryIds: List<Int>,
|
||||
categoriesEmpty: Boolean,
|
||||
startAmount: Double?,
|
||||
endAmount: Double?
|
||||
endAmount: Double?,
|
||||
startDate: Long?,
|
||||
endDate: Long?
|
||||
): Flow<List<ExpenseDto>>
|
||||
|
||||
@Query(
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package cc.n0th1ng.tripmoney.data.repository
|
||||
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
@@ -10,9 +8,11 @@ import cc.n0th1ng.tripmoney.Filter
|
||||
import cc.n0th1ng.tripmoney.data.dao.ExpenseDao
|
||||
import cc.n0th1ng.tripmoney.data.entity.Expense
|
||||
import cc.n0th1ng.tripmoney.data.entity.ExpenseDto
|
||||
import cc.n0th1ng.tripmoney.screens.listexpense.toEpochMilli
|
||||
import cc.n0th1ng.tripmoney.utils.Currencies
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import java.time.LocalDate
|
||||
import javax.inject.Inject
|
||||
|
||||
class ExpenseRepository @Inject constructor(
|
||||
@@ -49,9 +49,10 @@ class ExpenseRepository @Inject constructor(
|
||||
categoryIds = categoryIds,
|
||||
categoriesEmpty = categoryIds.isEmpty(),
|
||||
startAmount = filter.startAmount,
|
||||
endAmount = filter.endAmount
|
||||
endAmount = filter.endAmount,
|
||||
startDate = if(filter.startDate == LocalDate.MIN) null else filter.startDate.toEpochMilli(),
|
||||
endDate = if(filter.endDate == LocalDate.MAX) null else filter.endDate.plusDays(1).toEpochMilli(),
|
||||
)
|
||||
|
||||
}
|
||||
).flow
|
||||
}
|
||||
@@ -62,18 +63,18 @@ class ExpenseRepository @Inject constructor(
|
||||
filter: Filter = Filter()
|
||||
): Flow<List<ExpenseDto>> {
|
||||
val categoryIds = filter.categories.map { it.id }
|
||||
|
||||
return expenseDao.expenseDto(
|
||||
tripId = tripId,
|
||||
search = search.takeIf { it.isNotBlank() },
|
||||
categoryIds = categoryIds,
|
||||
categoriesEmpty = categoryIds.isEmpty(),
|
||||
startAmount = filter.startAmount,
|
||||
endAmount = filter.endAmount
|
||||
endAmount = filter.endAmount,
|
||||
startDate = if(filter.startDate == LocalDate.MIN) null else filter.startDate.toEpochMilli(),
|
||||
endDate = if(filter.endDate == LocalDate.MAX) null else filter.endDate.plusDays(1).toEpochMilli(),
|
||||
)
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
suspend fun recalculateTripExpenses(tripId: Int) {
|
||||
val expenses = getExpensesDto(tripId).first()
|
||||
expenses.forEach { expenseDto ->
|
||||
|
||||
@@ -2,8 +2,6 @@ package cc.n0th1ng.tripmoney.navigation
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
@@ -12,11 +10,7 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Menu
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -44,6 +38,7 @@ import cc.n0th1ng.tripmoney.Filter
|
||||
import cc.n0th1ng.tripmoney.R
|
||||
import cc.n0th1ng.tripmoney.data.entity.Category
|
||||
import cc.n0th1ng.tripmoney.screens.addexpense.categoriesToPreview
|
||||
import cc.n0th1ng.tripmoney.screens.listexpense.FilterDialog
|
||||
import cc.n0th1ng.tripmoney.theme.TripMoneyTheme
|
||||
import cc.n0th1ng.tripmoney.utils.AllPreviews
|
||||
import com.composables.icons.materialsymbols.outlined.R.drawable
|
||||
@@ -159,71 +154,9 @@ fun TopBar(
|
||||
showFilter = false
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FilterDialog(
|
||||
onDismiss: () -> Unit,
|
||||
onSave: (Filter) -> Unit,
|
||||
onClear: () -> Unit,
|
||||
categories: List<Category>,
|
||||
filter: Filter
|
||||
) {
|
||||
var filter by remember { mutableStateOf(filter) }
|
||||
var fromAmountString by remember { mutableStateOf(filter.startAmount.toString()) }
|
||||
var toAmountString by remember { mutableStateOf(filter.endAmount.toString()) }
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
dismissButton = {
|
||||
Button(
|
||||
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondary),
|
||||
enabled = true,
|
||||
onClick = onClear
|
||||
) { Text(stringResource(R.string.clear)) }
|
||||
},
|
||||
confirmButton = {
|
||||
Button(
|
||||
enabled = true,
|
||||
onClick = {
|
||||
onSave(
|
||||
filter.withStartAmount(fromAmountString.safeToDouble())
|
||||
.withEndAmount(toAmountString.safeToDouble())
|
||||
)
|
||||
}) { Text(stringResource(R.string.save)) }
|
||||
}, title = { Text("Filter") },
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
Text(text = "Categories")
|
||||
FlowRow(horizontalArrangement = Arrangement.spacedBy(7.dp)) {
|
||||
categories.forEach {
|
||||
FilterChip(selected = filter.categories.contains(it), onClick = {
|
||||
filter = if (filter.categories.contains(it)) {
|
||||
filter.without(it)
|
||||
} else {
|
||||
filter.with(it)
|
||||
}
|
||||
}, label = {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(5.dp)) {
|
||||
Icon(painterResource(it.icon.resource), contentDescription = null)
|
||||
Text(text = it.name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
AmountTextField(label = "from", onValueChange = { newText ->
|
||||
fromAmountString = newText
|
||||
}, value = fromAmountString)
|
||||
AmountTextField(label = "to", onValueChange = { newText ->
|
||||
toAmountString = newText
|
||||
}, value = toAmountString)
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AmountTextField(label: String, onValueChange: (String) -> Unit, value: String) {
|
||||
var value by remember { mutableStateOf(value) }
|
||||
@@ -277,23 +210,3 @@ fun PreviewTopBar() {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@AllPreviews
|
||||
@Composable
|
||||
fun PreviewFilterDialog() {
|
||||
TripMoneyTheme {
|
||||
FilterDialog(
|
||||
onDismiss = {},
|
||||
onSave = {},
|
||||
categories = categoriesToPreview,
|
||||
filter = Filter(),
|
||||
onClear = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.safeToDouble(): Double {
|
||||
if (this == "∞") return Double.MAX_VALUE
|
||||
if (this.isEmpty()) return 0.0
|
||||
return this.toDouble()
|
||||
}
|
||||
@@ -98,7 +98,8 @@ fun AddExpenseBottomSheet(
|
||||
expenseDtoToEdit = expenseDtoToEdit,
|
||||
state = state,
|
||||
currentTrip = currentTrip ?: Trip.DUMMY,
|
||||
categories = categories
|
||||
categories = categories,
|
||||
onSaveCategory = {expenseAndCategoryViewModel.save(it)}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -111,7 +112,8 @@ fun AddExpenseBottomSheet(
|
||||
expenseDtoToEdit: ExpenseDto?,
|
||||
state: SheetState,
|
||||
currentTrip: Trip,
|
||||
categories: List<Category>
|
||||
categories: List<Category>,
|
||||
onSaveCategory: (Category) -> Unit
|
||||
) {
|
||||
val currentTripId = currentTrip.id
|
||||
|
||||
@@ -319,7 +321,8 @@ fun AddExpenseBottomSheet(
|
||||
category = selectedCategory
|
||||
},
|
||||
selected = category,
|
||||
categories = categories
|
||||
categories = categories,
|
||||
onSaveCategory = onSaveCategory
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -578,7 +581,8 @@ fun PreviewAddExpenseDisabled() {
|
||||
LocalDate.parse("2020-01-15"),
|
||||
Currencies.entries.random().name
|
||||
),
|
||||
categories = categoriesToPreview
|
||||
categories = categoriesToPreview,
|
||||
{}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -622,7 +626,8 @@ fun PreviewAddExpenseEnabled() {
|
||||
LocalDate.parse("2020-01-11"),
|
||||
Currencies.entries.random().name
|
||||
),
|
||||
categories = categoriesToPreview
|
||||
categories = categoriesToPreview,
|
||||
{}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -13,22 +13,26 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
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.focus.FocusRequester
|
||||
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 androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import cc.n0th1ng.tripmoney.R.string
|
||||
import cc.n0th1ng.tripmoney.data.entity.Category
|
||||
import cc.n0th1ng.tripmoney.screens.AddCategoryDialog
|
||||
import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel
|
||||
import cc.n0th1ng.tripmoney.screens.addexpense.categoriesToPreview
|
||||
import cc.n0th1ng.tripmoney.theme.TripMoneyTheme
|
||||
import cc.n0th1ng.tripmoney.utils.AllPreviews
|
||||
import cc.n0th1ng.tripmoney.utils.SearchTextOutlined
|
||||
import com.composables.icons.materialsymbols.outlined.R
|
||||
|
||||
@Composable
|
||||
@@ -37,22 +41,41 @@ fun CategorySelectionDialog(
|
||||
onCategorySelected: (Category) -> Unit,
|
||||
selected: Category?,
|
||||
categories: List<Category>,
|
||||
onSaveCategory: (Category) -> Unit
|
||||
) {
|
||||
val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel()
|
||||
val listState = rememberLazyListState()
|
||||
var showAddCategoryDialog by remember { mutableStateOf(false) }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss, title = { Text(stringResource(string.pick_category)) }, text = {
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(stringResource(string.pick_category)) },
|
||||
text = {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
LaunchedEffect(Unit) {
|
||||
if(selected != null) {
|
||||
listState.animateScrollToItem(categories.indexOfFirst { it == selected })
|
||||
}
|
||||
// focusRequester.requestFocus()
|
||||
}
|
||||
Column {
|
||||
var search by remember { mutableStateOf("") }
|
||||
val filteredCategories = if (search.isBlank()) {
|
||||
categories
|
||||
} else {
|
||||
categories.filter { category ->
|
||||
category.name.lowercase().contains(search.lowercase())
|
||||
}
|
||||
}
|
||||
|
||||
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]
|
||||
count = filteredCategories.size,
|
||||
key = { index -> filteredCategories[index].id }) { index ->
|
||||
val category = filteredCategories[index]
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -83,24 +106,41 @@ fun CategorySelectionDialog(
|
||||
.clickable {
|
||||
showAddCategoryDialog = true
|
||||
}
|
||||
.padding(top = 15.dp),
|
||||
.padding(bottom = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.materialsymbols_ic_add_outlined),
|
||||
contentDescription = stringResource(string.category)
|
||||
)
|
||||
Text(
|
||||
text = stringResource(string.add_new), modifier = Modifier.padding(start = 8.dp),
|
||||
text = stringResource(string.add_new),
|
||||
modifier = Modifier.padding(start = 8.dp),
|
||||
)
|
||||
}
|
||||
SearchTextOutlined(
|
||||
text = search,
|
||||
onTextChange = { newText -> search = newText },
|
||||
focusRequester = focusRequester
|
||||
)
|
||||
}
|
||||
}, confirmButton = {})
|
||||
},
|
||||
confirmButton = {})
|
||||
if (showAddCategoryDialog) {
|
||||
AddCategoryDialog(onDismiss = {
|
||||
showAddCategoryDialog = false
|
||||
}, onSave = { category ->
|
||||
expenseAndCategoryViewModel.save(category)
|
||||
onSaveCategory(category)
|
||||
showAddCategoryDialog = false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@AllPreviews
|
||||
@Composable
|
||||
fun PreviewCategorySelectionDialog() {
|
||||
TripMoneyTheme {
|
||||
CategorySelectionDialog(
|
||||
{}, {},
|
||||
categoriesToPreview.random(), categoriesToPreview, {})
|
||||
}
|
||||
}
|
||||
@@ -12,9 +12,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
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.RadioButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -25,14 +23,13 @@ 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.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import cc.n0th1ng.tripmoney.R
|
||||
import cc.n0th1ng.tripmoney.theme.TripMoneyTheme
|
||||
import cc.n0th1ng.tripmoney.utils.AllPreviews
|
||||
import cc.n0th1ng.tripmoney.utils.Currencies
|
||||
import com.composables.icons.materialsymbols.outlined.R.drawable
|
||||
import cc.n0th1ng.tripmoney.utils.SearchTextOutlined
|
||||
|
||||
@Composable
|
||||
fun CurrencySelectionDialog(
|
||||
@@ -57,20 +54,6 @@ fun CurrencySelectionDialog(
|
||||
}
|
||||
|
||||
Column(verticalArrangement = Arrangement.spacedBy(5.dp)) {
|
||||
OutlinedTextField(
|
||||
value = search,
|
||||
onValueChange = { newText ->
|
||||
search = newText
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
trailingIcon = {
|
||||
Icon(
|
||||
painter = painterResource(drawable.materialsymbols_ic_search_outlined),
|
||||
contentDescription = "search"
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
val filteredCurrencies = if (search.isBlank()) {
|
||||
currencies
|
||||
} else {
|
||||
@@ -79,7 +62,12 @@ fun CurrencySelectionDialog(
|
||||
}
|
||||
}
|
||||
|
||||
LazyColumn(state = scrollState) {
|
||||
LazyColumn(
|
||||
state = scrollState,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(bottom = 10.dp)
|
||||
) {
|
||||
items(
|
||||
count = filteredCurrencies.size,
|
||||
key = { index -> filteredCurrencies[index] }
|
||||
@@ -104,6 +92,8 @@ fun CurrencySelectionDialog(
|
||||
}
|
||||
}
|
||||
}
|
||||
SearchTextOutlined(
|
||||
text = search, onTextChange = { newText -> search = newText })
|
||||
}
|
||||
},
|
||||
confirmButton = {},
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package cc.n0th1ng.tripmoney.screens.listexpense
|
||||
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.DatePicker
|
||||
@@ -21,8 +19,7 @@ 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.tooling.preview.Preview
|
||||
import cc.n0th1ng.tripmoney.R.*
|
||||
import cc.n0th1ng.tripmoney.R.string
|
||||
import cc.n0th1ng.tripmoney.theme.TripMoneyTheme
|
||||
import cc.n0th1ng.tripmoney.utils.AllPreviews
|
||||
import java.time.Instant
|
||||
@@ -31,7 +28,6 @@ import java.time.LocalDateTime
|
||||
import java.time.LocalTime
|
||||
import java.time.ZoneId
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun DateRangePicker(
|
||||
@@ -75,7 +71,6 @@ fun DateRangePicker(
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun DatePicker(
|
||||
@@ -117,7 +112,6 @@ fun DatePicker(
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun TimePicker(
|
||||
@@ -147,7 +141,6 @@ fun TimePicker(
|
||||
}
|
||||
|
||||
@Composable
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
fun DateTimePicker(
|
||||
dateTime: LocalDateTime = LocalDateTime.now(),
|
||||
@@ -181,15 +174,12 @@ fun DateTimePicker(
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
fun LocalDateTime.toEpochMilli(): Long =
|
||||
this.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
fun LocalDate.toEpochMilli(): Long =
|
||||
this.atStartOfDay().atZone(ZoneId.of("UTC")).toInstant().toEpochMilli()
|
||||
this.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
@AllPreviews
|
||||
@Composable
|
||||
fun DatePickerPreview() {
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
package cc.n0th1ng.tripmoney.screens.listexpense
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.sizeIn
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
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.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import cc.n0th1ng.tripmoney.Filter
|
||||
import cc.n0th1ng.tripmoney.R
|
||||
import cc.n0th1ng.tripmoney.data.entity.Category
|
||||
import cc.n0th1ng.tripmoney.navigation.AmountTextField
|
||||
import cc.n0th1ng.tripmoney.screens.addexpense.categoriesToPreview
|
||||
import cc.n0th1ng.tripmoney.theme.TripMoneyTheme
|
||||
import cc.n0th1ng.tripmoney.utils.AllPreviews
|
||||
import cc.n0th1ng.tripmoney.utils.pretty
|
||||
import java.time.LocalDate
|
||||
|
||||
@Composable
|
||||
fun FilterDialog(
|
||||
onDismiss: () -> Unit,
|
||||
onSave: (Filter) -> Unit,
|
||||
onClear: () -> Unit,
|
||||
categories: List<Category>,
|
||||
filter: Filter
|
||||
) {
|
||||
var filter by remember { mutableStateOf(filter) }
|
||||
var fromAmountString by remember { mutableStateOf(filter.startAmount.toString()) }
|
||||
var toAmountString by remember { mutableStateOf(filter.endAmount.toString()) }
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
dismissButton = {
|
||||
Button(
|
||||
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondary),
|
||||
enabled = true,
|
||||
onClick = onClear
|
||||
) { Text(stringResource(R.string.clear)) }
|
||||
},
|
||||
confirmButton = {
|
||||
Button(
|
||||
enabled = true,
|
||||
onClick = {
|
||||
onSave(filter)
|
||||
}) { Text(stringResource(R.string.save)) }
|
||||
}, title = { Text("Filter") },
|
||||
text = {
|
||||
var showDatePicker by remember { mutableStateOf(false) }
|
||||
var startDate by remember {
|
||||
mutableStateOf(filter.startDate)
|
||||
}
|
||||
var endDate by remember {
|
||||
mutableStateOf(filter.endDate)
|
||||
}
|
||||
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
Text(text = "Categories")
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(7.dp),
|
||||
modifier = Modifier
|
||||
.sizeIn(maxHeight = 200.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
categories.forEach {
|
||||
FilterChip(selected = filter.categories.contains(it), onClick = {
|
||||
filter = if (filter.categories.contains(it)) {
|
||||
filter.without(it)
|
||||
} else {
|
||||
filter.with(it)
|
||||
}
|
||||
}, label = {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(5.dp)) {
|
||||
Icon(painterResource(it.icon.resource), contentDescription = null)
|
||||
Text(text = it.name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Button(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(1f),
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
onClick = { showDatePicker = true }) {
|
||||
val startDateFormatted = startDate.pretty()
|
||||
val endDateFormatted = endDate.pretty()
|
||||
Text(
|
||||
text =
|
||||
if(startDate == LocalDate.MIN && endDate == LocalDate.MAX) "Show all dates" else
|
||||
"$startDateFormatted - $endDateFormatted",
|
||||
fontSize = 17.sp
|
||||
)
|
||||
}
|
||||
AmountTextField(label = "from", onValueChange = { newText ->
|
||||
fromAmountString = newText
|
||||
filter = filter.withStartAmount(newText.safeToDouble())
|
||||
}, value = fromAmountString)
|
||||
AmountTextField(label = "to", onValueChange = { newText ->
|
||||
toAmountString = newText
|
||||
filter = filter.withEndAmount(newText.safeToDouble())
|
||||
}, value = toAmountString)
|
||||
}
|
||||
|
||||
if (showDatePicker) {
|
||||
DateRangePicker(
|
||||
startDate = if(startDate == LocalDate.MIN) LocalDate.now() else startDate,
|
||||
endDate = if(endDate == LocalDate.MAX) LocalDate.now() else endDate,
|
||||
onDismiss = { showDatePicker = false },
|
||||
onConfirm = { newStartDate, newEndDate ->
|
||||
startDate = newStartDate
|
||||
endDate = newEndDate
|
||||
filter = filter.withStartDate(startDate).withEndDate(endDate)
|
||||
showDatePicker = false
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@AllPreviews
|
||||
@Composable
|
||||
fun PreviewFilterDialog() {
|
||||
TripMoneyTheme {
|
||||
FilterDialog(
|
||||
onDismiss = {},
|
||||
onSave = {},
|
||||
categories = categoriesToPreview.plus(categoriesToPreview).plus(categoriesToPreview),
|
||||
filter = Filter(),
|
||||
onClear = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun String.safeToDouble(): Double {
|
||||
if (this == "∞") return Double.MAX_VALUE
|
||||
if (this.isEmpty()) return 0.0
|
||||
return this.toDouble()
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
package cc.n0th1ng.tripmoney.screens.listexpense
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -85,7 +83,6 @@ import java.time.format.DateTimeFormatter
|
||||
import kotlin.random.Random
|
||||
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
@Composable
|
||||
fun ListExpenseScreen(
|
||||
filter: Filter,
|
||||
@@ -124,7 +121,6 @@ fun ListExpenseScreen(
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
@Composable
|
||||
fun ListExpenseScreen(
|
||||
currentTrip: Trip?,
|
||||
@@ -286,7 +282,6 @@ fun ListExpenseScreen(
|
||||
}
|
||||
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
@Composable
|
||||
fun CustomDivider(date: LocalDate, sum: Double, currency: String) {
|
||||
Row(
|
||||
@@ -329,7 +324,6 @@ fun CustomDivider(date: LocalDate, sum: Double, currency: String) {
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
@Composable
|
||||
fun SwipeToDeleteExpenseCard(
|
||||
expenseDto: ExpenseDto,
|
||||
@@ -422,7 +416,6 @@ fun DeleteConfirmationDialog(
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
@Composable
|
||||
fun ExpenseCard(
|
||||
expenseDto: ExpenseDto,
|
||||
@@ -523,7 +516,6 @@ fun ExpenseCard(
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
@AllPreviews
|
||||
@Composable
|
||||
fun PreviewListExpenseScreen() {
|
||||
@@ -550,7 +542,6 @@ fun PreviewListExpenseScreen() {
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
@AllPreviews
|
||||
@Composable
|
||||
fun PreviewListExpenseScreenWithoutExpenses() {
|
||||
@@ -577,7 +568,6 @@ fun PreviewListExpenseScreenWithoutExpenses() {
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
@AllPreviews
|
||||
@Composable
|
||||
fun PreviewListExpenseScreenWithoutTrip() {
|
||||
@@ -609,7 +599,6 @@ fun PreviewDeleteConfirmationDialog() {
|
||||
}
|
||||
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
private fun sampleExpenseDtoWithConvertedAmountList(): List<ExpenseListItemUi> {
|
||||
val sampleCategories = listOf(
|
||||
Category(
|
||||
|
||||
@@ -88,7 +88,6 @@ fun StatisticsScreen(navController: NavController) {
|
||||
)
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
@Composable
|
||||
fun StatisticsScreen(
|
||||
summaryPerCategoryList: List<SummaryPerCategory>,
|
||||
@@ -124,7 +123,7 @@ fun StatisticsScreen(
|
||||
modifier = Modifier.heightIn(max = 300.dp),
|
||||
summaryPerCategoryList = summaryPerCategoryList
|
||||
)
|
||||
SummaryPerDayCard(modifier = Modifier.height(300.dp), summaryPerDayList = summaryPerDayList, onDayClicked = onDayClicked)
|
||||
SummaryPerDayCard(modifier = Modifier.height(300.dp), summaryPerDayList = summaryPerDayList.sortedBy { it.day }, onDayClicked = onDayClicked)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
package cc.n0th1ng.tripmoney.utils
|
||||
|
||||
val colors: List<String> = listOf(
|
||||
"#af1b3f",
|
||||
"#083D77",
|
||||
"#5998c5",
|
||||
"#f7934c",
|
||||
"#ec0b43",
|
||||
"#87A330",
|
||||
"#6F8AB7",
|
||||
"#F26CA7",
|
||||
"#5E4AE3",
|
||||
"#0B7189",
|
||||
"#2A7F62",
|
||||
"#0B7189"
|
||||
"#5998c5",
|
||||
"#5E4AE3",
|
||||
"#6F8AB7",
|
||||
"#87A330",
|
||||
"#F26CA7",
|
||||
"#af1b3f",
|
||||
"#ec0b43",
|
||||
"#f7934c"
|
||||
)
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
package cc.n0th1ng.tripmoney.utils
|
||||
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import java.time.LocalDate
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
fun LocalDate.pretty(): String {
|
||||
return this.format(DateTimeFormatter.ofPattern("dd MMM yyyy"))
|
||||
}
|
||||
@@ -14,5 +14,10 @@ enum class Icons(@DrawableRes val resource: Int) {
|
||||
COFFEE(R.drawable.materialsymbols_ic_local_cafe_outlined),
|
||||
GENERAL(R.drawable.materialsymbols_ic_shoppingmode_outlined),
|
||||
ENTERTAINMENT(R.drawable.materialsymbols_ic_theaters_outlined),
|
||||
LAUNDRY(R.drawable.materialsymbols_ic_local_laundry_service_outlined)
|
||||
LAUNDRY(R.drawable.materialsymbols_ic_local_laundry_service_outlined),
|
||||
INSURANCE(R.drawable.materialsymbols_ic_health_and_safety_outlined),
|
||||
SIM_DATA(R.drawable.materialsymbols_ic_sim_card_outlined),
|
||||
CAR_RENTAL(R.drawable.materialsymbols_ic_directions_car_outlined),
|
||||
FUEL(R.drawable.materialsymbols_ic_local_gas_station_outlined),
|
||||
TOURS(R.drawable.materialsymbols_ic_tour_outlined)
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package cc.n0th1ng.tripmoney.utils
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import com.composables.icons.materialsymbols.outlined.R.drawable
|
||||
|
||||
@Composable
|
||||
fun SearchTextOutlined(
|
||||
modifier: Modifier = Modifier,
|
||||
text: String,
|
||||
onTextChange: (String) -> Unit,
|
||||
focusRequester: FocusRequester = FocusRequester()
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = text,
|
||||
onValueChange = onTextChange,
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester),
|
||||
trailingIcon = {
|
||||
if (text.isNotBlank()) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = "close",
|
||||
modifier = Modifier.clickable(true, onClick = { onTextChange("") })
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
painter = painterResource(drawable.materialsymbols_ic_search_outlined),
|
||||
contentDescription = "search"
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -98,7 +98,6 @@ open class ExpenseAndCategoryViewModel @Inject constructor(
|
||||
): Flow<List<ExpenseDto>> =
|
||||
expenseRepo.getExpensesDto(tripId, search, filter)
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
fun save(expense: Expense, trip: Trip, onComplete: (Int) -> Unit) {
|
||||
viewModelScope.launch {
|
||||
val rate = exchangeRateRepository.getRate(
|
||||
@@ -133,7 +132,6 @@ open class ExpenseAndCategoryViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
suspend fun generateCSVToFile(tripId: Int, file: File) {
|
||||
file.writer().use { writer ->
|
||||
CSVPrinter(
|
||||
@@ -153,7 +151,6 @@ open class ExpenseAndCategoryViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
fun getDailySums(tripId: Int, search: String, filter: Filter): Flow<Map<LocalDate, Double>> {
|
||||
return getExpensesDto(tripId, search, filter)
|
||||
.map { expenses ->
|
||||
@@ -164,14 +161,12 @@ open class ExpenseAndCategoryViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
fun getSummaryAmount(tripId: Int): Flow<Double> {
|
||||
return getExpensesDto(tripId).map { list ->
|
||||
list.sumOf { it.expense.amount * it.expense.rate }
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
fun getSummaryPerCategory(tripId: Int): Flow<List<SummaryPerCategory>> {
|
||||
val tripFlow = tripRepo.getTrip(tripId)
|
||||
val expensesFlow = getExpensesDto(tripId)
|
||||
@@ -194,7 +189,6 @@ open class ExpenseAndCategoryViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
fun getSummaryPerDay(tripId: Int): Flow<List<SummaryPerDay>> {
|
||||
val tripFlow = tripRepo.getTrip(tripId)
|
||||
val expensesFlow = getExpensesDto(tripId)
|
||||
@@ -220,14 +214,12 @@ open class ExpenseAndCategoryViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
fun clearOldRates() {
|
||||
viewModelScope.launch {
|
||||
exchangeRateRepository.clearOldRates()
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
sealed class ExpenseListItemUi {
|
||||
data class Item(val expenseDto: ExpenseDto) : ExpenseListItemUi()
|
||||
data class Header(val date: LocalDate, val sum: Double, val currency: String) :
|
||||
|
||||
Reference in New Issue
Block a user