fix(#25,#26,#28): Expenses screen polish — scrim, tab badges, shared SearchToolbar #37
@@ -5,10 +5,15 @@ import androidx.compose.animation.expandVertically
|
|||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.animation.shrinkVertically
|
import androidx.compose.animation.shrinkVertically
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.Add
|
import androidx.compose.material.icons.outlined.Add
|
||||||
@@ -20,6 +25,7 @@ import androidx.compose.material3.SmallFloatingActionButton
|
|||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
@@ -57,6 +63,32 @@ fun ExpandedFab(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ExpandedFabScrim(
|
||||||
|
visible: Boolean,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = visible,
|
||||||
|
enter = fadeIn(),
|
||||||
|
exit = fadeOut(),
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.clickable(
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
indication = null,
|
||||||
|
onClick = onDismiss,
|
||||||
|
)
|
||||||
|
.background(MaterialTheme.colorScheme.scrim.copy(alpha = 0.32f)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MiniFab(
|
fun MiniFab(
|
||||||
label: String,
|
label: String,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package dev.achmad.ledgerr.ui.screens.expenses
|
package dev.achmad.ledgerr.ui.screens.expenses
|
||||||
|
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
@@ -19,14 +20,11 @@ import androidx.compose.foundation.shape.CircleShape
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.Edit
|
import androidx.compose.material.icons.outlined.Edit
|
||||||
import androidx.compose.material.icons.outlined.Repeat
|
import androidx.compose.material.icons.outlined.Repeat
|
||||||
import androidx.compose.material.icons.outlined.Search
|
|
||||||
import androidx.compose.material.icons.outlined.UploadFile
|
import androidx.compose.material.icons.outlined.UploadFile
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedTextField
|
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.SnackbarHost
|
import androidx.compose.material3.SnackbarHost
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
@@ -61,12 +59,15 @@ import dev.achmad.ledgerr.R
|
|||||||
import dev.achmad.ledgerr.domain.expense.model.ExpenseWithCategory
|
import dev.achmad.ledgerr.domain.expense.model.ExpenseWithCategory
|
||||||
import dev.achmad.ledgerr.domain.recurring.model.RecurringExpenseWithCategory
|
import dev.achmad.ledgerr.domain.recurring.model.RecurringExpenseWithCategory
|
||||||
import dev.achmad.ledgerr.domain.recurring.model.RecurringInterval
|
import dev.achmad.ledgerr.domain.recurring.model.RecurringInterval
|
||||||
import dev.achmad.ledgerr.ui.components.AppBar
|
import dev.achmad.ledgerr.ui.components.AppBarTitle
|
||||||
import dev.achmad.ledgerr.ui.components.EmptyStateIllustration
|
import dev.achmad.ledgerr.ui.components.EmptyStateIllustration
|
||||||
import dev.achmad.ledgerr.ui.components.ExpandedFab
|
import dev.achmad.ledgerr.ui.components.ExpandedFab
|
||||||
|
import dev.achmad.ledgerr.ui.components.ExpandedFabScrim
|
||||||
import dev.achmad.ledgerr.ui.components.ExportAction
|
import dev.achmad.ledgerr.ui.components.ExportAction
|
||||||
import dev.achmad.ledgerr.ui.components.MiniFab
|
import dev.achmad.ledgerr.ui.components.MiniFab
|
||||||
|
import dev.achmad.ledgerr.ui.components.SearchToolbar
|
||||||
import dev.achmad.ledgerr.ui.components.SingleSelectFilterChipGroup
|
import dev.achmad.ledgerr.ui.components.SingleSelectFilterChipGroup
|
||||||
|
import dev.achmad.ledgerr.ui.components.TabText
|
||||||
import dev.achmad.ledgerr.ui.screens.add_edit_expense.AddEditExpenseScreen
|
import dev.achmad.ledgerr.ui.screens.add_edit_expense.AddEditExpenseScreen
|
||||||
import dev.achmad.ledgerr.ui.screens.add_edit_recurring.AddEditRecurringScreen
|
import dev.achmad.ledgerr.ui.screens.add_edit_recurring.AddEditRecurringScreen
|
||||||
import dev.achmad.ledgerr.ui.screens.import_bank_statement.ImportBankStatementScreen
|
import dev.achmad.ledgerr.ui.screens.import_bank_statement.ImportBankStatementScreen
|
||||||
@@ -100,11 +101,18 @@ object ExpenseListScreen : Screen {
|
|||||||
LaunchedEffect(selectedTab) {
|
LaunchedEffect(selectedTab) {
|
||||||
isFabExpanded = false
|
isFabExpanded = false
|
||||||
}
|
}
|
||||||
|
BackHandler(enabled = isFabExpanded) {
|
||||||
|
isFabExpanded = false
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppBar(
|
SearchToolbar(
|
||||||
title = stringResource(R.string.expense_list_title),
|
searchQuery = searchQuery,
|
||||||
|
onChangeSearchQuery = screenModel::setSearchQuery,
|
||||||
|
titleContent = {
|
||||||
|
AppBarTitle(title = stringResource(R.string.expense_list_title))
|
||||||
|
},
|
||||||
navigateUp = { navigator.pop() },
|
navigateUp = { navigator.pop() },
|
||||||
actions = {
|
actions = {
|
||||||
ExportAction(
|
ExportAction(
|
||||||
@@ -124,10 +132,65 @@ object ExpenseListScreen : Screen {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||||
floatingActionButton = {
|
) { padding ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(padding),
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
|
PrimaryTabRow(selectedTabIndex = selectedTab) {
|
||||||
|
val tabs = listOf(
|
||||||
|
stringResource(R.string.expense_list_tab_expenses) to expenses.size,
|
||||||
|
stringResource(R.string.expense_list_tab_recurring) to recurring.size,
|
||||||
|
)
|
||||||
|
tabs.forEachIndexed { index, (title, count) ->
|
||||||
|
Tab(
|
||||||
|
selected = selectedTab == index,
|
||||||
|
onClick = {
|
||||||
|
coroutineScope.launch {
|
||||||
|
pagerState.animateScrollToPage(index)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
text = { TabText(text = title, badgeCount = count) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HorizontalPager(
|
||||||
|
state = pagerState,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
) { page ->
|
||||||
|
when (page) {
|
||||||
|
0 -> ExpensesTabContent(
|
||||||
|
screenModel = screenModel,
|
||||||
|
expenses = expenses,
|
||||||
|
searchQuery = searchQuery,
|
||||||
|
dateRangeFilter = dateRangeFilter,
|
||||||
|
contentPadding = PaddingValues(bottom = 88.dp),
|
||||||
|
onExpenseClick = { id -> navigator.push(AddEditExpenseScreen(expenseId = id)) },
|
||||||
|
onExpenseLongClick = { id -> pendingDeleteId = id },
|
||||||
|
)
|
||||||
|
1 -> RecurringTabContent(
|
||||||
|
screenModel = screenModel,
|
||||||
|
recurring = recurring,
|
||||||
|
searchQuery = searchQuery,
|
||||||
|
contentPadding = PaddingValues(bottom = 88.dp),
|
||||||
|
onRecurringClick = { id -> navigator.push(AddEditRecurringScreen(recurringId = id)) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ExpandedFabScrim(
|
||||||
|
visible = isFabExpanded,
|
||||||
|
onDismiss = { isFabExpanded = false },
|
||||||
|
modifier = Modifier.matchParentSize(),
|
||||||
|
)
|
||||||
ExpandedFab(
|
ExpandedFab(
|
||||||
expanded = isFabExpanded,
|
expanded = isFabExpanded,
|
||||||
onToggle = { isFabExpanded = !isFabExpanded },
|
onToggle = { isFabExpanded = !isFabExpanded },
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomEnd)
|
||||||
|
.padding(16.dp),
|
||||||
) {
|
) {
|
||||||
if (selectedTab == 0) {
|
if (selectedTab == 0) {
|
||||||
MiniFab(
|
MiniFab(
|
||||||
@@ -157,51 +220,6 @@ object ExpenseListScreen : Screen {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
) { padding ->
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(padding),
|
|
||||||
) {
|
|
||||||
PrimaryTabRow(selectedTabIndex = selectedTab) {
|
|
||||||
listOf(
|
|
||||||
stringResource(R.string.expense_list_tab_expenses),
|
|
||||||
stringResource(R.string.expense_list_tab_recurring),
|
|
||||||
).forEachIndexed { index, title ->
|
|
||||||
Tab(
|
|
||||||
selected = selectedTab == index,
|
|
||||||
onClick = {
|
|
||||||
coroutineScope.launch {
|
|
||||||
pagerState.animateScrollToPage(index)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
text = { Text(text = title) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
HorizontalPager(
|
|
||||||
state = pagerState,
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
) { page ->
|
|
||||||
when (page) {
|
|
||||||
0 -> ExpensesTabContent(
|
|
||||||
screenModel = screenModel,
|
|
||||||
expenses = expenses,
|
|
||||||
searchQuery = searchQuery,
|
|
||||||
dateRangeFilter = dateRangeFilter,
|
|
||||||
contentPadding = PaddingValues(bottom = 88.dp),
|
|
||||||
onExpenseClick = { id -> navigator.push(AddEditExpenseScreen(expenseId = id)) },
|
|
||||||
onExpenseLongClick = { id -> pendingDeleteId = id },
|
|
||||||
)
|
|
||||||
1 -> RecurringTabContent(
|
|
||||||
screenModel = screenModel,
|
|
||||||
recurring = recurring,
|
|
||||||
contentPadding = PaddingValues(bottom = 88.dp),
|
|
||||||
onRecurringClick = { id -> navigator.push(AddEditRecurringScreen(recurringId = id)) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,15 +252,20 @@ object ExpenseListScreen : Screen {
|
|||||||
private fun ExpensesTabContent(
|
private fun ExpensesTabContent(
|
||||||
screenModel: ExpenseListScreenModel,
|
screenModel: ExpenseListScreenModel,
|
||||||
expenses: List<ExpenseWithCategory>,
|
expenses: List<ExpenseWithCategory>,
|
||||||
searchQuery: String,
|
searchQuery: String?,
|
||||||
dateRangeFilter: DateRangeFilter,
|
dateRangeFilter: DateRangeFilter,
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
onExpenseClick: (Long) -> Unit,
|
onExpenseClick: (Long) -> Unit,
|
||||||
onExpenseLongClick: (Long) -> Unit,
|
onExpenseLongClick: (Long) -> Unit,
|
||||||
) {
|
) {
|
||||||
if (expenses.isEmpty() && searchQuery.isBlank() && dateRangeFilter == DateRangeFilter.ALL) {
|
if (expenses.isEmpty()) {
|
||||||
|
val message = when {
|
||||||
|
!searchQuery.isNullOrBlank() -> R.string.expense_list_no_results
|
||||||
|
dateRangeFilter != DateRangeFilter.ALL -> R.string.expense_list_no_results
|
||||||
|
else -> R.string.expense_list_empty
|
||||||
|
}
|
||||||
EmptyStateIllustration(
|
EmptyStateIllustration(
|
||||||
message = stringResource(R.string.expense_list_empty),
|
message = stringResource(message),
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -250,20 +273,6 @@ private fun ExpensesTabContent(
|
|||||||
LazyColumn(
|
LazyColumn(
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
) {
|
) {
|
||||||
item {
|
|
||||||
OutlinedTextField(
|
|
||||||
value = searchQuery,
|
|
||||||
onValueChange = screenModel::setSearchQuery,
|
|
||||||
label = { Text(stringResource(R.string.expense_list_search_hint)) },
|
|
||||||
leadingIcon = {
|
|
||||||
Icon(imageVector = Icons.Outlined.Search, contentDescription = null)
|
|
||||||
},
|
|
||||||
singleLine = true,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
item {
|
item {
|
||||||
SingleSelectFilterChipGroup(
|
SingleSelectFilterChipGroup(
|
||||||
options = DateRangeFilter.entries.map { it to stringResource(it.labelRes()) },
|
options = DateRangeFilter.entries.map { it to stringResource(it.labelRes()) },
|
||||||
@@ -292,20 +301,19 @@ private fun ExpensesTabContent(
|
|||||||
private fun RecurringTabContent(
|
private fun RecurringTabContent(
|
||||||
screenModel: ExpenseListScreenModel,
|
screenModel: ExpenseListScreenModel,
|
||||||
recurring: List<RecurringExpenseWithCategory>,
|
recurring: List<RecurringExpenseWithCategory>,
|
||||||
|
searchQuery: String?,
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
onRecurringClick: (Long) -> Unit,
|
onRecurringClick: (Long) -> Unit,
|
||||||
) {
|
) {
|
||||||
if (recurring.isEmpty()) {
|
if (recurring.isEmpty()) {
|
||||||
Box(
|
val message = if (searchQuery.isNullOrBlank()) {
|
||||||
modifier = Modifier.fillMaxSize(),
|
R.string.recurring_list_empty
|
||||||
contentAlignment = Alignment.Center,
|
} else {
|
||||||
) {
|
R.string.expense_list_no_results
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.recurring_list_empty),
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
EmptyStateIllustration(
|
||||||
|
message = stringResource(message),
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+28
-11
@@ -12,11 +12,15 @@ import dev.achmad.ledgerr.domain.export.interactor.ExportExpensesToCsv
|
|||||||
import dev.achmad.ledgerr.domain.recurring.interactor.GetRecurringExpenses
|
import dev.achmad.ledgerr.domain.recurring.interactor.GetRecurringExpenses
|
||||||
import dev.achmad.ledgerr.domain.recurring.interactor.UpsertRecurringExpense
|
import dev.achmad.ledgerr.domain.recurring.interactor.UpsertRecurringExpense
|
||||||
import dev.achmad.ledgerr.domain.recurring.model.RecurringExpenseWithCategory
|
import dev.achmad.ledgerr.domain.recurring.model.RecurringExpenseWithCategory
|
||||||
|
import dev.achmad.ledgerr.ui.components.SEARCH_DEBOUNCE_MILLIS
|
||||||
|
import kotlinx.coroutines.FlowPreview
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.debounce
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@@ -32,6 +36,7 @@ enum class DateRangeFilter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(FlowPreview::class)
|
||||||
class ExpenseListScreenModel(
|
class ExpenseListScreenModel(
|
||||||
private val getExpenses: GetExpenses = inject(),
|
private val getExpenses: GetExpenses = inject(),
|
||||||
private val getRecurring: GetRecurringExpenses = inject(),
|
private val getRecurring: GetRecurringExpenses = inject(),
|
||||||
@@ -40,21 +45,21 @@ class ExpenseListScreenModel(
|
|||||||
private val exportExpensesToCsv: ExportExpensesToCsv = inject(),
|
private val exportExpensesToCsv: ExportExpensesToCsv = inject(),
|
||||||
) : ScreenModel {
|
) : ScreenModel {
|
||||||
|
|
||||||
private val _searchQuery = MutableStateFlow("")
|
private val _searchQuery = MutableStateFlow<String?>(null)
|
||||||
val searchQuery: StateFlow<String> = _searchQuery.asStateFlow()
|
val searchQuery: StateFlow<String?> = _searchQuery.asStateFlow()
|
||||||
|
|
||||||
private val _dateRangeFilter = MutableStateFlow(DateRangeFilter.ALL)
|
private val _dateRangeFilter = MutableStateFlow(DateRangeFilter.ALL)
|
||||||
val dateRangeFilter: StateFlow<DateRangeFilter> = _dateRangeFilter.asStateFlow()
|
val dateRangeFilter: StateFlow<DateRangeFilter> = _dateRangeFilter.asStateFlow()
|
||||||
|
|
||||||
val expenses: StateFlow<List<ExpenseWithCategory>> = combine(
|
val expenses: StateFlow<List<ExpenseWithCategory>> = combine(
|
||||||
getExpenses.subscribeAll(),
|
getExpenses.subscribeAll(),
|
||||||
_searchQuery,
|
_searchQuery.debounce(SEARCH_DEBOUNCE_MILLIS).distinctUntilChanged(),
|
||||||
_dateRangeFilter,
|
_dateRangeFilter,
|
||||||
) { list, query, filter ->
|
) { list, query, filter ->
|
||||||
val range = filter.toDateRange()
|
val range = filter.toDateRange()
|
||||||
list.asSequence()
|
list.asSequence()
|
||||||
.filter { item -> inDateRange(item, range) }
|
.filter { item -> inDateRange(item, range) }
|
||||||
.filter { item -> matchesQuery(item, query) }
|
.filter { item -> matchesQuery(item, query.orEmpty()) }
|
||||||
.toList()
|
.toList()
|
||||||
}.stateIn(
|
}.stateIn(
|
||||||
scope = screenModelScope,
|
scope = screenModelScope,
|
||||||
@@ -62,14 +67,18 @@ class ExpenseListScreenModel(
|
|||||||
initialValue = emptyList(),
|
initialValue = emptyList(),
|
||||||
)
|
)
|
||||||
|
|
||||||
val recurring: StateFlow<List<RecurringExpenseWithCategory>> =
|
val recurring: StateFlow<List<RecurringExpenseWithCategory>> = combine(
|
||||||
getRecurring.subscribeAll().stateIn(
|
getRecurring.subscribeAll(),
|
||||||
scope = screenModelScope,
|
_searchQuery.debounce(SEARCH_DEBOUNCE_MILLIS).distinctUntilChanged(),
|
||||||
started = SharingStarted.WhileSubscribed(5_000L),
|
) { list, query ->
|
||||||
initialValue = emptyList(),
|
list.filter { item -> matchesQuery(item, query.orEmpty()) }
|
||||||
)
|
}.stateIn(
|
||||||
|
scope = screenModelScope,
|
||||||
|
started = SharingStarted.WhileSubscribed(5_000L),
|
||||||
|
initialValue = emptyList(),
|
||||||
|
)
|
||||||
|
|
||||||
fun setSearchQuery(query: String) {
|
fun setSearchQuery(query: String?) {
|
||||||
_searchQuery.value = query
|
_searchQuery.value = query
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,4 +121,12 @@ class ExpenseListScreenModel(
|
|||||||
amountStr.contains(query) ||
|
amountStr.contains(query) ||
|
||||||
categoryName.contains(query, ignoreCase = true)
|
categoryName.contains(query, ignoreCase = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun matchesQuery(item: RecurringExpenseWithCategory, query: String): Boolean {
|
||||||
|
if (query.isBlank()) return true
|
||||||
|
val note = item.recurring.note.orEmpty()
|
||||||
|
val categoryName = item.category.name
|
||||||
|
return note.contains(query, ignoreCase = true) ||
|
||||||
|
categoryName.contains(query, ignoreCase = true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,6 +83,7 @@
|
|||||||
<string name="expense_list_delete_message">This will permanently delete the expense.</string>
|
<string name="expense_list_delete_message">This will permanently delete the expense.</string>
|
||||||
<string name="expense_list_empty">No expenses yet</string>
|
<string name="expense_list_empty">No expenses yet</string>
|
||||||
<string name="recurring_list_empty">No recurring expenses</string>
|
<string name="recurring_list_empty">No recurring expenses</string>
|
||||||
|
<string name="expense_list_no_results">No results</string>
|
||||||
|
|
||||||
<!-- Add / Edit expense (issue #6) -->
|
<!-- Add / Edit expense (issue #6) -->
|
||||||
<string name="add_edit_expense_title_new">Add Expense</string>
|
<string name="add_edit_expense_title_new">Add Expense</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user