Added a search field to BottomBar to search / filter transactions
This commit is contained in:
parent
b2dfd3fb71
commit
d0069a0563
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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(
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()) {
|
|
@ -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)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue