From 75e2268d107e448f7e61a5b1b37174c24fbe67e3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 4 Sep 2023 09:52:39 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../core/ui/extensions/CryptoCurrency.kt | 66 ++++---- .../com/tangem/core/ui/res/TangemTheme.kt | 2 + .../main/res/drawable/ic_custom_token_44.xml | 10 ++ .../TokenDetailsSkeletonStateConverter.kt | 8 +- .../presentation/common/WalletPreviewData.kt | 79 +++++---- .../common/component/TokenItem.kt | 9 +- .../common/component/token/TokenIcon.kt | 152 ------------------ .../component/token/icon/ContentIcon.kt | 129 +++++++++++++++ .../common/component/token/icon/IconBadge.kt | 57 +++++++ .../common/component/token/icon/TokenIcon.kt | 95 +++++++++++ .../common/state/TokenItemState.kt | 105 ++++++++---- .../CryptoCurrencyToIconStateConverter.kt | 48 ++++++ .../organizetokens/OrganizeTokensScreen.kt | 9 +- .../CryptoCurrencyToDraggableItemConverter.kt | 10 +- ...ryptoCurrencyStatusToTokenItemConverter.kt | 14 +- 15 files changed, 522 insertions(+), 271 deletions(-) create mode 100644 core/ui/src/main/res/drawable/ic_custom_token_44.xml delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenIcon.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/ContentIcon.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/IconBadge.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/TokenIcon.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/utils/CryptoCurrencyToIconStateConverter.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/CryptoCurrency.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/CryptoCurrency.kt index 7c32f55014..752b3cc492 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/CryptoCurrency.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/CryptoCurrency.kt @@ -1,45 +1,49 @@ package com.tangem.core.ui.extensions import androidx.annotation.DrawableRes -import com.tangem.core.ui.R +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.luminance +import androidx.core.graphics.toColorInt +import com.tangem.core.ui.res.TangemColorPalette import com.tangem.domain.tokens.models.CryptoCurrency +private const val LIGHT_LUMINANCE = 0.5f +private const val COLOR_HEX_START_INDEX = 2 +private const val COLOR_HEX_END_INDEX = 7 + /** - * Retrieves the resource ID for the network badge of a [CryptoCurrency]. + * Retrieves the resource ID for the network of a [CryptoCurrency]. * - * This property provides a way to fetch the appropriate drawable resource ID - * for the network badge of a given cryptocurrency. For coins, this will typically - * return null as they do not have network badges, while tokens will fetch the icon - * based on their associated network ID. - * - * @return Drawable resource ID for the network badge or null if the cryptocurrency is a coin. + * @return Drawable resource ID for the network. */ @get:DrawableRes -val CryptoCurrency.networkBadgeIconResId: Int? - get() = when (this) { - is CryptoCurrency.Coin -> null - is CryptoCurrency.Token -> getActiveIconRes(network.id.value) +val CryptoCurrency.networkIconResId: Int + get() = getActiveIconRes(network.id.value) + +/** + * Tries to extract a background color from the contract address of a token. + * + * @param fallbackColor The color to use as a fallback. + * @return The extracted background color or the fallback color if extraction fails or if it is a test network token. + */ +fun CryptoCurrency.Token.tryGetBackgroundForTokenIcon(fallbackColor: Color = TangemColorPalette.Black): Color { + if (network.isTestnet) return fallbackColor + + return try { + val colorHex = "#" + contractAddress.substring(range = COLOR_HEX_START_INDEX..COLOR_HEX_END_INDEX) + Color(colorHex.toColorInt()) + } catch (exception: Exception) { + fallbackColor } +} /** - * Retrieves the resource ID for the icon of a [CryptoCurrency]. + * Determines the tint color to be used for a token icon based on its background color. + * If the icon's background color is light, a dark tint is chosen; otherwise, a light tint is chosen. * - * This property provides a way to fetch the appropriate drawable resource ID - * for the icon of a given cryptocurrency. - * - * @return Drawable resource ID for the cryptocurrency icon. + * @param iconBackground The background color of the custom token icon. + * @return The tint color to be used for the icon. */ -@get:DrawableRes -val CryptoCurrency.iconResId: Int - get() = when (this) { - is CryptoCurrency.Coin -> { - val rawCoinId = id.rawCurrencyId - - if (rawCoinId != null) { - getActiveIconResByCoinId(rawCoinId, network.id.value) - } else { - R.drawable.ic_alert_24 - } - } - is CryptoCurrency.Token -> R.drawable.ic_alert_24 - } \ No newline at end of file +fun getTintForTokenIcon(iconBackground: Color): Color { + return if (iconBackground.luminance() > LIGHT_LUMINANCE) TangemColorPalette.Black else TangemColorPalette.White +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt index 139dc810c6..efe05116e7 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt @@ -88,6 +88,7 @@ private fun materialThemeColors(colors: TangemColors, isDark: Boolean): Colors { } @Composable +@ReadOnlyComposable private fun lightThemeColors(): TangemColors { return TangemColors( text = TangemColors.Text( @@ -135,6 +136,7 @@ private fun lightThemeColors(): TangemColors { } @Composable +@ReadOnlyComposable private fun darkThemeColors(): TangemColors { return TangemColors( text = TangemColors.Text( diff --git a/core/ui/src/main/res/drawable/ic_custom_token_44.xml b/core/ui/src/main/res/drawable/ic_custom_token_44.xml new file mode 100644 index 0000000000..b1d31ab0bd --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_custom_token_44.xml @@ -0,0 +1,10 @@ + + + + diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt index 0bb96c3d93..29e967a070 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt @@ -3,14 +3,10 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.iconResId +import com.tangem.core.ui.extensions.networkIconResId import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.tokens.models.CryptoCurrency import com.tangem.feature.tokendetails.presentation.tokendetails.state.* -import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState -import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState -import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarConfig -import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenInfoBlockState import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsSkeletonStateConverter.SkeletonModel import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents @@ -38,7 +34,7 @@ internal class TokenDetailsSkeletonStateConverter( is CryptoCurrency.Token -> TokenInfoBlockState.Currency.Token( networkName = currency.network.standardType.name, blockchainName = currency.network.name, - networkIcon = currency.iconResId, + networkIcon = currency.networkIconResId, ) }, ), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt index 60f3ec11ff..de4226ceca 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt @@ -8,13 +8,17 @@ import com.tangem.core.ui.components.transactions.state.TransactionState import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.event.consumed import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.res.TangemColorPalette import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.wallet.presentation.common.state.TokenItemState import com.tangem.feature.wallet.presentation.common.state.TokenItemState.TokenOptionsState import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState -import com.tangem.feature.wallet.presentation.wallet.state.* +import com.tangem.feature.wallet.presentation.wallet.state.ActionsBottomSheetConfig +import com.tangem.feature.wallet.presentation.wallet.state.TokenActionButtonConfig +import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState +import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState import com.tangem.feature.wallet.presentation.wallet.state.components.* import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState.TokensListItemState import kotlinx.collections.immutable.persistentListOf @@ -87,12 +91,34 @@ internal object WalletPreviewData { ) } + private val coinIconState + get() = TokenItemState.IconState.CoinIcon( + url = null, + fallbackResId = R.drawable.img_polygon_22, + isGrayscale = false, + ) + + private val tokenIconState + get() = TokenItemState.IconState.TokenIcon( + url = null, + networkBadgeIconResId = R.drawable.img_polygon_22, + fallbackTint = TangemColorPalette.Black, + fallbackBackground = TangemColorPalette.Meadow, + isGrayscale = false, + ) + + private val customTokenIconState + get() = TokenItemState.IconState.CustomTokenIcon( + tint = TangemColorPalette.Black, + background = TangemColorPalette.Meadow, + networkBadgeIconResId = R.drawable.img_polygon_22, + isGrayscale = false, + ) + val tokenItemVisibleState by lazy { TokenItemState.Content( id = UUID.randomUUID().toString(), - tokenIconUrl = null, - tokenIconResId = R.drawable.img_polygon_22, - networkBadgeIconResId = R.drawable.img_polygon_22, + icon = coinIconState, name = "Polygon", amount = "5,412 MATIC", hasPending = true, @@ -103,7 +129,6 @@ internal object WalletPreviewData { type = PriceChangeConfig.Type.UP, ), ), - isTestnet = false, onItemClick = {}, onItemLongClick = {}, ) @@ -112,16 +137,14 @@ internal object WalletPreviewData { val testnetTokenItemVisibleState by lazy { tokenItemVisibleState.copy( name = "Polygon testnet", - isTestnet = true, + icon = tokenIconState.copy(isGrayscale = true), ) } val tokenItemHiddenState by lazy { TokenItemState.Content( id = UUID.randomUUID().toString(), - tokenIconUrl = null, - tokenIconResId = R.drawable.img_polygon_22, - networkBadgeIconResId = R.drawable.img_polygon_22, + icon = tokenIconState, name = "Polygon", amount = "5,412 MATIC", hasPending = true, @@ -131,7 +154,6 @@ internal object WalletPreviewData { type = PriceChangeConfig.Type.UP, ), ), - isTestnet = false, onItemClick = {}, onItemLongClick = {}, ) @@ -140,11 +162,8 @@ internal object WalletPreviewData { val tokenItemDragState by lazy { TokenItemState.Draggable( id = UUID.randomUUID().toString(), - tokenIconUrl = null, - tokenIconResId = R.drawable.img_polygon_22, - networkBadgeIconResId = R.drawable.img_polygon_22, + icon = tokenIconState, name = "Polygon", - isTestnet = false, fiatAmount = "3 172,14 $", ) } @@ -152,13 +171,28 @@ internal object WalletPreviewData { val tokenItemUnreachableState by lazy { TokenItemState.Unreachable( id = UUID.randomUUID().toString(), - tokenIconUrl = null, - tokenIconResId = R.drawable.img_polygon_22, - networkBadgeIconResId = R.drawable.img_polygon_22, + icon = tokenIconState, name = "Polygon", ) } + val customTokenItemVisibleState by lazy { + tokenItemVisibleState.copy( + name = "Polygon custom", + icon = customTokenIconState.copy( + tint = TangemColorPalette.White, + background = TangemColorPalette.Black, + ), + ) + } + + val customTestnetTokenItemVisibleState by lazy { + tokenItemVisibleState.copy( + name = "Polygon custom testnet", + icon = customTokenIconState.copy(isGrayscale = true), + ) + } + val loadingTokenItemState by lazy { TokenItemState.Loading(id = "Loading#1") } private const val networksSize = 10 @@ -188,7 +222,6 @@ internal object WalletPreviewData { tokenItemState = tokenItemDragState.copy( id = "${group.id}_token_$tokenNumber", name = "Token $tokenNumber from $networkNumber network", - networkBadgeIconResId = R.drawable.img_eth_22.takeIf { i != 0 }, ), groupId = group.id, roundingMode = when { @@ -299,8 +332,6 @@ internal object WalletPreviewData { tokenItemVisibleState.copy( id = "token_1", name = "Ethereum", - tokenIconResId = R.drawable.img_eth_22, - networkBadgeIconResId = null, amount = "1,89340821 ETH", ), ), @@ -308,8 +339,6 @@ internal object WalletPreviewData { tokenItemVisibleState.copy( id = "token_2", name = "Ethereum", - tokenIconResId = R.drawable.img_eth_22, - networkBadgeIconResId = null, amount = "1,89340821 ETH", ), ), @@ -317,8 +346,6 @@ internal object WalletPreviewData { tokenItemVisibleState.copy( id = "token_3", name = "Ethereum", - tokenIconResId = R.drawable.img_eth_22, - networkBadgeIconResId = null, amount = "1,89340821 ETH", ), ), @@ -326,8 +353,6 @@ internal object WalletPreviewData { tokenItemVisibleState.copy( id = "token_4", name = "Ethereum", - tokenIconResId = R.drawable.img_eth_22, - networkBadgeIconResId = null, amount = "1,89340821 ETH", ), ), @@ -336,8 +361,6 @@ internal object WalletPreviewData { tokenItemVisibleState.copy( id = "token_5", name = "Ethereum", - tokenIconResId = R.drawable.img_eth_22, - networkBadgeIconResId = null, amount = "1,89340821 ETH", ), ), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt index 6169aa9a8c..18cc14cbe1 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt @@ -17,16 +17,14 @@ import androidx.constraintlayout.compose.ConstrainedLayoutReference import androidx.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.ConstraintLayoutScope import androidx.constraintlayout.compose.Dimension -import com.tangem.core.ui.components.* import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.wallet.presentation.common.WalletPreviewData import com.tangem.feature.wallet.presentation.common.component.token.TokenCryptoInfoBlock import com.tangem.feature.wallet.presentation.common.component.token.TokenFiatInfoBlock -import com.tangem.feature.wallet.presentation.common.component.token.TokenIcon +import com.tangem.feature.wallet.presentation.common.component.token.icon.TokenIcon import com.tangem.feature.wallet.presentation.common.state.TokenItemState import org.burnoutcrew.reorderable.ReorderableLazyListState -// TODO: Add custom token state: [REDACTED_JIRA] @OptIn(ExperimentalFoundationApi::class) @Composable internal fun TokenItem( @@ -90,7 +88,7 @@ private inline fun BaseContainer( ) { ConstraintLayout( modifier = Modifier - .fillMaxSize() + .fillMaxWidth() .padding( horizontal = TangemTheme.dimens.spacing14, vertical = TangemTheme.dimens.spacing14, @@ -111,7 +109,6 @@ private fun Modifier.constrainAsOptionsItem(scope: ConstraintLayoutScope, ref: C } // region preview - @Preview @Composable private fun Preview_Tokens_LightTheme(@PreviewParameter(TokenConfigProvider::class) state: TokenItemState) { @@ -136,6 +133,8 @@ private class TokenConfigProvider : CollectionPreviewParameterProvider ContentIcon(content = state, modifier = modifier) - is TokenItemState.Loading -> LoadingIcon(modifier = modifier) - is TokenItemState.Locked -> LockedIcon(modifier = modifier) - } -} - -@Composable -private fun ContentIcon(content: TokenItemState.ContentState, modifier: Modifier = Modifier) { - BaseContainer(modifier = modifier) { - val isTestnet = when (content) { - is TokenItemState.Content -> content.isTestnet - is TokenItemState.Draggable -> content.isTestnet - is TokenItemState.Unreachable -> false - } - - val colorFilter = remember(isTestnet) { - if (isTestnet) { - ColorFilter.colorMatrix( - colorMatrix = ColorMatrix().apply { setToSaturation(GRAY_SCALE_SATURATION) }, - ) - } else { - null - } - } - - Icon( - content = content, - colorFilter = colorFilter, - modifier = Modifier.align(Alignment.BottomStart), - ) - - NetworkBadge( - iconResId = content.networkBadgeIconResId, - colorFilter = colorFilter, - modifier = Modifier.align(Alignment.TopEnd), - ) - } -} - -@Composable -private fun Icon(content: TokenItemState.ContentState, colorFilter: ColorFilter?, modifier: Modifier = Modifier) { - val iconUrl = content.tokenIconUrl - val iconData: Any = remember(iconUrl) { - if (iconUrl.isNullOrEmpty()) content.tokenIconResId else iconUrl - } - - SubcomposeAsyncImage( - modifier = modifier.iconSize(), - model = ImageRequest.Builder(context = LocalContext.current) - .data(data = iconData) - .placeholder(drawableResId = content.tokenIconResId) - .error(drawableResId = content.tokenIconResId) - .fallback(drawableResId = content.tokenIconResId) - .crossfade(enable = true) - .build(), - colorFilter = colorFilter, - contentDescription = null, - ) -} - -@Composable -private fun BoxScope.NetworkBadge( - @DrawableRes iconResId: Int?, - colorFilter: ColorFilter?, - modifier: Modifier = Modifier, -) { - AnimatedVisibility( - visible = iconResId != null, - modifier = modifier - .size(TangemTheme.dimens.size18) - .background(color = TangemTheme.colors.background.primary, shape = CircleShape), - ) { - if (iconResId == null) return@AnimatedVisibility - - Image( - modifier = Modifier - .padding(all = TangemTheme.dimens.spacing2) - .align(Alignment.Center), - painter = painterResource(id = iconResId), - colorFilter = colorFilter, - contentDescription = null, - ) - } -} - -@Composable -private fun LoadingIcon(modifier: Modifier = Modifier) { - BaseContainer(modifier) { - CircleShimmer( - modifier = Modifier - .iconSize() - .align(alignment = Alignment.BottomStart), - ) - } -} - -@Composable -private fun LockedIcon(modifier: Modifier = Modifier) { - BaseContainer(modifier) { - Box( - modifier = Modifier - .iconSize() - .align(Alignment.BottomStart), - ) { - Box( - modifier = Modifier - .matchParentSize() - .background(color = TangemTheme.colors.background.secondary, shape = CircleShape), - ) - } - } -} - -@Composable -private inline fun BaseContainer(modifier: Modifier = Modifier, content: @Composable BoxScope.() -> Unit) { - Box(modifier = modifier.size(size = TangemTheme.dimens.size40), content = content) -} - -private fun Modifier.iconSize(): Modifier = composed { - return@composed this.size(size = TangemTheme.dimens.size36) -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/ContentIcon.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/ContentIcon.kt new file mode 100644 index 0000000000..c51cb1e957 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/ContentIcon.kt @@ -0,0 +1,129 @@ +package com.tangem.feature.wallet.presentation.common.component.token.icon + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import coil.compose.SubcomposeAsyncImage +import coil.request.ImageRequest +import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.common.state.TokenItemState + +@Composable +internal fun ContentIcon(icon: TokenItemState.IconState, colorFilter: ColorFilter?, modifier: Modifier = Modifier) { + when (icon) { + is TokenItemState.IconState.CoinIcon -> CoinIcon( + modifier = modifier, + url = icon.url, + fallbackResId = icon.fallbackResId, + colorFilter = colorFilter, + ) + is TokenItemState.IconState.TokenIcon -> TokenIcon( + modifier = modifier, + url = icon.url, + colorFilter = colorFilter, + errorIcon = { + CustomTokenIcon( + modifier = modifier, + tint = icon.fallbackTint, + background = icon.fallbackBackground, + ) + }, + ) + is TokenItemState.IconState.CustomTokenIcon -> CustomTokenIcon( + modifier = modifier, + tint = icon.tint, + background = icon.background, + ) + } +} + +@Composable +private fun CoinIcon( + url: String?, + @DrawableRes fallbackResId: Int, + colorFilter: ColorFilter?, + modifier: Modifier = Modifier, +) { + val iconData: Any = if (url.isNullOrBlank()) fallbackResId else url + + DefaultCurrencyIcon( + modifier = modifier, + iconData = iconData, + errorIcon = { + Image( + painter = painterResource(id = fallbackResId), + colorFilter = colorFilter, + contentDescription = null, + ) + }, + colorFilter = colorFilter, + ) +} + +@Composable +private fun TokenIcon( + url: String?, + colorFilter: ColorFilter?, + errorIcon: @Composable () -> Unit, + modifier: Modifier = Modifier, +) { + if (url == null) { + errorIcon() + } else { + DefaultCurrencyIcon( + modifier = modifier, + iconData = url, + errorIcon = errorIcon, + colorFilter = colorFilter, + ) + } +} + +@Composable +private fun CustomTokenIcon(tint: Color, background: Color, modifier: Modifier = Modifier) { + Box( + modifier = modifier + .background( + color = background, + shape = CircleShape, + ), + contentAlignment = Alignment.Center, + ) { + Icon( + modifier = Modifier.matchParentSize(), + painter = painterResource(id = R.drawable.ic_custom_token_44), + tint = tint, + contentDescription = null, + ) + } +} + +@Composable +private inline fun DefaultCurrencyIcon( + iconData: Any, + colorFilter: ColorFilter?, + crossinline errorIcon: @Composable () -> Unit, + modifier: Modifier = Modifier, +) { + SubcomposeAsyncImage( + modifier = modifier, + model = ImageRequest.Builder(context = LocalContext.current) + .data(iconData) + .crossfade(enable = true) + .build(), + loading = { LoadingIcon() }, + error = { errorIcon() }, + colorFilter = colorFilter, + contentDescription = null, + ) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/IconBadge.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/IconBadge.kt new file mode 100644 index 0000000000..8bc2dbb218 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/IconBadge.kt @@ -0,0 +1,57 @@ +package com.tangem.feature.wallet.presentation.common.component.token.icon + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.res.painterResource +import com.tangem.core.ui.res.TangemTheme + +@Composable +internal fun NetworkBadge(@DrawableRes iconResId: Int, colorFilter: ColorFilter?, modifier: Modifier = Modifier) { + Box( + modifier = modifier + .size(TangemTheme.dimens.size18) + .background( + color = TangemTheme.colors.background.primary, + shape = CircleShape, + ), + ) { + Image( + modifier = Modifier + .padding(all = TangemTheme.dimens.spacing2) + .matchParentSize(), + painter = painterResource(id = iconResId), + colorFilter = colorFilter, + contentDescription = null, + ) + } +} + +@Composable +internal fun CustomBadge(modifier: Modifier = Modifier) { + Box( + modifier = modifier + .size(TangemTheme.dimens.size12) + .background( + color = TangemTheme.colors.background.primary, + shape = CircleShape, + ), + ) { + Box( + modifier = Modifier + .padding(all = TangemTheme.dimens.spacing2) + .matchParentSize() + .background( + color = TangemTheme.colors.icon.informative, + shape = CircleShape, + ), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/TokenIcon.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/TokenIcon.kt new file mode 100644 index 0000000000..c227566471 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/TokenIcon.kt @@ -0,0 +1,95 @@ +package com.tangem.feature.wallet.presentation.common.component.token.icon + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.ColorMatrix +import com.tangem.core.ui.components.CircleShimmer +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.wallet.presentation.common.state.TokenItemState + +private const val GRAY_SCALE_SATURATION = 0f + +@Composable +internal fun TokenIcon(state: TokenItemState, modifier: Modifier = Modifier) { + BaseContainer(modifier = modifier) { + val iconModifier = Modifier + .align(Alignment.Center) + .size(TangemTheme.dimens.size36) + + when (state) { + is TokenItemState.Loading -> LoadingIcon(modifier = iconModifier) + is TokenItemState.Locked -> LockedIcon(modifier = iconModifier) + is TokenItemState.ContentState -> ContentIconContainer( + modifier = iconModifier, + icon = state.icon, + ) + } + } +} + +@Composable +internal fun LoadingIcon(modifier: Modifier = Modifier) { + CircleShimmer(modifier = modifier) +} + +@Composable +private fun LockedIcon(modifier: Modifier = Modifier) { + Box(modifier = modifier) { + Box( + modifier = Modifier + .matchParentSize() + .background( + color = TangemTheme.colors.background.secondary, + shape = CircleShape, + ), + ) + } +} + +@Composable +private fun BoxScope.ContentIconContainer(icon: TokenItemState.IconState, modifier: Modifier = Modifier) { + val networkBadgeOffset = TangemTheme.dimens.spacing4 + val colorFilter = remember(icon.isGrayscale) { + if (icon.isGrayscale) { + ColorFilter.colorMatrix( + colorMatrix = ColorMatrix().apply { setToSaturation(GRAY_SCALE_SATURATION) }, + ) + } else { + null + } + } + + ContentIcon( + modifier = modifier, + icon = icon, + colorFilter = colorFilter, + ) + + if (icon.networkBadgeIconResId != null) { + NetworkBadge( + modifier = Modifier + .offset(x = networkBadgeOffset, y = -networkBadgeOffset) + .align(Alignment.TopEnd), + iconResId = requireNotNull(icon.networkBadgeIconResId), + colorFilter = colorFilter, + ) + } + + if (icon is TokenItemState.IconState.CustomTokenIcon) { + CustomBadge(modifier = Modifier.align(Alignment.BottomEnd)) + } +} + +@Composable +private inline fun BaseContainer(modifier: Modifier = Modifier, content: @Composable BoxScope.() -> Unit) { + Box(modifier = modifier.size(size = TangemTheme.dimens.size40), content = content) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt index 8310f134d5..16e58f8a87 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt @@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.common.state import androidx.annotation.DrawableRes import androidx.compose.runtime.Immutable +import androidx.compose.ui.graphics.Color import com.tangem.core.ui.components.marketprice.PriceChangeConfig /** Token item state */ @@ -18,80 +19,120 @@ internal sealed interface TokenItemState { data class Locked(override val id: String) : TokenItemState /** Content state */ - sealed class ContentState( - override val id: String, - open val tokenIconUrl: String?, - @DrawableRes open val tokenIconResId: Int, - @DrawableRes open val networkBadgeIconResId: Int?, - open val name: String, - ) : TokenItemState + sealed class ContentState : TokenItemState { + + abstract val icon: IconState + abstract val name: String + } /** * Content token state * * @property id unique id - * @property tokenIconUrl token icon url - * @property tokenIconResId token icon resource id - * @property networkBadgeIconResId network badge icon resource id, may be null if it is a coin + * @property icon token icon state * @property name token name * @property amount amount of token * @property hasPending pending tx in blockchain * @property tokenOptions state for token options - * @property isTestnet indicates whether the token is from test network or not * @property onItemClick callback which will be called when an item is clicked * @property onItemLongClick callback which will be called when an item is long clicked */ data class Content( override val id: String, - override val tokenIconUrl: String?, - @DrawableRes override val tokenIconResId: Int, - @DrawableRes override val networkBadgeIconResId: Int?, + override val icon: IconState, override val name: String, val amount: String, val hasPending: Boolean, val tokenOptions: TokenOptionsState, - val isTestnet: Boolean, val onItemClick: () -> Unit, val onItemLongClick: () -> Unit, - ) : ContentState(id, tokenIconUrl, tokenIconResId, networkBadgeIconResId, name) + ) : ContentState() /** * Draggable token state * * @property id unique id - * @property tokenIconUrl token icon url - * @property tokenIconResId token icon resource id - * @property networkBadgeIconResId network badge icon resource id, may be null if it is a coin + * @property icon token icon state * @property name token name * @property fiatAmount fiat amount of token - * @property isTestnet indicates whether the token is from test network or not */ data class Draggable( override val id: String, - override val tokenIconUrl: String?, - @DrawableRes override val tokenIconResId: Int, - @DrawableRes override val networkBadgeIconResId: Int?, + override val icon: IconState, override val name: String, val fiatAmount: String, - val isTestnet: Boolean, - ) : ContentState(id, tokenIconUrl, tokenIconResId, networkBadgeIconResId, name) + ) : ContentState() /** * Unreachable token state * * @property id token id - * @property tokenIconUrl token icon url - * @property tokenIconResId token icon resource id - * @property networkBadgeIconResId network badge icon resource id, may be null if it is a coin + * @property icon token icon state * @property name token name */ data class Unreachable( override val id: String, - override val tokenIconUrl: String?, - @DrawableRes override val tokenIconResId: Int, - @DrawableRes override val networkBadgeIconResId: Int?, + override val icon: IconState, override val name: String, - ) : ContentState(id, tokenIconUrl, tokenIconResId, networkBadgeIconResId, name) + ) : ContentState() + + /** + * Represents the various states an icon can be in. + */ + @Immutable + sealed class IconState { + + abstract val networkBadgeIconResId: Int? + abstract val isGrayscale: Boolean + + /** + * Represents a coin icon. + * + * @property url The URL where the coin icon can be fetched from. May be `null` if not found. + * @property fallbackResId The drawable resource ID to be used as a fallback if the URL is not available. + * @property isGrayscale Specifies whether to show the icon in grayscale. + */ + data class CoinIcon( + val url: String?, + @DrawableRes val fallbackResId: Int, + override val isGrayscale: Boolean, + ) : IconState() { + + override val networkBadgeIconResId: Int? = null + } + + /** + * Represents a token icon. + * + * @property url The URL where the token icon can be fetched from. May be `null` if not found. + * @property networkBadgeIconResId The drawable resource ID for the network badge. + * @property isGrayscale Specifies whether to show the icon in grayscale. + * @property fallbackTint The color to be used for tinting the fallback icon. + * @property fallbackBackground The background color to be used for the fallback icon. + */ + data class TokenIcon( + val url: String?, + @DrawableRes override val networkBadgeIconResId: Int, + override val isGrayscale: Boolean, + val fallbackTint: Color, + val fallbackBackground: Color, + ) : IconState() + + /** + * Represents a custom token icon. + * + * @property tint The color to be used for tinting the icon. + * @property background The background color to be used for the icon. + * @property networkBadgeIconResId The drawable resource ID for the network badge. + * @property isGrayscale Specifies whether to show the icon in grayscale. + */ + data class CustomTokenIcon( + val tint: Color, + val background: Color, + @DrawableRes override val networkBadgeIconResId: Int, + override val isGrayscale: Boolean, + ) : IconState() + } /** Token options state */ @Immutable diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/utils/CryptoCurrencyToIconStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/utils/CryptoCurrencyToIconStateConverter.kt new file mode 100644 index 0000000000..6f8c7c9b05 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/utils/CryptoCurrencyToIconStateConverter.kt @@ -0,0 +1,48 @@ +package com.tangem.feature.wallet.presentation.common.utils + +import com.tangem.core.ui.extensions.getTintForTokenIcon +import com.tangem.core.ui.extensions.networkIconResId +import com.tangem.core.ui.extensions.tryGetBackgroundForTokenIcon +import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.feature.wallet.presentation.common.state.TokenItemState +import com.tangem.utils.converter.Converter + +internal class CryptoCurrencyToIconStateConverter : Converter { + + override fun convert(value: CryptoCurrency): TokenItemState.IconState { + return when (value) { + is CryptoCurrency.Coin -> getIconStateForCoin(value) + is CryptoCurrency.Token -> getIconStateForToken(value) + } + } + + private fun getIconStateForCoin(coin: CryptoCurrency.Coin): TokenItemState.IconState.CoinIcon { + return TokenItemState.IconState.CoinIcon( + url = coin.iconUrl, + fallbackResId = coin.networkIconResId, + isGrayscale = coin.network.isTestnet, + ) + } + + private fun getIconStateForToken(token: CryptoCurrency.Token): TokenItemState.IconState { + val background = token.tryGetBackgroundForTokenIcon() + val tint = getTintForTokenIcon(background) + + return if (token.isCustom) { + TokenItemState.IconState.CustomTokenIcon( + tint = tint, + background = background, + networkBadgeIconResId = token.networkIconResId, + isGrayscale = token.network.isTestnet, + ) + } else { + TokenItemState.IconState.TokenIcon( + url = token.iconUrl, + networkBadgeIconResId = token.networkIconResId, + isGrayscale = token.network.isTestnet, + fallbackTint = tint, + fallbackBackground = background, + ) + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt index 238ff49a18..4cd2ee53d0 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt @@ -1,7 +1,9 @@ package com.tangem.feature.wallet.presentation.organizetokens import androidx.activity.compose.BackHandler -import androidx.compose.animation.core.* +import androidx.compose.animation.core.LinearOutSlowInEasing +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.tween import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.layout.* @@ -37,7 +39,10 @@ import com.tangem.feature.wallet.presentation.common.component.TokenItem import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState -import org.burnoutcrew.reorderable.* +import org.burnoutcrew.reorderable.ReorderableItem +import org.burnoutcrew.reorderable.ReorderableLazyListState +import org.burnoutcrew.reorderable.rememberReorderableLazyListState +import org.burnoutcrew.reorderable.reorderable @Composable internal fun OrganizeTokensScreen(state: OrganizeTokensState, modifier: Modifier = Modifier) { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt index 11f4747c2b..9bad7941b4 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt @@ -1,12 +1,11 @@ package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items import com.tangem.common.Provider -import com.tangem.core.ui.extensions.iconResId -import com.tangem.core.ui.extensions.networkBadgeIconResId import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.wallet.presentation.common.state.TokenItemState +import com.tangem.feature.wallet.presentation.common.utils.CryptoCurrencyToIconStateConverter import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getGroupHeaderId import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getTokenItemId @@ -16,6 +15,8 @@ internal class CryptoCurrencyToDraggableItemConverter( private val appCurrencyProvider: Provider, ) : Converter { + private val iconStateConverter = CryptoCurrencyToIconStateConverter() + override fun convert(value: CryptoCurrencyStatus): DraggableItem.Token { return createDraggableToken(value, appCurrencyProvider()) } @@ -44,12 +45,9 @@ internal class CryptoCurrencyToDraggableItemConverter( return TokenItemState.Draggable( id = getTokenItemId(currency.id), - tokenIconUrl = currency.iconUrl, - tokenIconResId = currencyStatus.currency.iconResId, - networkBadgeIconResId = currencyStatus.currency.networkBadgeIconResId, + icon = iconStateConverter.convert(currency), name = currency.name, fiatAmount = getFormattedFiatAmount(currencyStatus, appCurrency), - isTestnet = currencyStatus.currency.network.isTestnet, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt index b19dcf646a..02b175aad7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt @@ -2,12 +2,11 @@ package com.tangem.feature.wallet.presentation.wallet.utils import com.tangem.common.Provider import com.tangem.core.ui.components.marketprice.PriceChangeConfig -import com.tangem.core.ui.extensions.iconResId -import com.tangem.core.ui.extensions.networkBadgeIconResId import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.wallet.presentation.common.state.TokenItemState +import com.tangem.feature.wallet.presentation.common.utils.CryptoCurrencyToIconStateConverter import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents import com.tangem.utils.converter.Converter import java.math.BigDecimal @@ -18,6 +17,8 @@ internal class CryptoCurrencyStatusToTokenItemConverter( private val clickIntents: WalletClickIntents, ) : Converter { + private val iconStateConverter = CryptoCurrencyToIconStateConverter() + override fun convert(value: CryptoCurrencyStatus): TokenItemState { return when (value.value) { is CryptoCurrencyStatus.Loading -> TokenItemState.Loading(id = value.currency.id.value) @@ -37,9 +38,7 @@ internal class CryptoCurrencyStatusToTokenItemConverter( return TokenItemState.Content( id = currency.id.value, name = currency.name, - tokenIconUrl = currency.iconUrl, - tokenIconResId = currency.iconResId, - networkBadgeIconResId = currency.networkBadgeIconResId, + icon = iconStateConverter.convert(currency), amount = getFormattedAmount(), hasPending = value.hasCurrentNetworkTransactions, tokenOptions = if (isWalletContentHidden) { @@ -50,7 +49,6 @@ internal class CryptoCurrencyStatusToTokenItemConverter( config = getPriceChangeConfig(), ) }, - isTestnet = currency.network.isTestnet, onItemClick = { clickIntents.onTokenItemClick(currency) }, onItemLongClick = { clickIntents.onTokenItemLongClick(currency) }, ) @@ -72,9 +70,7 @@ internal class CryptoCurrencyStatusToTokenItemConverter( private fun CryptoCurrencyStatus.mapToUnreachableTokenItemState() = TokenItemState.Unreachable( id = currency.id.value, name = currency.name, - tokenIconUrl = currency.iconUrl, - tokenIconResId = currency.iconResId, - networkBadgeIconResId = currency.networkBadgeIconResId, + icon = iconStateConverter.convert(currency), ) private fun CryptoCurrencyStatus.getPriceChangeConfig(): PriceChangeConfig {