From 871c8348a393e02785667136b41ef712d58e834f Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 12 Mar 2026 08:51:56 +0100 Subject: [PATCH] Updated on 2026-08-14 --- .../main/res/drawable/ic_star_filled_20.xml | 10 ++ .../detailed/MarketsTokenDetailsContent.kt | 3 + .../components/InformationTextBlock.kt | 87 +++++++++++ .../detailed/components/ScoreStarsBlock.kt | 96 +++++++++++- .../detailed/components/SecurityScoreBlock.kt | 143 ++++++++++++++++-- .../components/TokenMarketDetailsBody.kt | 35 ++++- 6 files changed, 351 insertions(+), 23 deletions(-) create mode 100644 core/ui/src/main/res/drawable/ic_star_filled_20.xml create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/components/InformationTextBlock.kt diff --git a/core/ui/src/main/res/drawable/ic_star_filled_20.xml b/core/ui/src/main/res/drawable/ic_star_filled_20.xml new file mode 100644 index 0000000000..891f2755c5 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_star_filled_20.xml @@ -0,0 +1,10 @@ + + + diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/MarketsTokenDetailsContent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/MarketsTokenDetailsContent.kt index 67cc815484..eb0862ce2e 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/MarketsTokenDetailsContent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/MarketsTokenDetailsContent.kt @@ -37,6 +37,7 @@ import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.LocalRedesignEnabled import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.domain.markets.PriceChangeInterval @@ -81,6 +82,7 @@ private fun Content( modifier: Modifier = Modifier, portfolioBlock: @Composable ((Modifier) -> Unit)?, ) { + val isRedesignEnabled = LocalRedesignEnabled.current val density = LocalDensity.current val bottomBarHeight = with(density) { WindowInsets.systemBars.getBottom(this).toDp() } val lazyListState = rememberLazyListState() @@ -133,6 +135,7 @@ private fun Content( state = state.body, portfolioBlock = portfolioBlock, relatedNews = state.relatedNews, + isRedesignEnabled = isRedesignEnabled, ) } } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/components/InformationTextBlock.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/components/InformationTextBlock.kt new file mode 100644 index 0000000000..c0d6ca107e --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/components/InformationTextBlock.kt @@ -0,0 +1,87 @@ +package com.tangem.features.feed.ui.market.detailed.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.requiredSize +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +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.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextOverflow +import com.tangem.core.ui.R +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme + +@Composable +internal fun InformationTextBlock( + text: TextReference, + onInfoClick: () -> Unit, + modifier: Modifier = Modifier, + textStyle: TextStyle = TangemTheme.typography2.captionSemibold12, + textColor: Color = TangemTheme.colors2.text.neutral.secondary, + informationTextBlockIconPosition: InformationTextBlockIconPosition = InformationTextBlockIconPosition.START, +) { + val interactionSource = remember { MutableInteractionSource() } + + val infoIcon: @Composable () -> Unit = { + IconButton( + modifier = Modifier.requiredSize(TangemTheme.dimens2.x4), + interactionSource = interactionSource, + onClick = onInfoClick, + ) { + Icon( + modifier = Modifier.size(TangemTheme.dimens2.x4), + imageVector = ImageVector.vectorResource(id = R.drawable.ic_information_24), + tint = TangemTheme.colors2.markers.iconGray, + contentDescription = null, + ) + } + } + + val contentText: @Composable () -> Unit = { + Text( + text = text.resolveReference(), + style = textStyle, + color = textColor, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + + Row( + modifier = modifier + .clickable( + interactionSource = interactionSource, + indication = null, + onClick = onInfoClick, + ), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1), + ) { + when (informationTextBlockIconPosition) { + InformationTextBlockIconPosition.START -> { + infoIcon() + contentText() + } + InformationTextBlockIconPosition.END -> { + contentText() + infoIcon() + } + } + } +} + +internal enum class InformationTextBlockIconPosition { + START, END +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/components/ScoreStarsBlock.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/components/ScoreStarsBlock.kt index 6190849b99..9243c0a91a 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/components/ScoreStarsBlock.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/components/ScoreStarsBlock.kt @@ -1,11 +1,7 @@ package com.tangem.features.feed.ui.market.detailed.components import androidx.annotation.FloatRange -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.requiredSize -import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.* import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -22,6 +18,7 @@ import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import com.tangem.core.ui.res.LocalRedesignEnabled import com.tangem.core.ui.res.TangemTheme import com.tangem.features.feed.impl.R import kotlin.math.round @@ -34,6 +31,28 @@ internal fun ScoreStarsBlock( horizontalSpacing: Dp, scoreTextStyle: TextStyle, modifier: Modifier = Modifier, +) { + if (LocalRedesignEnabled.current) { + ScoreStarsBlockV2( + score = score, + modifier = modifier, + ) + } else { + ScoreStarsBlockV1( + score = score, + horizontalSpacing = horizontalSpacing, + scoreTextStyle = scoreTextStyle, + modifier = modifier, + ) + } +} + +@Composable +private fun ScoreStarsBlockV1( + score: Float, + horizontalSpacing: Dp, + scoreTextStyle: TextStyle, + modifier: Modifier = Modifier, ) { val rounded = score.roundTo1decimal() val percentage = rounded / STARS_COUNT @@ -51,16 +70,38 @@ internal fun ScoreStarsBlock( } } +@Composable +private fun ScoreStarsBlockV2(score: Float, modifier: Modifier = Modifier) { + val rounded = score.roundTo1decimal() + val percentage = rounded / STARS_COUNT + Box( + modifier = modifier, + contentAlignment = Alignment.Center, + ) { + Stars(fraction = percentage) + } +} + @Suppress("MagicNumber") @Composable private fun Stars(@FloatRange(0.0, 1.0) fraction: Float = 0f) { + if (LocalRedesignEnabled.current) { + StarsV2(fraction) + } else { + StarsV1(fraction) + } +} + +@Suppress("MagicNumber") +@Composable +private fun StarsV1(@FloatRange(0.0, 1.0) fraction: Float = 0f) { val grayColor = TangemTheme.colors.icon.inactive Row( horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), verticalAlignment = Alignment.CenterVertically, ) { - repeat(times = 5) { i -> + repeat(times = STARS_COUNT) { i -> Box( modifier = Modifier.size(TangemTheme.dimens.size16), contentAlignment = Alignment.Center, @@ -94,6 +135,49 @@ private fun Stars(@FloatRange(0.0, 1.0) fraction: Float = 0f) { } } +@Suppress("MagicNumber") +@Composable +private fun StarsV2(@FloatRange(0.0, 1.0) fraction: Float = 0f) { + val grayColor = TangemTheme.colors2.markers.iconGray + + Row( + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1), + verticalAlignment = Alignment.CenterVertically, + ) { + repeat(times = STARS_COUNT) { i -> + Box( + modifier = Modifier.size(TangemTheme.dimens2.x5), + contentAlignment = Alignment.Center, + ) { + Icon( + modifier = Modifier + .requiredSize(16.dp) + .graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen) + .drawWithCache { + onDrawWithContent { + val starFraction = ((fraction - i * 0.2) / 0.2).coerceIn(0.0, 1.0) + val starFractionFloat = starFraction + .toFloat() + .roundTo1decimal() + + drawContent() + drawRect( + color = grayColor, + topLeft = Offset(x = size.width * starFractionFloat, y = 0f), + size = Size(size.width * (1 - starFractionFloat), size.height), + blendMode = BlendMode.SrcIn, + ) + } + }, + imageVector = ImageVector.vectorResource(R.drawable.ic_star_filled_20), + contentDescription = null, + tint = TangemTheme.colors2.markers.iconBlue, + ) + } + } + } +} + @Suppress("MagicNumber") private fun Float.roundTo1decimal(): Float { return round(this * 10) / 10 diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/components/SecurityScoreBlock.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/components/SecurityScoreBlock.kt index 1fe9bbc7fa..ec4c86a487 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/components/SecurityScoreBlock.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/components/SecurityScoreBlock.kt @@ -2,33 +2,44 @@ package com.tangem.features.feed.ui.market.detailed.components import android.content.res.Configuration import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.layoutId 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.components.SpacerH import com.tangem.core.ui.components.TextShimmer import com.tangem.core.ui.components.text.TooltipText +import com.tangem.core.ui.ds.row.TangemRowContainer +import com.tangem.core.ui.ds.row.TangemRowLayoutId import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.LocalRedesignEnabled import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.res.TangemThemePreviewRedesign import com.tangem.core.ui.utils.PreviewShimmerContainer import com.tangem.features.feed.impl.R import com.tangem.features.feed.ui.market.detailed.state.SecurityScoreUM @Composable internal fun SecurityScoreBlock(state: SecurityScoreUM, modifier: Modifier = Modifier) { + if (LocalRedesignEnabled.current) { + SecurityScoreBlockV2(state, modifier) + } else { + SecurityScoreBlockV1(state, modifier) + } +} + +@Composable +private fun SecurityScoreBlockV1(state: SecurityScoreUM, modifier: Modifier = Modifier) { Row( modifier = modifier .clip(TangemTheme.shapes.roundedCornersXMedium) @@ -66,8 +77,92 @@ internal fun SecurityScoreBlock(state: SecurityScoreUM, modifier: Modifier = Mod } } +@Composable +private fun SecurityScoreBlockV2(state: SecurityScoreUM, modifier: Modifier = Modifier) { + TangemRowContainer(modifier = modifier) { + Text( + modifier = Modifier.layoutId(layoutId = TangemRowLayoutId.START_TOP), + text = "${state.score}", + color = TangemTheme.colors2.text.neutral.primary, + style = TangemTheme.typography2.headingBold28, + ) + + InformationTextBlock( + modifier = Modifier.layoutId(layoutId = TangemRowLayoutId.START_BOTTOM), + text = resourceReference(R.string.markets_token_details_security_score), + onInfoClick = state.onInfoClick, + textColor = TangemTheme.colors2.text.neutral.primary, + informationTextBlockIconPosition = InformationTextBlockIconPosition.END, + ) + + Text( + modifier = Modifier.layoutId(layoutId = TangemRowLayoutId.END_BOTTOM), + text = state.description.resolveReference(), + style = TangemTheme.typography2.captionSemibold12, + color = TangemTheme.colors2.text.neutral.tertiary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + + ScoreStarsBlock( + modifier = Modifier + .padding(bottom = 16.dp) + .layoutId(layoutId = TangemRowLayoutId.END_TOP), + score = state.score, + scoreTextStyle = TangemTheme.typography.body1, + horizontalSpacing = TangemTheme.dimens.spacing8, + ) + } +} + @Composable internal fun SecurityScoreBlockPlaceholder(modifier: Modifier = Modifier) { + if (LocalRedesignEnabled.current) { + SecurityScoreBlockPlaceholderV2(modifier) + } else { + SecurityScoreBlockPlaceholderV1(modifier) + } +} + +@Composable +private fun SecurityScoreBlockPlaceholderV2(modifier: Modifier = Modifier) { + TangemRowContainer(modifier = modifier) { + TextShimmer( + modifier = Modifier + .width(74.dp) + .layoutId(layoutId = TangemRowLayoutId.START_TOP), + style = TangemTheme.typography2.headingBold28, + radius = TangemTheme.dimens2.x25, + ) + + TextShimmer( + modifier = Modifier + .width(96.dp) + .layoutId(layoutId = TangemRowLayoutId.START_BOTTOM), + style = TangemTheme.typography2.captionSemibold12, + radius = TangemTheme.dimens2.x25, + ) + + TextShimmer( + modifier = Modifier + .width(120.dp) + .layoutId(layoutId = TangemRowLayoutId.END_TOP), + style = TangemTheme.typography2.headingBold28, + radius = TangemTheme.dimens2.x25, + ) + + TextShimmer( + modifier = Modifier + .width(72.dp) + .layoutId(layoutId = TangemRowLayoutId.END_BOTTOM), + style = TangemTheme.typography2.captionSemibold12, + radius = TangemTheme.dimens2.x25, + ) + } +} + +@Composable +private fun SecurityScoreBlockPlaceholderV1(modifier: Modifier = Modifier) { Row( modifier = modifier .clip(TangemTheme.shapes.roundedCornersXMedium) @@ -109,7 +204,7 @@ internal fun SecurityScoreBlockPlaceholder(modifier: Modifier = Modifier) { @Preview(widthDp = 328, showBackground = true, locale = "ru") @Preview(widthDp = 328, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun ContentPreview() { +private fun ContentPreviewV1() { TangemThemePreview { SecurityScoreBlock( state = SecurityScoreUM( @@ -124,7 +219,7 @@ private fun ContentPreview() { @Preview(widthDp = 328, showBackground = true) @Preview(widthDp = 328, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun PreviewPlaceholder() { +private fun PreviewPlaceholderV1() { TangemThemePreview { PreviewShimmerContainer( shimmerContent = { @@ -133,8 +228,36 @@ private fun PreviewPlaceholder() { ) }, actualContent = { - ContentPreview() + ContentPreviewV1() }, ) } +} + +@Preview(widthDp = 328, showBackground = true) +@Preview(widthDp = 328, showBackground = true, locale = "ru") +@Preview(widthDp = 328, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun ContentPreviewV2() { + CompositionLocalProvider( + LocalRedesignEnabled provides true, + ) { + TangemThemePreviewRedesign { + Column(modifier = Modifier.background(TangemTheme.colors2.surface.level2)) { + SecurityScoreBlock( + state = SecurityScoreUM( + score = 3.5f, + description = stringReference("Based on 3 ratings"), + onInfoClick = {}, + ), + ) + + SpacerH(10.dp) + + SecurityScoreBlockPlaceholder( + modifier = Modifier.fillMaxWidth(), + ) + } + } + } } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/components/TokenMarketDetailsBody.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/components/TokenMarketDetailsBody.kt index a0457c58ec..4abff26fa9 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/components/TokenMarketDetailsBody.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/components/TokenMarketDetailsBody.kt @@ -22,6 +22,7 @@ import com.tangem.features.feed.ui.market.detailed.state.MarketsTokenDetailsUM.R @Suppress("CanBeNonNullable") internal fun LazyListScope.tokenMarketDetailsBody( + isRedesignEnabled: Boolean, state: MarketsTokenDetailsUM.Body, portfolioBlock: @Composable ((Modifier) -> Unit)?, relatedNews: RelatedNews, @@ -40,7 +41,7 @@ internal fun LazyListScope.tokenMarketDetailsBody( aboutCoinHeader() - loadingInfoBlocks() + loadingInfoBlocks(isRedesignEnabled) } is MarketsTokenDetailsUM.Body.Content -> { if (state.description != null) { @@ -59,7 +60,10 @@ internal fun LazyListScope.tokenMarketDetailsBody( aboutCoinHeader() - infoBlocksList(state.infoBlocks) + infoBlocksList( + state = state.infoBlocks, + isRedesignEnabled = isRedesignEnabled, + ) } is MarketsTokenDetailsUM.Body.Error -> { error(state) @@ -112,7 +116,7 @@ private fun LazyListScope.description(description: MarketsTokenDetailsUM.Descrip } } -internal fun LazyListScope.infoBlocksList(state: MarketsTokenDetailsUM.InformationBlocks) { +internal fun LazyListScope.infoBlocksList(state: MarketsTokenDetailsUM.InformationBlocks, isRedesignEnabled: Boolean) { if (state.insights != null) { item("insights") { InsightsBlock( @@ -122,7 +126,7 @@ internal fun LazyListScope.infoBlocksList(state: MarketsTokenDetailsUM.Informati } } - if (state.securityScore != null) { + if (state.securityScore != null && !isRedesignEnabled) { item("securityScore") { SecurityScoreBlock( modifier = Modifier.blockPaddings(), @@ -156,6 +160,15 @@ internal fun LazyListScope.infoBlocksList(state: MarketsTokenDetailsUM.Informati ) } + if (state.securityScore != null && isRedesignEnabled) { + item("securityScore") { + SecurityScoreBlock( + modifier = Modifier.blockPaddings(), + state = state.securityScore, + ) + } + } + if (state.links != null) { item("links") { LinksBlock( @@ -166,15 +179,17 @@ internal fun LazyListScope.infoBlocksList(state: MarketsTokenDetailsUM.Informati } } -private fun LazyListScope.loadingInfoBlocks() { +private fun LazyListScope.loadingInfoBlocks(isRedesignEnabled: Boolean) { item("insights-loading") { InsightsBlockPlaceholder( modifier = Modifier.blockPaddings(), ) } - item("securityScore-loading") { - SecurityScoreBlockPlaceholder(modifier = Modifier.blockPaddings()) + if (!isRedesignEnabled) { + item("securityScore-loading") { + SecurityScoreBlockPlaceholder(modifier = Modifier.blockPaddings()) + } } item("metrics-loading") { @@ -189,6 +204,12 @@ private fun LazyListScope.loadingInfoBlocks() { ListedOnBlockPlaceholder(modifier = Modifier.blockPaddings()) } + if (isRedesignEnabled) { + item("securityScore-loading") { + SecurityScoreBlockPlaceholder(modifier = Modifier.blockPaddings()) + } + } + item("links-loading") { LinksBlockPlaceholder(modifier = Modifier.blockPaddings()) }