diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/appskeleton/BottomBar.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/appskeleton/BottomBar.kt index bf30cc0..630ae9d 100644 --- a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/appskeleton/BottomBar.kt +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/appskeleton/BottomBar.kt @@ -1,33 +1,64 @@ package net.codinux.banking.ui.appskeleton +import androidx.compose.animation.* import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.width +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Menu -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material.icons.filled.Search +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment 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.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 net.codinux.banking.ui.config.Colors import net.codinux.banking.ui.config.DI import net.codinux.banking.ui.extensions.toggle +import org.jetbrains.compose.resources.imageResource private val uiState = DI.uiState +private val IconWidth = 48.dp + @Composable fun BottomBar() { + val userAccounts by uiState.userAccounts.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 configuration = LocalViewConfiguration.current + println("config: $configuration") + + val windowInfo = LocalWindowInfo.current + println("windowInfo: $windowInfo") + + BottomAppBar(modifier = Modifier.background(Colors.Accent)) { IconButton( onClick = { coroutineScope.launch { @@ -37,23 +68,80 @@ fun BottomBar() { Icon(Icons.Filled.Menu, contentDescription = "Open Navigation Drawer with sidebar menu") } - Column { - val title = if (accountFilter.isEmpty()) { - "Bankmeister" - } else { - val filter = accountFilter.first() - if (filter.bankAccount != null) { - filter.bankAccount.productName ?: filter.bankAccount.identifier - } else { - filter.userAccount.bankName + Row(Modifier.fillMaxWidth().padding(end = 64.dp)) { // 72.dp = leave space for Floating Action Button + val color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current) + + Column(Modifier.fillMaxHeight().weight(if (showSearchbar) 0.1f else 1f), verticalArrangement = Arrangement.Center) { + if (showSearchbar == false) { + val title = if (accountFilter.isEmpty()) { + "Bankmeister" + } else { + 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 } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/composables/NavigationMenuItem.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/composables/NavigationMenuItem.kt index 3241f23..5a74927 100644 --- a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/composables/NavigationMenuItem.kt +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/composables/NavigationMenuItem.kt @@ -22,7 +22,7 @@ import net.codinux.banking.ui.config.Colors import net.codinux.banking.ui.config.DI import org.jetbrains.compose.resources.DrawableResource -private val filterService = DI.bankAccountFilterService +private val filterService = DI.accountTransactionsFilterService @Composable fun NavigationMenuItem( diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/composables/TransactionsList.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/composables/TransactionsList.kt index 5f74789..e0b19b3 100644 --- a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/composables/TransactionsList.kt +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/composables/TransactionsList.kt @@ -23,7 +23,7 @@ import net.codinux.banking.ui.config.DI import net.codinux.banking.ui.state.UiState import org.jetbrains.compose.ui.tooling.preview.Preview -private val filterService = DI.bankAccountFilterService +private val filterService = DI.accountTransactionsFilterService private val formatUtil = DI.formatUtil @@ -36,10 +36,12 @@ fun TransactionsList(uiState: UiState) { val accountFilter by uiState.accountFilter.collectAsState() + val transactionsFilter by uiState.transactionsFilter.collectAsState() + val transactions by uiState.transactions.collectAsState() - val transactionsToDisplay by remember(accountFilter, transactions) { - derivedStateOf { filterService.filterAccounts(transactions, accountFilter) } + val transactionsToDisplay by remember(accountFilter, transactionsFilter, transactions) { + derivedStateOf { filterService.filterAccounts(transactions, accountFilter, transactionsFilter) } } val groupedByMonth by remember(transactionsToDisplay) { diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/config/DI.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/config/DI.kt index 20d799d..fde6540 100644 --- a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/config/DI.kt +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/config/DI.kt @@ -24,7 +24,7 @@ object DI { val bankIconService = BankIconService() - val bankAccountFilterService = BankAccountFilterService() + val accountTransactionsFilterService = AccountTransactionsFilterService() var bankingRepository: BankingRepository = InMemoryBankingRepository(emptyList()) diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/model/AccountTransactionsFilter.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/model/AccountTransactionsFilter.kt new file mode 100644 index 0000000..1c33bd9 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/model/AccountTransactionsFilter.kt @@ -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 + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/service/BankAccountFilterService.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/service/AccountTransactionsFilterService.kt similarity index 63% rename from composeApp/src/commonMain/kotlin/net/codinux/banking/ui/service/BankAccountFilterService.kt rename to composeApp/src/commonMain/kotlin/net/codinux/banking/ui/service/AccountTransactionsFilterService.kt index 344d700..e09ddce 100644 --- a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/service/BankAccountFilterService.kt +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/service/AccountTransactionsFilterService.kt @@ -3,17 +3,26 @@ package net.codinux.banking.ui.service import net.codinux.banking.dataaccess.entities.BankAccountEntity import net.codinux.banking.dataaccess.entities.UserAccountEntity import net.codinux.banking.ui.model.AccountTransactionViewModel +import net.codinux.banking.ui.model.AccountTransactionsFilter import net.codinux.banking.ui.model.BankAccountFilter -class BankAccountFilterService { +class AccountTransactionsFilterService { - fun filterAccounts(transactions: List, accountsFilter: List): List = - if (accountsFilter.isEmpty()) { + fun filterAccounts(transactions: List, accountsFilter: List, transactionsFilter: AccountTransactionsFilter): List { + val appliedAccountFilter = if (accountsFilter.isEmpty()) { transactions } else { 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): Boolean = accountsFilter.any { (userAccount, bankAccount) -> 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): Boolean { if (accountFilter.isEmpty()) { diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/state/UiState.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/state/UiState.kt index f914866..1cf54b9 100644 --- a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/state/UiState.kt +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/state/UiState.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.ViewModel import kotlinx.coroutines.flow.MutableStateFlow import net.codinux.banking.dataaccess.entities.UserAccountEntity 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.TanChallengeReceived import net.codinux.banking.ui.model.error.ApplicationError @@ -19,11 +20,13 @@ class UiState : ViewModel() { val transactions = MutableStateFlow>(emptyList()) - val drawerState = MutableStateFlow(DrawerState(DrawerValue.Open)) + val drawerState = MutableStateFlow(DrawerState(DrawerValue.Closed)) val accountFilter = MutableStateFlow>(emptyList()) + val transactionsFilter = MutableStateFlow(AccountTransactionsFilter()) + val showAddAccountDialog = MutableStateFlow(false)