Added menu button and side navigation menu with all accounts

This commit is contained in:
dankito 2024-08-28 22:24:11 +02:00
parent 5cf086485e
commit 572bd8e9d8
11 changed files with 280 additions and 36 deletions

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,24C18.6274,24 24,18.6274 24,12C24,5.3726 18.6274,0 12,0C5.3726,0 0,5.3726 0,12C0,18.6274 5.3726,24 12,24ZM6,7.5C6,6.9477 6.4477,6.5 7,6.5H17C17.5523,6.5 18,6.9477 18,7.5V16.5C18,17.0523 17.5523,17.5 17,17.5H7C6.4477,17.5 6,17.0523 6,16.5V7.5ZM7.5,12.9C7.5,12.6791 7.6791,12.5 7.9,12.5H16.1C16.3209,12.5 16.5,12.6791 16.5,12.9V13.1C16.5,13.3209 16.3209,13.5 16.1,13.5H7.9C7.6791,13.5 7.5,13.3209 7.5,13.1V12.9ZM13.9,8.5C13.6791,8.5 13.5,8.6791 13.5,8.9V9.3C13.5,9.5209 13.6791,9.7 13.9,9.7H16.1C16.3209,9.7 16.5,9.5209 16.5,9.3V8.9C16.5,8.6791 16.3209,8.5 16.1,8.5H13.9ZM7.5,14.9C7.5,14.6791 7.6791,14.5 7.9,14.5H16.1C16.3209,14.5 16.5,14.6791 16.5,14.9V15.1C16.5,15.3209 16.3209,15.5 16.1,15.5H7.9C7.6791,15.5 7.5,15.3209 7.5,15.1V14.9Z"
android:fillColor="#989792"
android:fillType="evenOdd"/>
</vector>

View File

@ -14,11 +14,13 @@ import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.launch
import net.codinux.banking.ui.appskeleton.BottomBar
import net.codinux.banking.ui.appskeleton.SideMenu
import net.codinux.banking.ui.composables.StateHandler
import net.codinux.banking.ui.composables.TransactionsList
import net.codinux.banking.ui.dialogs.AddAccountDialog
import net.codinux.banking.ui.config.Colors
import net.codinux.banking.ui.config.DI
import net.codinux.banking.ui.dialogs.AddAccountDialog
import net.codinux.log.LoggerFactory
import org.jetbrains.compose.ui.tooling.preview.Preview
@ -31,7 +33,7 @@ private val typography = Typography(
fun App() {
LoggerFactory.defaultLoggerName = "net.codinux.banking.ui.Bankmeister"
val colors = MaterialTheme.colors.copy(secondary = Colors.CodinuxSecondaryColor, onSecondary = Color.White)
val colors = MaterialTheme.colors.copy(primary = Colors.Primary, primaryVariant = Colors.PrimaryDark, secondary = Colors.Accent, onSecondary = Color.White)
var showAddAccountDialog by remember { mutableStateOf(false) }
@ -43,29 +45,34 @@ fun App() {
MaterialTheme(colors = colors, typography = typography) {
Box {
Column(
Modifier.fillMaxWidth().fillMaxHeight().background(color = Colors.Zinc100),
horizontalAlignment = Alignment.CenterHorizontally
) {
TransactionsList(DI.uiState)
}
Row(Modifier.align(Alignment.BottomEnd)) {
FloatingActionButton(
shape = CircleShape,
modifier = Modifier.offset((-12).dp, (-12).dp),
onClick = { showAddAccountDialog = true }
SideMenu {
Box(modifier = Modifier.background(Colors.CodinuxSecondaryColor).padding(0.dp)) {
Column(
Modifier.fillMaxWidth().fillMaxHeight().background(color = Colors.Zinc100)
) {
Icon(Icons.Filled.Add, contentDescription = "Add a bank account")
Column(Modifier.fillMaxWidth().weight(1f), horizontalAlignment = Alignment.CenterHorizontally) {
TransactionsList(DI.uiState)
}
BottomBar()
}
Row(Modifier.align(Alignment.BottomEnd)) {
FloatingActionButton(
shape = CircleShape,
modifier = Modifier.offset((-12).dp, (-26).dp),
onClick = { showAddAccountDialog = true }
) {
Icon(Icons.Filled.Add, contentDescription = "Add a bank account")
}
}
if (showAddAccountDialog) {
AddAccountDialog { showAddAccountDialog = false }
}
}
if (showAddAccountDialog) {
AddAccountDialog { showAddAccountDialog = false }
}
StateHandler(DI.uiState)
}
StateHandler(DI.uiState)
}
}

View File

@ -0,0 +1,40 @@
package net.codinux.banking.ui.appskeleton
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.width
import androidx.compose.material.BottomAppBar
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Menu
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
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
private val uiState = DI.uiState
@Composable
fun BottomBar() {
val coroutineScope = rememberCoroutineScope()
BottomAppBar(modifier = Modifier.background(Colors.Accent)) {
IconButton(
onClick = { coroutineScope.launch {
uiState.drawerState.value.toggle()
} }
) {
Icon(Icons.Filled.Menu, contentDescription = "Open Navigation Drawer with sidebar menu")
}
Spacer(Modifier.weight(1f))
Spacer(Modifier.width(72.dp)) // space for Floating Action Button
}
}

View File

@ -0,0 +1,67 @@
package net.codinux.banking.ui.appskeleton
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import bankmeister.composeapp.generated.resources.AppIcon
import bankmeister.composeapp.generated.resources.Res
import net.codinux.banking.ui.composables.BanksList
import net.codinux.banking.ui.config.Colors
import net.codinux.banking.ui.config.DI
import org.jetbrains.compose.resources.imageResource
private val uiState = DI.uiState
private val HeaderHeight = 160f
private val HeaderBackground = Brush.linearGradient(
colors = listOf(
Color(0xFF00695C), // endColor: #00695C
Color(0xFF009688), // centerColor: #009688
Color(0xFF4DB6AC), // startColor: #4DB6AC
)
)
@Composable
fun SideMenu(appContent: @Composable () -> Unit) {
val drawerState = uiState.drawerState.collectAsState().value
ModalDrawer(
modifier = Modifier.fillMaxHeight(),
drawerState = drawerState,
content = appContent,
drawerBackgroundColor = Colors.DrawerContentBackground,
drawerContentColor = Colors.DrawerPrimaryText, // seems to have no effect
drawerContent = {
Column(Modifier) {
Column(Modifier.fillMaxWidth().height(HeaderHeight.dp).background(HeaderBackground).padding(16.dp)) {
Spacer(Modifier.weight(1f))
Image(imageResource(Res.drawable.AppIcon), "Bankmeister's app icon", Modifier.size(48.dp))
Text("Bankmeister", color = Color.White, modifier = Modifier.padding(top = 16.dp, bottom = 8.dp))
Text("Version 1.0.0 Alpha 12", color = Color.LightGray)
}
Divider(color = Colors.DrawerDivider)
Column(Modifier.padding(horizontal = 16.dp, vertical = 24.dp)) {
Column(Modifier.height(40.dp), verticalArrangement = Arrangement.Center) {
Text("Konten", color = Colors.DrawerPrimaryText)
}
BanksList(textColor = Colors.DrawerPrimaryText)
}
}
}
)
}

View File

@ -1,8 +1,8 @@
package net.codinux.banking.ui.composables
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@ -14,37 +14,45 @@ import net.codinux.banking.client.model.UserAccountViewInfo
import net.codinux.banking.ui.config.DI
import net.codinux.banking.ui.model.BankInfo
import net.dankito.banking.banklistcreator.prettifier.BankingGroupMapper
import org.jetbrains.compose.resources.DrawableResource
import org.jetbrains.compose.resources.vectorResource
private val bankIconService = DI.bankIconService
private val DefaultIconModifier: Modifier = Modifier.size(16.dp)
@Composable
fun BankIcon(userAccount: UserAccount?, modifier: Modifier = Modifier) {
fun BankIcon(userAccount: UserAccount?, modifier: Modifier = Modifier, iconModifier: Modifier = DefaultIconModifier, fallbackIcon: DrawableResource? = null) {
val iconUrl by remember(userAccount?.bic) { mutableStateOf(userAccount?.let { bankIconService.findIconForBank(it) }) }
BankIcon(iconUrl, modifier)
BankIcon(iconUrl, modifier, iconModifier, fallbackIcon = fallbackIcon)
}
private val bankingGroupMapper = BankingGroupMapper()
@Composable
fun BankIcon(bank: BankInfo, modifier: Modifier = Modifier) {
fun BankIcon(bank: BankInfo, modifier: Modifier = Modifier, iconModifier: Modifier = DefaultIconModifier, fallbackIcon: DrawableResource? = null) {
val iconUrl by remember(bank.bic) { mutableStateOf(bankIconService.findIconForBank(bank.name, bankingGroupMapper.getBankingGroup(bank.name, bank.bic))) }
BankIcon(iconUrl, modifier)
BankIcon(iconUrl, modifier, iconModifier, fallbackIcon = fallbackIcon)
}
@Composable
fun BankIcon(userAccount: UserAccountViewInfo?, modifier: Modifier = Modifier) {
fun BankIcon(userAccount: UserAccountViewInfo?, modifier: Modifier = Modifier, iconModifier: Modifier = DefaultIconModifier, fallbackIcon: DrawableResource? = null) {
val iconUrl = userAccount?.let { bankIconService.findIconForBank(it.bankName, it.bankingGroup) }
BankIcon(iconUrl, modifier)
BankIcon(iconUrl, modifier, iconModifier, fallbackIcon = fallbackIcon)
}
@Composable
fun BankIcon(iconUrl: String?, modifier: Modifier = Modifier) {
fun BankIcon(iconUrl: String?, modifier: Modifier = Modifier, iconModifier: Modifier = DefaultIconModifier, contentDescription: String = "Favicon of this bank", fallbackIcon: DrawableResource? = null) {
Column(modifier) {
iconUrl?.let {
IconForUrl(iconUrl, "Favicon of this bank", Modifier.width(16.dp).height(16.dp))
if (iconUrl != null) {
IconForUrl(iconUrl, contentDescription, modifier = iconModifier)
} else if (fallbackIcon != null) {
Image(vectorResource(fallbackIcon), contentDescription, iconModifier)
} else {
Column(iconModifier) { } // show a placeholder for consistent layout
}
}
}

View File

@ -0,0 +1,47 @@
package net.codinux.banking.ui.composables
import androidx.compose.foundation.layout.*
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import bankmeister.composeapp.generated.resources.Res
import bankmeister.composeapp.generated.resources.account
import net.codinux.banking.ui.config.DI
private val uiState = DI.uiState
private val IconTextSpacing = 36.dp
@Composable
fun BanksList(modifier: Modifier = Modifier, itemModifier: Modifier = Modifier.height(48.dp).widthIn(min = 300.dp).padding(start = 8.dp), iconSize: Dp = 24.dp, textColor: Color = Color.White) {
val userAccounts = uiState.userAccounts.collectAsState()
Column(modifier) {
Row(itemModifier, verticalAlignment = Alignment.CenterVertically) {
BankIcon(null as? String?, Modifier.padding(end = IconTextSpacing), Modifier.size(iconSize), fallbackIcon = Res.drawable.account)
Text("Alle Konten", color = textColor)
}
userAccounts.value.forEach { userAccount ->
Spacer(Modifier.fillMaxWidth().height(12.dp))
Row(itemModifier, verticalAlignment = Alignment.CenterVertically) {
BankIcon(userAccount, Modifier.padding(end = IconTextSpacing), Modifier.size(iconSize), fallbackIcon = Res.drawable.account)
Text(userAccount.bankName, color = textColor)
}
userAccount.accounts.sortedBy { it.displayIndex }.forEach { account ->
Column(itemModifier.padding(start = iconSize + IconTextSpacing), verticalArrangement = Arrangement.Center) {
Text(account.productName ?: account.identifier, color = textColor)
}
}
}
}
}

View File

@ -27,9 +27,9 @@ private val formatUtil = DI.formatUtil
@Composable
fun TransactionsList(uiState: UiState) {
val users by uiState.userAccounts.collectAsState()
val usersById by remember(users) {
derivedStateOf { users.associateBy { it.id } }
val userAccounts by uiState.userAccounts.collectAsState()
val userAccountsId by remember(userAccounts) {
derivedStateOf { userAccounts.associateBy { it.id } }
}
val transactions by uiState.transactions.collectAsState()
@ -58,7 +58,7 @@ fun TransactionsList(uiState: UiState) {
Column(Modifier.background(Color.White)) { // LazyColumn inside LazyColumn is not allowed
monthTransactions.forEachIndexed { index, transaction ->
val backgroundColor = if (index % 2 == 1) Colors.Zinc100_50 else Color.Transparent
TransactionListItem(usersById[transaction.userAccountId], transaction, backgroundColor)
TransactionListItem(userAccountsId[transaction.userAccountId], transaction, backgroundColor)
if (index < monthTransactions.size - 1) {
Divider(color = Colors.Zinc200, thickness = 1.dp)

View File

@ -1,9 +1,32 @@
package net.codinux.banking.ui.config
import androidx.compose.ui.graphics.Color
import net.codinux.banking.ui.extensions.Color
object Colors {
val Primary = Color("#014e45")
val PrimaryDark = Color("#FF042204")
val Accent = Color("#00786a")
val PrimaryTextColorDark = Color("#BABABA")
val PrimaryTextColorLight = Color("#000000")
val BackgroundColorDark = Color("#303030")
val BackgroundColorLight = Color("#FFFFFF")
val DrawerContentBackground = BackgroundColorDark
val DrawerPrimaryText = PrimaryTextColorDark
val DrawerDivider = PrimaryTextColorDark
val CodinuxPrimaryColor = Color(30, 54, 78)
val CodinuxSecondaryColor = Color(251, 187, 33)

View File

@ -0,0 +1,25 @@
package net.codinux.banking.ui.extensions
import androidx.compose.ui.graphics.Color
fun Color(hex: String): Color {
val colorInt = parseColor(hex)
return Color(colorInt)
}
fun parseColor(colorString: String): Int {
val colorString2 = if (colorString[0] == '#') colorString.substring(1) else colorString
if (colorString2.length == 6 || colorString2.length == 8) {
// Use a long to avoid rollovers on #ffXXXXXX
var color = colorString2.toLong(16)
if (colorString2.length == 6) {
// Set the alpha value
color = color or 0x00000000ff000000L
}
return color.toInt()
}
throw IllegalArgumentException("Unknown color: $colorString")
}

View File

@ -0,0 +1,11 @@
package net.codinux.banking.ui.extensions
import androidx.compose.material.DrawerState
suspend fun DrawerState.toggle() {
if (this.isClosed) {
this.open()
} else {
this.close()
}
}

View File

@ -1,5 +1,7 @@
package net.codinux.banking.ui.state
import androidx.compose.material.DrawerState
import androidx.compose.material.DrawerValue
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import net.codinux.banking.dataaccess.entities.UserAccountEntity
@ -15,6 +17,10 @@ class UiState : ViewModel() {
val transactions = MutableStateFlow<List<AccountTransactionViewModel>>(emptyList())
val drawerState = MutableStateFlow(DrawerState(DrawerValue.Open))
val applicationErrorOccurred = MutableStateFlow<ApplicationError?>(null)
val bankingClientErrorOccurred = MutableStateFlow<BankingClientError?>(null)