Showing bank's favicon (if available)
This commit is contained in:
parent
4856ced158
commit
d9a3e942e9
|
@ -1,6 +1,11 @@
|
|||
package net.codinux.banking.ui
|
||||
|
||||
import android.os.Build
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
actual val Dispatchers.IOorDefault: CoroutineDispatcher
|
||||
get() = Dispatchers.IO
|
||||
|
||||
class AndroidPlatform : Platform {
|
||||
override val name: String = "Android ${Build.VERSION.SDK_INT}"
|
||||
|
|
|
@ -3,6 +3,10 @@ package net.codinux.banking.ui.service
|
|||
import android.graphics.BitmapFactory
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import java.net.URL
|
||||
|
||||
actual fun createImageBitmap(imageBytes: ByteArray): ImageBitmap =
|
||||
BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size).asImageBitmap()
|
||||
|
||||
actual fun fetchBytesFromUrl(url: String): ByteArray =
|
||||
URL(url).openStream().buffered().use { it.readBytes() }
|
|
@ -1,5 +1,10 @@
|
|||
package net.codinux.banking.ui
|
||||
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
|
||||
expect val Dispatchers.IOorDefault: CoroutineDispatcher
|
||||
|
||||
expect fun getPlatform(): Platform
|
||||
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
package net.codinux.banking.ui.composables
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import net.codinux.banking.client.model.UserAccount
|
||||
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
|
||||
|
||||
private val bankIconService = DI.bankIconService
|
||||
|
||||
@Composable
|
||||
fun BankIcon(userAccount: UserAccount?, modifier: Modifier = Modifier) {
|
||||
val iconUrl by remember(userAccount?.bic) { mutableStateOf(userAccount?.let { bankIconService.findIconForBank(it) }) }
|
||||
|
||||
BankIcon(iconUrl, modifier)
|
||||
}
|
||||
|
||||
private val bankingGroupMapper = BankingGroupMapper()
|
||||
|
||||
@Composable
|
||||
fun BankIcon(bank: BankInfo, modifier: Modifier = Modifier) {
|
||||
val iconUrl by remember(bank.bic) { mutableStateOf(bankIconService.findIconForBank(bank.name, bankingGroupMapper.getBankingGroup(bank.name, bank.bic))) }
|
||||
|
||||
BankIcon(iconUrl, modifier)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BankIcon(userAccount: UserAccountViewInfo?, modifier: Modifier = Modifier) {
|
||||
val iconUrl = userAccount?.let { bankIconService.findIconForBank(it.bankName, it.bankingGroup) }
|
||||
|
||||
BankIcon(iconUrl, modifier)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BankIcon(iconUrl: String?, modifier: Modifier = Modifier) {
|
||||
Column(modifier) {
|
||||
iconUrl?.let {
|
||||
IconForUrl(iconUrl, "Favicon of this bank", Modifier.width(16.dp).height(16.dp))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
package net.codinux.banking.ui.composables
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.codinux.banking.ui.IOorDefault
|
||||
import net.codinux.banking.ui.config.DI
|
||||
|
||||
private val imageCache = DI.imageCache
|
||||
|
||||
@Composable
|
||||
fun IconForUrl(iconUrl: String, contentDescription: String, modifier: Modifier = Modifier) {
|
||||
var imageBitmap by remember(iconUrl) { mutableStateOf<ImageBitmap?>(null) }
|
||||
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
if (iconUrl.endsWith(".svg") == false) { // SVG is not supported on Android
|
||||
coroutineScope.launch(Dispatchers.IOorDefault) {
|
||||
val received = imageCache.getImageBitmap(iconUrl)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
imageBitmap = received
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
imageBitmap?.let { imageBitmap ->
|
||||
Image(imageBitmap, contentDescription, modifier)
|
||||
}
|
||||
}
|
|
@ -9,25 +9,30 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import net.codinux.banking.client.model.UserAccount
|
||||
import net.codinux.banking.ui.config.DI
|
||||
import net.codinux.banking.ui.model.AccountTransactionViewModel
|
||||
|
||||
private val formatUtil = DI.formatUtil
|
||||
|
||||
@Composable
|
||||
fun TransactionListItem(transaction: AccountTransactionViewModel, backgroundColor: Color) {
|
||||
fun TransactionListItem(userAccount: UserAccount?, transaction: AccountTransactionViewModel, backgroundColor: Color) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
.background(color = backgroundColor)
|
||||
.padding(horizontal = 6.dp, vertical = 6.dp)
|
||||
) {
|
||||
Column(Modifier.weight(1f)) {
|
||||
Row {
|
||||
BankIcon(userAccount, Modifier.padding(end = 6.dp))
|
||||
|
||||
Text(
|
||||
text = transaction.otherPartyName ?: "",
|
||||
Modifier.fillMaxWidth(),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
|
||||
|
|
|
@ -15,7 +15,6 @@ import androidx.compose.ui.text.font.FontWeight
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import kotlinx.datetime.LocalDate
|
||||
import net.codinux.banking.client.model.AccountTransaction
|
||||
import net.codinux.banking.client.model.Amount
|
||||
import net.codinux.banking.ui.extensions.toBigDecimal
|
||||
import net.codinux.banking.ui.forms.RoundedCornersCard
|
||||
|
@ -28,6 +27,11 @@ 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 transactions by uiState.transactions.collectAsState()
|
||||
|
||||
val groupedByMonth by remember(transactions) {
|
||||
|
@ -53,7 +57,8 @@ fun TransactionsList(uiState: UiState) {
|
|||
RoundedCornersCard {
|
||||
Column(Modifier.background(Color.White)) { // LazyColumn inside LazyColumn is not allowed
|
||||
monthTransactions.forEachIndexed { index, transaction ->
|
||||
TransactionListItem(transaction, if (index % 2 == 1) Colors.Zinc100_50 else Color.Transparent)
|
||||
val backgroundColor = if (index % 2 == 1) Colors.Zinc100_50 else Color.Transparent
|
||||
TransactionListItem(usersById[transaction.userAccountId], transaction, backgroundColor)
|
||||
|
||||
if (index < monthTransactions.size - 1) {
|
||||
Divider(color = Colors.Zinc200, thickness = 1.dp)
|
||||
|
|
|
@ -18,8 +18,11 @@ object DI {
|
|||
|
||||
val formatUtil = FormatUtil()
|
||||
|
||||
val imageCache = ImageCache()
|
||||
|
||||
val bankFinder = BankFinder()
|
||||
|
||||
val bankIconService = BankIconService()
|
||||
|
||||
|
||||
var bankingRepository: BankingRepository = InMemoryBankingRepository(emptyList())
|
||||
|
|
|
@ -1,24 +1,20 @@
|
|||
package net.codinux.banking.ui.dialogs
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.codinux.banking.ui.forms.*
|
||||
import net.codinux.banking.ui.model.BankInfo
|
||||
import net.codinux.banking.ui.composables.BankIcon
|
||||
import net.codinux.banking.ui.config.Colors
|
||||
import net.codinux.banking.ui.config.DI
|
||||
import net.codinux.banking.ui.config.Style
|
||||
import net.codinux.banking.ui.forms.*
|
||||
import net.codinux.banking.ui.model.BankInfo
|
||||
|
||||
|
||||
private val bankingService = DI.bankingService
|
||||
|
@ -72,7 +68,16 @@ fun AddAccountDialog(
|
|||
getItemTitle = { bank -> bank.name },
|
||||
fetchSuggestions = { query -> bankingService.findBanks(query) }
|
||||
) { bank ->
|
||||
Text(bank.name)
|
||||
Row {
|
||||
BankIcon(bank, Modifier.padding(end = 6.dp))
|
||||
|
||||
Text(
|
||||
bank.name,
|
||||
Modifier.fillMaxWidth(),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
|
||||
Row(Modifier.fillMaxWidth().padding(top = 8.dp)) {
|
||||
Text(bank.bankCode)
|
||||
|
|
|
@ -20,7 +20,7 @@ import bankmeister.composeapp.generated.resources.zoom_in
|
|||
import bankmeister.composeapp.generated.resources.zoom_out
|
||||
import net.codinux.banking.client.model.tan.AllowedTanFormat
|
||||
import net.codinux.banking.client.model.tan.EnterTanResult
|
||||
import net.codinux.banking.ui.config.DI
|
||||
import net.codinux.banking.ui.composables.BankIcon
|
||||
import net.codinux.banking.ui.config.Internationalization
|
||||
import net.codinux.banking.ui.forms.OutlinedTextField
|
||||
import net.codinux.banking.ui.model.TanChallengeReceived
|
||||
|
@ -63,6 +63,8 @@ fun EnterTanDialog(tanChallengeReceived: TanChallengeReceived, onDismiss: () ->
|
|||
Column(Modifier.fillMaxWidth()) {
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
Row {
|
||||
BankIcon(challenge.user, Modifier.padding(end = 6.dp))
|
||||
|
||||
Text("${challenge.user.bankName}, Nutzer ${challenge.user.loginName}${challenge.account?.let { ", Konto ${it.productName ?: it.identifier}" } ?: ""}")
|
||||
}
|
||||
Text(
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
package net.codinux.banking.ui.service
|
||||
|
||||
import net.codinux.banking.client.model.BankingGroup
|
||||
import net.codinux.banking.client.model.UserAccount
|
||||
|
||||
class BankIconService { // TODO: extract to a common library
|
||||
|
||||
fun findIconForBank(user: UserAccount) = findIconForBank(user.bankName, user.bankingGroup)
|
||||
|
||||
fun findIconForBank(bankName: String, bankingGroup: BankingGroup? = null): String? = when (bankingGroup) {
|
||||
BankingGroup.Sparkasse -> "https://sparkasse.de/favicon-32x32.png"
|
||||
BankingGroup.DKB -> "https://www.ib.dkb.de/favicon.ico"
|
||||
BankingGroup.OldenburgischeLandesbank -> "https://olb.de/assets/img/icon/olb/favicon-32x32.png"
|
||||
|
||||
BankingGroup.VolksUndRaiffeisenbanken -> "https://vr.de/favicon.ico"
|
||||
BankingGroup.Sparda -> "https://www.sparda.de/hidden/layout/images/touchicons/favicon-32x32.png"
|
||||
BankingGroup.PSD -> "https://www.psd-muenchen.de/favicon.ico"
|
||||
BankingGroup.GLS -> "https://gls.de/assets/dist/img/icons/v2/favicon-32x32.png" // "https://gls.de/favicon.ico"
|
||||
|
||||
BankingGroup.DeutscheBank -> "https://www.deutsche-bank.de/etc/designs/db-eccs-pws-pwcc/assets/db-favicon-167x167.png" // "https://www.deutsche-bank.de/etc/designs/db-eccs-pws-pwcc/assets/favicon.svg" // https://master.dwebcms.db.com/application/themes/default/favicon/favicon-32x32.png
|
||||
BankingGroup.Postbank -> "https://postbank.de/etc/designs/pb-eccs-pb/icons/pb-favicon-167x167.png" // "https://postbank.de/etc/designs/pb-eccs-pb/icons/favicon.svg"
|
||||
|
||||
BankingGroup.Commerzbank -> "https://commerzbank.de/ms/media/favicons/apple-touch-icon-57x57_A.png"
|
||||
BankingGroup.Comdirect -> "https://comdirect.de/favicon.ico"
|
||||
|
||||
BankingGroup.Unicredit -> "https://hypovereinsbank.de/etc/designs/hypovereinsbank/img/favicon/favicon.ico"
|
||||
BankingGroup.Targobank -> "https://targobank.de/de/images/favicon/favicon.png" // https://targobank.de/favicon.ico
|
||||
BankingGroup.ING -> "https://ing.de/favicon.ico" // "https://cdn.ing.de/ing-cms-ui/129.0.2/assets/favicons/apple-touch-icon.png"
|
||||
BankingGroup.Santander -> "https://www.santander.de/ressourcen/img/favicons/favicon.ico"
|
||||
|
||||
BankingGroup.Norisbank -> "https://norisbank.de/etc/designs/db-eccs-nb/assets/nb-favicon-167x167.png" // ""https://norisbank.de/etc/designs/db-eccs-nb/assets/favicon.svg"
|
||||
BankingGroup.Degussa -> "https://www.degussa-bank.de/o/degussa-bank-theme/images/favicon.ico"
|
||||
|
||||
BankingGroup.N26 -> "https://n26.de/favicon.ico"
|
||||
|
||||
else -> null // TODO: call Favicon web service
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
package net.codinux.banking.ui.service
|
||||
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import net.codinux.log.logger
|
||||
|
||||
// TODO: save retrieved bytes to disk so that they don't have to be retrieved on the next app start again
|
||||
class ImageCache {
|
||||
|
||||
private val mutex = Mutex()
|
||||
|
||||
private val imageBytesCache = mutableMapOf<String, Deferred<ByteArray?>>()
|
||||
|
||||
private val imageBitmapCache = mutableMapOf<String, Deferred<ImageBitmap?>>()
|
||||
|
||||
private val log by logger()
|
||||
|
||||
// would be create but didn't get it to work with Ktor. Had therefor to introduce platform specific URL loading with loadImageBytes()
|
||||
// simply re-using KtorWebClient as it already has all platform specific engines set up
|
||||
// private val webClient = object : KtorWebClient() {
|
||||
//
|
||||
// val ktorClient = client
|
||||
// init {
|
||||
// client
|
||||
// }
|
||||
// }
|
||||
|
||||
suspend fun getImageBytes(url: String): ByteArray? = coroutineScope {
|
||||
mutex.withLock {
|
||||
var cached = imageBytesCache[url]
|
||||
if (cached == null || cached.isCancelled) {
|
||||
//LAZY - to free the mutex lock as fast as possible
|
||||
val loadJob = async(start = CoroutineStart.LAZY) {
|
||||
loadImage(url) // TODO: in case of failure, remove url from cache and try again?
|
||||
}
|
||||
|
||||
imageBytesCache[url] = loadJob
|
||||
loadJob
|
||||
} else {
|
||||
cached
|
||||
}
|
||||
}.await()
|
||||
}
|
||||
|
||||
suspend fun getImageBitmap(url: String): ImageBitmap? = coroutineScope {
|
||||
getImageBytes(url)?.let {
|
||||
createImageBitmap(it)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadImage(url: String): ByteArray? {
|
||||
return try {
|
||||
// val clientResponse = webClient.ktorClient.get(url)
|
||||
// if (clientResponse.status.isSuccess()) {
|
||||
// clientResponse.readBytes()
|
||||
// } else {
|
||||
// null
|
||||
// }
|
||||
|
||||
fetchBytesFromUrl(url)
|
||||
} catch (e: Throwable) {
|
||||
log.error(e) { "Failed to load image from url '$url'" }
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -3,3 +3,5 @@ package net.codinux.banking.ui.service
|
|||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
|
||||
expect fun createImageBitmap(imageBytes: ByteArray): ImageBitmap
|
||||
|
||||
expect fun fetchBytesFromUrl(url: String): ByteArray
|
|
@ -0,0 +1,16 @@
|
|||
package net.codinux.banking.ui
|
||||
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
actual val Dispatchers.IOorDefault: CoroutineDispatcher
|
||||
get() = Dispatchers.IO
|
||||
|
||||
|
||||
class JVMPlatform: Platform {
|
||||
override val name: String = "Java ${System.getProperty("java.version")}"
|
||||
|
||||
override val type: PlatformType = PlatformType.JVM
|
||||
}
|
||||
|
||||
actual fun getPlatform(): Platform = JVMPlatform()
|
|
@ -1,9 +0,0 @@
|
|||
package net.codinux.banking.ui
|
||||
|
||||
class JVMPlatform: Platform {
|
||||
override val name: String = "Java ${System.getProperty("java.version")}"
|
||||
|
||||
override val type: PlatformType = PlatformType.JVM
|
||||
}
|
||||
|
||||
actual fun getPlatform(): Platform = JVMPlatform()
|
|
@ -2,6 +2,7 @@ package net.codinux.banking.ui.dialogs
|
|||
|
||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||
import androidx.compose.runtime.Composable
|
||||
import net.codinux.banking.client.model.BankingGroup
|
||||
import net.codinux.banking.client.model.UserAccountViewInfo
|
||||
import net.codinux.banking.client.model.tan.*
|
||||
import net.codinux.banking.ui.model.TanChallengeReceived
|
||||
|
@ -10,7 +11,7 @@ import net.codinux.banking.ui.model.TanChallengeReceived
|
|||
@Composable
|
||||
fun EnterTanDialogPreview_EnterTan() {
|
||||
val tanMethods = listOf(TanMethod("Zeig mich an", TanMethodType.AppTan, "902"))
|
||||
val user = UserAccountViewInfo("12345678", "SupiDupiNutzer", "Abzockbank")
|
||||
val user = UserAccountViewInfo("12345678", "SupiDupiNutzer", "Abzockbank", BankingGroup.Postbank)
|
||||
val tanChallenge = TanChallenge(TanChallengeType.EnterTan, ActionRequiringTan.GetAccountInfo, "Geben Sie die TAN ein", tanMethods.first().identifier, tanMethods, user = user)
|
||||
|
||||
EnterTanDialog(TanChallengeReceived(tanChallenge) { }) { }
|
||||
|
@ -24,7 +25,7 @@ fun EnterTanDialogPreview_TanImage() {
|
|||
val tanMethod = TanMethod("photoTAN-Verfahren", TanMethodType.photoTan, "902", 6, AllowedTanFormat.Numeric)
|
||||
val tanImage = TanImage("image/png", tanImageBytes)
|
||||
|
||||
val user = UserAccountViewInfo("10010010", "Ihr krasser Login Name", "Phantasie Bank")
|
||||
val user = UserAccountViewInfo("10010010", "Ihr krasser Login Name", "Phantasie Bank", BankingGroup.Comdirect)
|
||||
|
||||
val tanChallenge = TanChallenge(TanChallengeType.Image, ActionRequiringTan.GetAccountInfo, "Geben Sie die TAN ein", tanMethod.identifier, listOf(tanMethod), null, emptyList(), tanImage, null, user)
|
||||
|
||||
|
|
|
@ -3,6 +3,10 @@ package net.codinux.banking.ui.service
|
|||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.toComposeImageBitmap
|
||||
import org.jetbrains.skia.Image
|
||||
import java.net.URL
|
||||
|
||||
actual fun createImageBitmap(imageBytes: ByteArray): ImageBitmap =
|
||||
Image.makeFromEncoded(imageBytes).toComposeImageBitmap()
|
||||
|
||||
actual fun fetchBytesFromUrl(url: String): ByteArray =
|
||||
URL(url).openStream().buffered().use { it.readAllBytes() }
|
|
@ -1,5 +1,12 @@
|
|||
package net.codinux.banking.ui
|
||||
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
actual val Dispatchers.IOorDefault: CoroutineDispatcher
|
||||
get() = Dispatchers.Default
|
||||
|
||||
|
||||
class JsPlatform: Platform {
|
||||
override val name: String = "Web with Kotlin/Js"
|
||||
|
||||
|
|
|
@ -6,3 +6,5 @@ import org.jetbrains.skia.Image
|
|||
|
||||
actual fun createImageBitmap(imageBytes: ByteArray): ImageBitmap =
|
||||
Image.makeFromEncoded(imageBytes).toComposeImageBitmap()
|
||||
|
||||
actual fun fetchBytesFromUrl(url: String): ByteArray = ByteArray(0)
|
Loading…
Reference in New Issue