diff --git a/composeApp/src/commonMain/composeResources/drawable/account.xml b/composeApp/src/commonMain/composeResources/drawable/account.xml new file mode 100644 index 0000000..f6b8d2c --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/account.xml @@ -0,0 +1,10 @@ + + + diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/App.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/App.kt index f2692c0..446ab8f 100644 --- a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/App.kt +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/App.kt @@ -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) } } \ No newline at end of file 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 new file mode 100644 index 0000000..a363d6c --- /dev/null +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/appskeleton/BottomBar.kt @@ -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 + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/appskeleton/SideMenu.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/appskeleton/SideMenu.kt new file mode 100644 index 0000000..ba066f5 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/appskeleton/SideMenu.kt @@ -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) + } + } + } + ) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/composables/BankIcon.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/composables/BankIcon.kt index b47ff6d..08f93b1 100644 --- a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/composables/BankIcon.kt +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/composables/BankIcon.kt @@ -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 } } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/composables/BanksList.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/composables/BanksList.kt new file mode 100644 index 0000000..d8b3007 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/composables/BanksList.kt @@ -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) + } + } + } + } +} \ No newline at end of file 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 a7eb892..fbceebd 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 @@ -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) diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/config/Colors.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/config/Colors.kt index 2b8b526..58411dd 100644 --- a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/config/Colors.kt +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/config/Colors.kt @@ -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) diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/extensions/ColorExtensions.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/extensions/ColorExtensions.kt new file mode 100644 index 0000000..b27aa56 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/extensions/ColorExtensions.kt @@ -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") +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/extensions/DrawerStateExtensions.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/extensions/DrawerStateExtensions.kt new file mode 100644 index 0000000..d4601a8 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/extensions/DrawerStateExtensions.kt @@ -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() + } +} \ No newline at end of file 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 84e0c22..8e0ec6e 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 @@ -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>(emptyList()) + + val drawerState = MutableStateFlow(DrawerState(DrawerValue.Open)) + + val applicationErrorOccurred = MutableStateFlow(null) val bankingClientErrorOccurred = MutableStateFlow(null)