diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/appskeleton/FloatingActionMenu.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/appskeleton/FloatingActionMenu.kt new file mode 100644 index 0000000..89712e8 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/appskeleton/FloatingActionMenu.kt @@ -0,0 +1,99 @@ +package net.codinux.banking.ui.appskeleton + +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import net.codinux.banking.ui.config.DI +import net.codinux.banking.ui.config.Style.FabMenuSpacing +import net.codinux.banking.ui.config.Style.FabSize +import net.codinux.banking.ui.config.Style.SmallFabSize +import net.codinux.banking.ui.model.ShowTransferMoneyDialogData + + +private val uiState = DI.uiState + +@Composable +fun FloatingActionMenu( + showFloatingActionMenu: Boolean, + menuItemClicked: () -> Unit +) { + + val fabVisibilityAnimation = animateFloatAsState(targetValue = if (showFloatingActionMenu) 1f else 0f) + + val bottomPadding = FabSize + FabSize / 2 + + val accountsThatSupportMoneyTransfer = uiState.accountsThatSupportMoneyTransfer.collectAsState().value + + val coroutineScope = rememberCoroutineScope() + + + fun handleClick(action: () -> Unit) { + menuItemClicked() + + coroutineScope.launch { + delay(50) + + action() + } + } + + + if (fabVisibilityAnimation.value > 0) { + Box(Modifier.fillMaxSize().padding(bottom = bottomPadding, end = 12.dp), contentAlignment = Alignment.BottomEnd) { + Column(Modifier, horizontalAlignment = Alignment.End) { + FloatingActionMenuItem("Überweisung", "Neue Überweisung", enabled = accountsThatSupportMoneyTransfer.isNotEmpty()) { + handleClick { + uiState.showTransferMoneyDialogData.value = ShowTransferMoneyDialogData() + } + } + + FloatingActionMenuItem("Konto", "Neues Konto hinzufügen") { + handleClick { + uiState.showAddAccountDialog.value = true + } + } + } + } + } +} + +@Composable +fun FloatingActionMenuItem( + label: String, + contentDescription: String, + enabled: Boolean = true, + onClick: () -> Unit +) { + + Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(bottom = FabMenuSpacing).clickable(enabled) { onClick() }) { + Text(label, fontSize = 16.sp, color = contentColorFor(MaterialTheme.colors.secondary).copy(if (enabled) 1f else ContentAlpha.disabled), modifier = Modifier.padding(end = 8.dp).background(MaterialTheme.colors.secondary).shadow(20.dp).padding(horizontal = 20.dp, vertical = 4.dp)) // the same background color as the FAB + + FloatingActionButton( + shape = CircleShape, + elevation = FloatingActionButtonDefaults.elevation(defaultElevation = 20.dp), + modifier = Modifier.padding(end = (FabSize - SmallFabSize) / 2).size(SmallFabSize), + onClick = { + if (enabled) { + onClick() + } + } + ) { + Icon(Icons.Filled.Add, contentDescription = contentDescription, tint = LocalContentColor.current.copy(if (enabled) LocalContentAlpha.current else ContentAlpha.disabled)) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/appskeleton/SideMenuContent.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/appskeleton/SideMenuContent.kt index ad9ebf6..5d0a16b 100644 --- a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/appskeleton/SideMenuContent.kt +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/appskeleton/SideMenuContent.kt @@ -56,7 +56,9 @@ private val VerticalSpacing = 8.dp fun SideMenuContent() { val drawerState = uiState.drawerState.collectAsState().value - val accounts = uiState.banks.collectAsState().value + val accounts = uiState.accounts.collectAsState().value + + val accountsThatSupportMoneyTransfer = uiState.accountsThatSupportMoneyTransfer.collectAsState().value val coroutineScope = rememberCoroutineScope() @@ -97,7 +99,7 @@ fun SideMenuContent() { } } - if (accounts.isNotEmpty()) { + if (accountsThatSupportMoneyTransfer.isNotEmpty()) { Spacer(Modifier.height(VerticalSpacing)) NavigationMenuItem(itemModifier, "Neue Überweisung", textColor, horizontalPadding = ItemHorizontalPadding, diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/config/Style.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/config/Style.kt index 1b09a41..29f1264 100644 --- a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/config/Style.kt +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/config/Style.kt @@ -19,6 +19,15 @@ object Style { val ListItemHeaderWeight = FontWeight.Medium // couldn't believe it, the FontWeights look different on Desktop and Android + val FabSize = 56.dp + + val SmallFabSize = 46.dp + + val FabSpacing = 16.dp + + val FabMenuSpacing = FabSpacing / 2 + + val DividerThickness = 1.dp } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/dialogs/TransferMoneyDialog.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/dialogs/TransferMoneyDialog.kt index 60a4a6c..948b6b6 100644 --- a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/dialogs/TransferMoneyDialog.kt +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/dialogs/TransferMoneyDialog.kt @@ -42,8 +42,7 @@ fun TransferMoneyDialog( val accountsToBank = banks.sortedBy { it.displayIndex } .flatMap { bank -> bank.accountsSorted.map { it to bank } }.toMap() - val accountsSupportingTransferringMoney = banks.flatMap { it.accounts } - .filter { it.supportsMoneyTransfer } + val accountsSupportingTransferringMoney = uiState.accountsThatSupportMoneyTransfer.collectAsState().value if (accountsSupportingTransferringMoney.isEmpty()) { uiState.applicationErrorOccurred(ErroneousAction.TransferMoney, "Keines Ihrer Konten unterstützt das Überweisen von Geld") diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/screens/MainScreen.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/screens/MainScreen.kt index 1abfd69..4632362 100644 --- a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/screens/MainScreen.kt +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/screens/MainScreen.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Close import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp @@ -30,6 +31,8 @@ fun MainScreen() { val fabPositionAdjustment = if (isMobile) 44.dp // FabSpacing = 16.dp + FAB height (= 56.dp) / 2 else (-10).dp + var showFloatingActionMenu by remember { mutableStateOf(false) } + val desktopDrawerWidth = 350.dp val uiState = DI.uiState @@ -51,9 +54,13 @@ fun MainScreen() { FloatingActionButton( shape = CircleShape, modifier = Modifier.offset(x = 4.dp, y = fabPositionAdjustment), - onClick = { uiState.showAddAccountDialog.value = true } + onClick = { showFloatingActionMenu = !showFloatingActionMenu } ) { - Icon(Icons.Filled.Add, contentDescription = "Add a bank account") + if (showFloatingActionMenu) { + Icon(Icons.Filled.Close, contentDescription = "Menü zum Hinzufügen eines Kontos, für eine neue Überweisung, ... verstecken") + } else { + Icon(Icons.Filled.Add, contentDescription = "Zeigt ein Menü zum Hinzufügen eines Kontos, für eine neue Überweisung, ... an") + } } }, drawerContent = { if (isMobile) SideMenuContent() else null }, @@ -85,6 +92,9 @@ fun MainScreen() { } } + + FloatingActionMenu(showFloatingActionMenu) { showFloatingActionMenu = false } + if (showFilterBar.value) { FilterBar() } 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 01970cd..f5a53c1 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 @@ -3,8 +3,8 @@ package net.codinux.banking.ui.state import androidx.compose.material.DrawerState import androidx.compose.material.DrawerValue import androidx.lifecycle.ViewModel -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.* import net.codinux.banking.client.model.tan.EnterTanResult import net.codinux.banking.client.model.tan.TanChallenge import net.codinux.banking.persistence.entities.HoldingEntity @@ -25,6 +25,18 @@ class UiState : ViewModel() { val banks = MutableStateFlow>(emptyList()) + val accounts = banks.map { it.flatMap { it.accounts } }.stateIn( + scope = this.viewModelScope, + started = SharingStarted.Eagerly, + initialValue = emptyList() + ) + + val accountsThatSupportMoneyTransfer = accounts.map { it.filter { it.supportsMoneyTransfer } }.stateIn( + scope = this.viewModelScope, + started = SharingStarted.Eagerly, + initialValue = emptyList() + ) + val transactions = MutableStateFlow>(emptyList()) val holdings = MutableStateFlow>(emptyList())