Added a search field to BottomBar to search / filter transactions

This commit is contained in:
dankito 2024-08-29 03:45:58 +02:00
parent b2dfd3fb71
commit d0069a0563
7 changed files with 148 additions and 30 deletions

View File

@ -1,33 +1,64 @@
package net.codinux.banking.ui.appskeleton package net.codinux.banking.ui.appskeleton
import androidx.compose.animation.*
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Menu import androidx.compose.material.icons.filled.Menu
import androidx.compose.runtime.Composable import androidx.compose.material.icons.filled.Refresh
import androidx.compose.runtime.collectAsState import androidx.compose.material.icons.filled.Search
import androidx.compose.runtime.getValue import androidx.compose.runtime.*
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.key.*
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.platform.LocalWindowInfo
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.toSize
import bankmeister.composeapp.generated.resources.Res
import bankmeister.composeapp.generated.resources.filter_alt
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import net.codinux.banking.ui.config.Colors import net.codinux.banking.ui.config.Colors
import net.codinux.banking.ui.config.DI import net.codinux.banking.ui.config.DI
import net.codinux.banking.ui.extensions.toggle import net.codinux.banking.ui.extensions.toggle
import org.jetbrains.compose.resources.imageResource
private val uiState = DI.uiState private val uiState = DI.uiState
private val IconWidth = 48.dp
@Composable @Composable
fun BottomBar() { fun BottomBar() {
val userAccounts by uiState.userAccounts.collectAsState()
val accountFilter by uiState.accountFilter.collectAsState() val accountFilter by uiState.accountFilter.collectAsState()
val transactionsFilter by uiState.transactionsFilter.collectAsState()
var showSearchbar by remember { mutableStateOf(false) }
val searchFieldFocus = remember { FocusRequester() }
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val configuration = LocalViewConfiguration.current
println("config: $configuration")
val windowInfo = LocalWindowInfo.current
println("windowInfo: $windowInfo")
BottomAppBar(modifier = Modifier.background(Colors.Accent)) { BottomAppBar(modifier = Modifier.background(Colors.Accent)) {
IconButton( IconButton(
onClick = { coroutineScope.launch { onClick = { coroutineScope.launch {
@ -37,23 +68,80 @@ fun BottomBar() {
Icon(Icons.Filled.Menu, contentDescription = "Open Navigation Drawer with sidebar menu") Icon(Icons.Filled.Menu, contentDescription = "Open Navigation Drawer with sidebar menu")
} }
Column { Row(Modifier.fillMaxWidth().padding(end = 64.dp)) { // 72.dp = leave space for Floating Action Button
val title = if (accountFilter.isEmpty()) { val color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current)
"Bankmeister"
} else { Column(Modifier.fillMaxHeight().weight(if (showSearchbar) 0.1f else 1f), verticalArrangement = Arrangement.Center) {
val filter = accountFilter.first() if (showSearchbar == false) {
if (filter.bankAccount != null) { val title = if (accountFilter.isEmpty()) {
filter.bankAccount.productName ?: filter.bankAccount.identifier "Bankmeister"
} else { } else {
filter.userAccount.bankName val filter = accountFilter.first()
if (filter.bankAccount != null) {
filter.bankAccount.productName ?: filter.bankAccount.identifier
} else {
filter.userAccount.bankName
}
}
Text(title, color = color, maxLines = 1, overflow = TextOverflow.Ellipsis)
} }
} }
Text(title, color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current), maxLines = 1, overflow = TextOverflow.Ellipsis) Row(Modifier.fillMaxHeight(), verticalAlignment = Alignment.CenterVertically) {
AnimatedVisibility(
modifier = Modifier,
visible = showSearchbar,
enter = slideInHorizontally(initialOffsetX = { it }) + fadeIn(),
exit = slideOutHorizontally(targetOffsetX = { it }) + fadeOut()
) {
TextField(
transactionsFilter.searchTerm,
{ value -> transactionsFilter.updateSearchTerm(value) },
textStyle = TextStyle(color),
placeholder = { Text("Umsätze suchen ...", color = color.copy(ContentAlpha.medium), modifier = Modifier.height(48.dp)) },
singleLine = true,
trailingIcon = { Icon(Icons.Filled.Close, contentDescription = "Close search bar", Modifier.clickable { showSearchbar = false }, color) },
modifier = Modifier.focusRequester(searchFieldFocus).widthIn(150.dp, 250.dp).padding(vertical = 4.dp).onKeyEvent { event ->
if (event.key == Key.Escape && event.type == KeyEventType.KeyUp) {
if (transactionsFilter.searchTerm.isBlank()) {
showSearchbar = false
} else {
transactionsFilter.updateSearchTerm("")
}
true
} else {
false
}
},
shape = RoundedCornerShape(16.dp),
colors = TextFieldDefaults.textFieldColors(
cursorColor = Colors.CodinuxSecondaryColor, // Spielerei, bräucht's eigentlich net
// disable horizontal line at the bottom
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent
)
)
if (showSearchbar) {
LaunchedEffect(showSearchbar) {
searchFieldFocus.requestFocus()
}
}
}
}
if (userAccounts.isNotEmpty()) {
if (showSearchbar == false) {
Row(Modifier.fillMaxHeight().widthIn(IconWidth, IconWidth), verticalAlignment = Alignment.CenterVertically) {
IconButton({ showSearchbar = true }, Modifier.width(IconWidth)) {
Icon(Icons.Filled.Search, contentDescription = "Open search bar")
}
}
}
}
} }
Spacer(Modifier.weight(1f))
Spacer(Modifier.width(72.dp)) // space for Floating Action Button
} }
} }

View File

@ -22,7 +22,7 @@ import net.codinux.banking.ui.config.Colors
import net.codinux.banking.ui.config.DI import net.codinux.banking.ui.config.DI
import org.jetbrains.compose.resources.DrawableResource import org.jetbrains.compose.resources.DrawableResource
private val filterService = DI.bankAccountFilterService private val filterService = DI.accountTransactionsFilterService
@Composable @Composable
fun NavigationMenuItem( fun NavigationMenuItem(

View File

@ -23,7 +23,7 @@ import net.codinux.banking.ui.config.DI
import net.codinux.banking.ui.state.UiState import net.codinux.banking.ui.state.UiState
import org.jetbrains.compose.ui.tooling.preview.Preview import org.jetbrains.compose.ui.tooling.preview.Preview
private val filterService = DI.bankAccountFilterService private val filterService = DI.accountTransactionsFilterService
private val formatUtil = DI.formatUtil private val formatUtil = DI.formatUtil
@ -36,10 +36,12 @@ fun TransactionsList(uiState: UiState) {
val accountFilter by uiState.accountFilter.collectAsState() val accountFilter by uiState.accountFilter.collectAsState()
val transactionsFilter by uiState.transactionsFilter.collectAsState()
val transactions by uiState.transactions.collectAsState() val transactions by uiState.transactions.collectAsState()
val transactionsToDisplay by remember(accountFilter, transactions) { val transactionsToDisplay by remember(accountFilter, transactionsFilter, transactions) {
derivedStateOf { filterService.filterAccounts(transactions, accountFilter) } derivedStateOf { filterService.filterAccounts(transactions, accountFilter, transactionsFilter) }
} }
val groupedByMonth by remember(transactionsToDisplay) { val groupedByMonth by remember(transactionsToDisplay) {

View File

@ -24,7 +24,7 @@ object DI {
val bankIconService = BankIconService() val bankIconService = BankIconService()
val bankAccountFilterService = BankAccountFilterService() val accountTransactionsFilterService = AccountTransactionsFilterService()
var bankingRepository: BankingRepository = InMemoryBankingRepository(emptyList()) var bankingRepository: BankingRepository = InMemoryBankingRepository(emptyList())

View File

@ -0,0 +1,12 @@
package net.codinux.banking.ui.model
import androidx.compose.runtime.*
class AccountTransactionsFilter {
var searchTerm by mutableStateOf("")
private set
fun updateSearchTerm(searchTerm: String) {
this.searchTerm = searchTerm
}
}

View File

@ -3,17 +3,26 @@ package net.codinux.banking.ui.service
import net.codinux.banking.dataaccess.entities.BankAccountEntity import net.codinux.banking.dataaccess.entities.BankAccountEntity
import net.codinux.banking.dataaccess.entities.UserAccountEntity import net.codinux.banking.dataaccess.entities.UserAccountEntity
import net.codinux.banking.ui.model.AccountTransactionViewModel import net.codinux.banking.ui.model.AccountTransactionViewModel
import net.codinux.banking.ui.model.AccountTransactionsFilter
import net.codinux.banking.ui.model.BankAccountFilter import net.codinux.banking.ui.model.BankAccountFilter
class BankAccountFilterService { class AccountTransactionsFilterService {
fun filterAccounts(transactions: List<AccountTransactionViewModel>, accountsFilter: List<BankAccountFilter>): List<AccountTransactionViewModel> = fun filterAccounts(transactions: List<AccountTransactionViewModel>, accountsFilter: List<BankAccountFilter>, transactionsFilter: AccountTransactionsFilter): List<AccountTransactionViewModel> {
if (accountsFilter.isEmpty()) { val appliedAccountFilter = if (accountsFilter.isEmpty()) {
transactions transactions
} else { } else {
transactions.filter { matchesFilter(it, accountsFilter) } transactions.filter { matchesFilter(it, accountsFilter) }
} }
val searchTerm = transactionsFilter.searchTerm
return if (searchTerm.isBlank()) {
appliedAccountFilter
} else {
appliedAccountFilter.filter { matchesSearchTerm(it, searchTerm) }
}
}
private fun matchesFilter(transaction: AccountTransactionViewModel, accountsFilter: List<BankAccountFilter>): Boolean = private fun matchesFilter(transaction: AccountTransactionViewModel, accountsFilter: List<BankAccountFilter>): Boolean =
accountsFilter.any { (userAccount, bankAccount) -> accountsFilter.any { (userAccount, bankAccount) ->
if (bankAccount != null) { if (bankAccount != null) {
@ -23,6 +32,10 @@ class BankAccountFilterService {
} }
} }
private fun matchesSearchTerm(transaction: AccountTransactionViewModel, searchTerm: String): Boolean =
transaction.reference.contains(searchTerm, true)
|| (transaction.otherPartyName != null && transaction.otherPartyName.contains(searchTerm, true))
fun isSelected(userAccount: UserAccountEntity, accountFilter: List<BankAccountFilter>): Boolean { fun isSelected(userAccount: UserAccountEntity, accountFilter: List<BankAccountFilter>): Boolean {
if (accountFilter.isEmpty()) { if (accountFilter.isEmpty()) {

View File

@ -6,6 +6,7 @@ import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import net.codinux.banking.dataaccess.entities.UserAccountEntity import net.codinux.banking.dataaccess.entities.UserAccountEntity
import net.codinux.banking.ui.model.AccountTransactionViewModel import net.codinux.banking.ui.model.AccountTransactionViewModel
import net.codinux.banking.ui.model.AccountTransactionsFilter
import net.codinux.banking.ui.model.BankAccountFilter import net.codinux.banking.ui.model.BankAccountFilter
import net.codinux.banking.ui.model.TanChallengeReceived import net.codinux.banking.ui.model.TanChallengeReceived
import net.codinux.banking.ui.model.error.ApplicationError import net.codinux.banking.ui.model.error.ApplicationError
@ -19,11 +20,13 @@ class UiState : ViewModel() {
val transactions = MutableStateFlow<List<AccountTransactionViewModel>>(emptyList()) val transactions = MutableStateFlow<List<AccountTransactionViewModel>>(emptyList())
val drawerState = MutableStateFlow(DrawerState(DrawerValue.Open)) val drawerState = MutableStateFlow(DrawerState(DrawerValue.Closed))
val accountFilter = MutableStateFlow<List<BankAccountFilter>>(emptyList()) val accountFilter = MutableStateFlow<List<BankAccountFilter>>(emptyList())
val transactionsFilter = MutableStateFlow(AccountTransactionsFilter())
val showAddAccountDialog = MutableStateFlow(false) val showAddAccountDialog = MutableStateFlow(false)