diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/opportunities/OpportunitiesBG.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/opportunities/OpportunitiesBG.kt new file mode 100644 index 0000000000..f0059fff8b --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/opportunities/OpportunitiesBG.kt @@ -0,0 +1,244 @@ +package com.tangem.core.ui.ds.opportunities + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.draw.innerShadow +import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.drawOutline +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.shadow.Shadow +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import coil.request.ImageRequest +import com.tangem.core.ui.R +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.components.haze.hazeForegroundEffectTangem +import com.tangem.core.ui.ds.image.TangemIconUM +import com.tangem.core.ui.res.LocalIsInDarkTheme +import com.tangem.core.ui.res.TangemThemePreview +import dev.chrisbanes.haze.HazeStyle + +/** + * Container that draws a blurred background (from URL or solid color) and + * applies a semi‑transparent overlay on top of it, then renders foreground content. + * + * Figma https://www.figma.com/design/X0IMgSMOT5rWWgiSIeZQwC/Bottom-sheet--Redesign-?node-id=3360-64755&m=dev + * + * @param icon Background configuration (URL, solid color or none). + * @param modifier Modifier applied to the outer container. + * @param content Foreground content rendered on top of the overlay. + * @param shape Shape used for inner shadow and border (e.g. rounded corners). + */ +@Suppress("MagicNumber") +@Composable +fun OpportunitiesBG( + icon: TangemIconUM, + modifier: Modifier = Modifier, + shape: Shape = RoundedCornerShape(16.dp), + content: @Composable BoxScope.() -> Unit, +) { + val isInDarkTheme = LocalIsInDarkTheme.current + val overlayColor = remember(isInDarkTheme) { + if (isInDarkTheme) { + Color(OVERLAY_DARK) + } else { + Color.White + } + } + + Box(modifier = modifier) { + BackgroundLayer(icon = icon) + + Box( + modifier = Modifier + .fillMaxWidth() + .clip(shape) + .innerShadow( + shape = shape, + shadow = Shadow( + radius = 30.dp, + spread = 5.dp, + color = Color(INNER_SHADOW_COLOR_START).copy(alpha = .3f), + offset = DpOffset(0.dp, 0.dp), + ), + ) + .innerShadow( + shape = shape, + shadow = Shadow( + radius = 100.dp, + spread = (-39).dp, + color = Color(INNER_SHADOW_COLOR_END).copy(.3f), + offset = DpOffset(0.dp, (-56).dp), + ), + ) + .innerShadow( + shape = shape, + shadow = Shadow( + radius = 40.dp, + spread = (-19).dp, + color = Color(INNER_SHADOW_COLOR_END).copy(alpha = .25f), + offset = DpOffset(0.dp, (-16).dp), + ), + ) + .drawWithContent { + drawRect(color = overlayColor.copy(alpha = .7f)) + drawContent() + val outline = shape.createOutline(size, layoutDirection, this) + drawOutline(outline, Color(BORDER_COLOR).copy(alpha = .1f), style = Stroke(width = 1.dp.toPx())) + }, + content = content, + ) + } +} + +@Suppress("CyclomaticComplexMethod") +@Composable +private fun BoxScope.BackgroundLayer(icon: TangemIconUM, blurRadius: Dp = 26.dp) { + when (icon) { + is TangemIconUM.Currency -> CurrencyIconBackgroundLayer(icon.currencyIconState, blurRadius) + is TangemIconUM.Icon -> SolidColorBackground(icon.tintReference(), blurRadius) + is TangemIconUM.Ident -> Unit + is TangemIconUM.Image -> ResBackground(icon.imageRes, blurRadius) + } +} + +@Suppress("CyclomaticComplexMethod") +@Composable +private fun BoxScope.CurrencyIconBackgroundLayer(state: CurrencyIconState, blurRadius: Dp) { + when (state) { + is CurrencyIconState.CryptoPortfolio.Icon -> SolidColorBackground( + color = state.color, + blurRadius = blurRadius, + ) + is CurrencyIconState.CryptoPortfolio.Letter -> SolidColorBackground( + color = state.color, + blurRadius = blurRadius, + ) + is CurrencyIconState.CustomTokenIcon -> SolidColorBackground( + color = state.background, + blurRadius = blurRadius, + ) + is CurrencyIconState.Empty -> ResBackground(res = state.resId, blurRadius = blurRadius) + is CurrencyIconState.CoinIcon -> { + state.url?.let { + UrlBackground(imageUrl = state.url, blurRadius = blurRadius) + } ?: run { + ResBackground(res = state.fallbackResId, blurRadius = blurRadius) + } + } + is CurrencyIconState.FiatIcon -> state.url?.let { + UrlBackground(imageUrl = state.url, blurRadius = blurRadius) + } ?: run { + ResBackground(res = state.fallbackResId, blurRadius = blurRadius) + } + is CurrencyIconState.TokenIcon -> state.url?.let { + UrlBackground(imageUrl = state.url, blurRadius = blurRadius) + } ?: run { + SolidColorBackground( + color = state.fallbackBackground, + blurRadius = blurRadius, + ) + } + CurrencyIconState.Loading -> Unit + CurrencyIconState.Locked -> Unit + } +} + +@Composable +private fun BoxScope.UrlBackground(imageUrl: String?, blurRadius: Dp) { + val context = LocalContext.current + + val imageRequest = remember(imageUrl) { + if (imageUrl.isNullOrBlank()) { + null + } else { + ImageRequest.Builder(context) + .data(imageUrl) + .crossfade(true) + .build() + } + } + + if (imageRequest != null) { + AsyncImage( + model = imageRequest, + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .matchParentSize() + .scale(SCALE_FACTOR) + .hazeForegroundEffectTangem(style = HazeStyle(blurRadius = blurRadius, tint = null)), + ) + } +} + +@Composable +private fun BoxScope.ResBackground(res: Int, blurRadius: Dp) { + Image( + painter = painterResource(res), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .matchParentSize() + .scale(SCALE_FACTOR) + .hazeForegroundEffectTangem(style = HazeStyle(blurRadius = blurRadius, tint = null)), + ) +} + +@Composable +private fun BoxScope.SolidColorBackground(color: Color, blurRadius: Dp) { + Box( + modifier = Modifier + .matchParentSize() + .background(color = color) + .hazeForegroundEffectTangem(style = HazeStyle(blurRadius = blurRadius, tint = null)), + ) +} + +private const val SCALE_FACTOR = 1.5f +private const val INNER_SHADOW_COLOR_START = 0x00000000 +private const val INNER_SHADOW_COLOR_END = 0xFFFFFFFF + +private const val BORDER_COLOR = 0xF0F0F0 +private const val OVERLAY_DARK = 0x141414 + +// region Previews + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun OpportunitiesBGPreview() { + TangemThemePreview { + OpportunitiesBG( + modifier = Modifier.size(400.dp), + icon = TangemIconUM.Currency( + CurrencyIconState.CoinIcon( + url = null, + fallbackResId = R.drawable.img_solana_22, + isGrayscale = false, + shouldShowCustomBadge = false, + ), + ), + content = {}, + ) + } +} + +// endregion \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/EarnContent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/EarnContent.kt index 4939a58ede..fb0a94723b 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/EarnContent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/EarnContent.kt @@ -3,7 +3,6 @@ package com.tangem.features.feed.ui.earn import android.content.res.Configuration import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.* import androidx.compose.material3.Icon @@ -14,21 +13,21 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.onFirstVisible import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.core.ui.R -import com.tangem.core.ui.components.* +import com.tangem.core.ui.components.SmallButtonShimmer +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.SpacerWMax +import com.tangem.core.ui.components.UnableToLoadData import com.tangem.core.ui.components.buttons.SecondarySmallButton import com.tangem.core.ui.components.buttons.SmallButtonConfig import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition -import com.tangem.core.ui.components.currency.icon.CurrencyIcon import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.list.InfiniteListHandler import com.tangem.core.ui.decorations.roundedShapeItemDecoration @@ -39,6 +38,7 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.feed.ui.earn.components.EarnItemPlaceholder import com.tangem.features.feed.ui.earn.components.EarnListItem +import com.tangem.features.feed.ui.earn.components.MostlyUsedCard import com.tangem.features.feed.ui.earn.components.MostlyUsedPlaceholder import com.tangem.features.feed.ui.earn.state.* import kotlinx.collections.immutable.persistentListOf @@ -161,57 +161,6 @@ private fun MostlyUsedContent(state: EarnListUM, onScroll: () -> Unit) { } } -@Composable -private fun MostlyUsedCard(item: EarnListItemUM, onClick: () -> Unit, modifier: Modifier = Modifier) { - Column( - modifier = modifier - .width(148.dp) - .clip(TangemTheme.shapes.roundedCornersXMedium) - .background(TangemTheme.colors.background.action) - .clickable(onClick = onClick) - .padding(12.dp), - ) { - CurrencyIcon( - modifier = Modifier.size(32.dp), - state = item.currencyIconState, - shouldDisplayNetwork = true, - networkBadgeSize = 12.dp, - networkBadgeBackground = TangemTheme.colors.background.action, - ) - - SpacerH(8.dp) - - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - modifier = Modifier.weight(weight = 1f, fill = false), - text = item.tokenName.resolveReference(), - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.subtitle2, - overflow = TextOverflow.Ellipsis, - maxLines = 1, - ) - SpacerW(4.dp) - Text( - text = item.symbol.resolveReference(), - color = TangemTheme.colors.text.tertiary, - style = TangemTheme.typography.subtitle2, - maxLines = 1, - ) - } - - SpacerH(2.dp) - - Text( - text = item.earnValue.resolveReference(), - color = TangemTheme.colors.text.accent, - style = TangemTheme.typography.caption1, - maxLines = 1, - ) - } -} - @Composable private fun BestOpportunitiesFilters( state: EarnBestOpportunitiesUM, diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/MostlyUsedCard.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/MostlyUsedCard.kt new file mode 100644 index 0000000000..21fdbcc0a4 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/MostlyUsedCard.kt @@ -0,0 +1,198 @@ +package com.tangem.features.feed.ui.earn.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.R +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.SpacerW +import com.tangem.core.ui.ds.opportunities.OpportunitiesBG +import com.tangem.core.ui.components.currency.icon.CurrencyIcon +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.ds.image.TangemIconUM +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.* +import com.tangem.features.feed.ui.earn.state.EarnListItemUM + +@Composable +internal fun MostlyUsedCard(item: EarnListItemUM, onClick: () -> Unit, modifier: Modifier = Modifier) { + val isRedesignEnabled = LocalRedesignEnabled.current + + if (isRedesignEnabled) { + MostlyUsedCardV2( + modifier = modifier, + item = item, + onClick = onClick, + ) + } else { + MostlyUsedCardV1( + modifier = modifier, + item = item, + onClick = onClick, + ) + } +} + +@Composable +private fun MostlyUsedCardV2(item: EarnListItemUM, onClick: () -> Unit, modifier: Modifier = Modifier) { + OpportunitiesBG( + modifier = modifier + .width(148.dp) + .clip(TangemTheme.shapes.roundedCornersXMedium) + .clickable(onClick = onClick), + icon = TangemIconUM.Currency(item.currencyIconState), + ) { + Column(modifier = Modifier.padding(12.dp)) { + CurrencyIcon( + modifier = Modifier.size(32.dp), + state = item.currencyIconState, + shouldDisplayNetwork = true, + networkBadgeSize = 12.dp, + networkBadgeBackground = TangemTheme.colors.background.action, + ) + + SpacerH(22.dp) + + Row( + verticalAlignment = Alignment.Bottom, + ) { + Text( + modifier = Modifier.weight(weight = 1f, fill = false), + text = item.tokenName.resolveReference(), + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography2.bodySemibold16, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + SpacerW(4.dp) + Text( + text = item.symbol.resolveReference(), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography2.captionSemibold12, + maxLines = 1, + ) + } + + SpacerH(2.dp) + + Text( + text = item.earnValue.resolveReference(), + color = TangemTheme.colors2.text.status.positive, + style = TangemTheme.typography2.captionSemibold12, + maxLines = 1, + ) + } + } +} + +@Composable +private fun MostlyUsedCardV1(item: EarnListItemUM, onClick: () -> Unit, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .width(148.dp) + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action) + .clickable(onClick = onClick) + .padding(12.dp), + ) { + CurrencyIcon( + modifier = Modifier.size(32.dp), + state = item.currencyIconState, + shouldDisplayNetwork = true, + networkBadgeSize = 12.dp, + networkBadgeBackground = TangemTheme.colors.background.action, + ) + + SpacerH(8.dp) + + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + modifier = Modifier.weight(weight = 1f, fill = false), + text = item.tokenName.resolveReference(), + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.subtitle2, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + SpacerW(4.dp) + Text( + text = item.symbol.resolveReference(), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.subtitle2, + maxLines = 1, + ) + } + + SpacerH(2.dp) + + Text( + text = item.earnValue.resolveReference(), + color = TangemTheme.colors.text.accent, + style = TangemTheme.typography.caption1, + maxLines = 1, + ) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun EarnListItemPreviewV1() { + TangemThemePreview { + MostlyUsedCardV1( + EarnListItemUM( + network = stringReference("Ethereum"), + symbol = stringReference("USDT"), + tokenName = stringReference("Tether"), + currencyIconState = CurrencyIconState.TokenIcon( + url = null, + topBadgeIconResId = R.drawable.img_eth_22, + fallbackTint = TangemColorPalette.Black, + fallbackBackground = TangemColorPalette.Meadow, + isGrayscale = false, + shouldShowCustomBadge = false, + ), + earnValue = stringReference("APY 6.54%"), + earnType = stringReference("Yield"), + onItemClick = {}, + ), + onClick = {}, + ) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun EarnListItemPreviewV2() { + TangemThemePreviewRedesign { + MostlyUsedCardV2( + EarnListItemUM( + network = stringReference("Ethereum"), + symbol = stringReference("USDT"), + tokenName = stringReference("Tether"), + currencyIconState = CurrencyIconState.TokenIcon( + url = null, + topBadgeIconResId = R.drawable.img_eth_22, + fallbackTint = TangemColorPalette.Black, + fallbackBackground = TangemColorPalette.Meadow, + isGrayscale = false, + shouldShowCustomBadge = false, + ), + earnValue = stringReference("APY 6.54%"), + earnType = stringReference("Yield"), + onItemClick = {}, + ), + onClick = {}, + ) + } +} \ No newline at end of file