Updated on 2026-08-14

This commit is contained in:
Tangem 2024-08-07 16:48:57 +03:00
parent 459db3d0df
commit 4b21bf989e
29 changed files with 1759 additions and 75 deletions

View file

@ -24,7 +24,7 @@ interface TangemTechMarketsApi {
suspend fun getCoinMarketData(
@Path("coin_id") coinId: String,
@Query("currency") currency: String,
): ApiResponse<TokenMarketDetailsResponse>
): ApiResponse<TokenMarketInfoResponse>
@GET("coins/{coin_id}/history")
suspend fun getCoinChart(

View file

@ -3,21 +3,19 @@ package com.tangem.datasource.api.markets.models.response
import com.squareup.moshi.Json
import java.math.BigDecimal
data class TokenMarketDetailsResponse(
data class TokenMarketInfoResponse(
@Json(name = "id")
val id: String,
@Json(name = "name")
val name: String,
@Json(name = "symbol")
val symbol: String,
@Json(name = "active")
val active: Boolean,
@Json(name = "current_price")
val currentPrice: BigDecimal,
@Json(name = "price_change_percentage")
val priceChangePercentage: PriceChangePercentage,
val priceChangePercentage: PriceChangePercentage?,
@Json(name = "networks")
val networks: List<Network>,
val networks: List<Network>?,
@Json(name = "short_description")
val shortDescription: String?,
@Json(name = "full_description")
@ -25,27 +23,28 @@ data class TokenMarketDetailsResponse(
@Json(name = "insights")
val insights: List<Insight>?,
@Json(name = "metrics")
val metrics: Metrics,
val metrics: Metrics?,
@Json(name = "links")
val links: Links,
val links: Links?,
@Json(name = "price_performance")
val pricePerformance: PricePerformance,
val pricePerformance: PricePerformance?,
) {
data class PriceChangePercentage(
@Json(name = "24h")
val h24: BigDecimal,
val day: Int?,
@Json(name = "1w")
val week1: BigDecimal,
val week: Int?,
@Json(name = "1m")
val month1: BigDecimal,
val month: Int?,
@Json(name = "3m")
val month3: BigDecimal,
val threeMonths: Int?,
@Json(name = "6m")
val month6: BigDecimal,
val sixMonths: Int?,
@Json(name = "1y")
val year1: BigDecimal,
val year: Int?,
@Json(name = "all_time")
val allTime: BigDecimal,
val allTime: Int?,
)
data class Network(
@ -55,78 +54,78 @@ data class TokenMarketDetailsResponse(
val exchangeable: Boolean,
@Json(name = "contract_address")
val contractAddress: String,
@Json(name = "decimalCount")
@Json(name = "decimal_count")
val decimalCount: Int,
)
data class Insight(
@Json(name = "holders_change")
val holdersChange: Change,
val holdersChange: Change?,
@Json(name = "liquidity_change")
val liquidityChange: Change,
val liquidityChange: Change?,
@Json(name = "buy_pressure_change")
val buyPressureChange: Change,
val buyPressureChange: Change?,
@Json(name = "experienced_buyer_change")
val experiencedBuyerChange: Change,
) {
data class Change(
@Json(name = "1d")
val day1: Int,
@Json(name = "1w")
val week1: Int,
@Json(name = "1m")
val month1: Int,
)
}
val experiencedBuyerChange: Change?,
)
data class Change(
@Json(name = "24h")
val day: Int?,
@Json(name = "1w")
val week: Int?,
@Json(name = "1m")
val month: Int?,
)
data class Metrics(
@Json(name = "market_rating")
val marketRating: Int,
val marketRating: Int?,
@Json(name = "circulating_supply")
val circulatingSupply: BigDecimal,
val circulatingSupply: BigDecimal?,
@Json(name = "market_cap")
val marketCap: BigDecimal,
val marketCap: BigDecimal?,
@Json(name = "volume_24h")
val volume24h: BigDecimal,
val volume24h: BigDecimal?,
@Json(name = "total_supply")
val totalSupply: BigDecimal,
val totalSupply: BigDecimal?,
@Json(name = "fully_diluted_valuation")
val fullyDilutedValuation: BigDecimal,
val fullyDilutedValuation: BigDecimal?,
)
data class Links(
@Json(name = "official_links")
val officialLinks: List<Link> = emptyList(),
val officialLinks: List<Link>?,
@Json(name = "social")
val social: List<Link> = emptyList(),
val social: List<Link>?,
@Json(name = "repository")
val repository: List<Link> = emptyList(),
val repository: List<Link>?,
@Json(name = "blockchain_site")
val blockchainSite: List<Link> = emptyList(),
val blockchainSite: List<Link>?,
)
data class Link(
@Json(name = "title")
val title: String?,
val title: String,
@Json(name = "id")
val id: String,
val id: String?,
@Json(name = "link")
val url: String,
val link: String,
)
data class PricePerformance(
@Json(name = "high_price")
val highPrice: Price,
@Json(name = "low_price")
val lowPrice: Price,
) {
data class Price(
@Json(name = "24h")
val h24: BigDecimal,
@Json(name = "1m")
val month1: BigDecimal,
@Json(name = "all_time")
val allTime: BigDecimal,
)
}
@Json(name = "24h")
val day: Range?,
@Json(name = "1m")
val month: Range?,
@Json(name = "all_time")
val allTime: Range?,
)
data class Range(
@Json(name = "low")
val low: Int?,
@Json(name = "high")
val high: Int?,
)
}

View file

@ -10,6 +10,7 @@ import androidx.compose.material.ButtonColors
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.R
import com.tangem.core.ui.components.buttons.common.*
@ -26,6 +27,7 @@ fun TextButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
colors: ButtonColors = TangemButtonsDefaults.defaultTextButtonColors,
textStyle: TextStyle = TangemTheme.typography.button,
enabled: Boolean = true,
) {
TangemButton(
@ -37,6 +39,7 @@ fun TextButton(
showProgress = false,
colors = colors,
size = TangemButtonSize.Text,
textStyle = textStyle,
)
}

View file

@ -9,14 +9,23 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.TextStyle
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.buttons.PrimarySmallButton
import com.tangem.core.ui.components.buttons.SmallButtonConfig
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.*
import com.valentinilk.shimmer.*
@ -45,6 +54,70 @@ fun CircleShimmer(modifier: Modifier = Modifier) {
)
}
/**
* Shimmer for text
* Height will be set automatically
*
* @param textSizeHeight if true, height will be set to font size height.
*/
@Composable
fun TextShimmer(
style: TextStyle,
modifier: Modifier = Modifier,
text: String = "A",
radius: Dp = TangemTheme.dimens.radius3,
textSizeHeight: Boolean = false,
) {
if (textSizeHeight) {
val lineHeight = with(LocalDensity.current) { style.lineHeight.toDp() }
Box(
modifier = Modifier.requiredHeight(lineHeight),
contentAlignment = Alignment.CenterStart,
) {
Text(
modifier = modifier
.clip(RoundedCornerShape(size = radius))
.shimmer(LocalTangemShimmer.current),
text = text,
style = style.copy(lineHeight = style.fontSize),
maxLines = 1,
)
}
} else {
Text(
modifier = modifier
.clip(RoundedCornerShape(size = radius))
.shimmer(LocalTangemShimmer.current),
text = text,
style = style,
maxLines = 1,
)
}
}
/**
* Shimmer for SmallButton
* Height and min width will be set automatically
*/
@Composable
fun SmallButtonShimmer(modifier: Modifier = Modifier, withIcon: Boolean = false) {
PrimarySmallButton(
config = SmallButtonConfig(
text = stringReference("B"),
onClick = {},
icon = if (withIcon) {
TangemButtonIconPosition.Start(iconResId = R.drawable.ic_plus_24)
} else {
TangemButtonIconPosition.None
},
),
modifier = modifier
.clip(RoundedCornerShape(size = TangemTheme.dimens.radius16))
.shimmer(LocalTangemShimmer.current),
)
}
internal val TangemShimmer: Shimmer
@Composable
get() = rememberShimmer(
@ -106,6 +179,17 @@ private fun ShimmersPreview() {
.height(TangemTheme.dimens.size24),
)
CircleShimmer(modifier = Modifier.size(size = TangemTheme.dimens.size42))
TextShimmer(
style = TangemTheme.typography.body1,
modifier = Modifier.fillMaxWidth(fraction = 0.4f),
)
TextShimmer(
style = TangemTheme.typography.body1,
textSizeHeight = true,
modifier = Modifier.fillMaxWidth(fraction = 0.4f),
)
SmallButtonShimmer(withIcon = true)
SmallButtonShimmer(withIcon = false)
}
}
}

View file

@ -97,7 +97,6 @@ private fun Preview_Grid() {
},
content = {
GridItems(
itemPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing4),
items = persistentListOf(
stringReference("Fist item"),
stringReference("Second item"),
@ -105,7 +104,6 @@ private fun Preview_Grid() {
itemContent = {
PreviewItem(text = it)
},
horizontalArragement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
)
},
)

View file

@ -44,7 +44,6 @@ inline fun <T : Any> InformationBlockContentScope.GridItems(
items: ImmutableList<T>,
itemContent: @Composable BoxScope.(T) -> Unit,
modifier: Modifier = Modifier,
itemPadding: PaddingValues = PaddingValues(all = TangemTheme.dimens.spacing0),
verticalAlignment: Alignment.Vertical = Alignment.Top,
horizontalArragement: Arrangement.Horizontal = Arrangement.Start,
) {
@ -59,7 +58,6 @@ inline fun <T : Any> InformationBlockContentScope.GridItems(
Column(
modifier = modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top,
) {
rowItems.fastForEach { row ->
@ -70,10 +68,7 @@ inline fun <T : Any> InformationBlockContentScope.GridItems(
) {
row.fastForEach { item ->
Box(
modifier = Modifier
.padding(itemPadding)
.weight(1f),
contentAlignment = Alignment.Center,
modifier = Modifier.weight(1f),
) {
itemContent(item)
}

View file

@ -13,6 +13,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@ -28,7 +29,7 @@ fun TooltipText(
text: TextReference,
onInfoClick: () -> Unit,
modifier: Modifier = Modifier,
useSmallerText: Boolean = false,
textStyle: TextStyle = TangemTheme.typography.caption2,
) {
val interactionSource = remember { MutableInteractionSource() }
@ -45,18 +46,16 @@ fun TooltipText(
Text(
modifier = Modifier.weight(1f, fill = false),
text = text.resolveReference(),
style = if (useSmallerText) {
TangemTheme.typography.caption2
} else {
TangemTheme.typography.subtitle2
},
style = textStyle,
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
IconButton(
modifier = Modifier.requiredSize(TangemTheme.dimens.size24),
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing4)
.requiredSize(TangemTheme.dimens.size16),
interactionSource = interactionSource,
onClick = onInfoClick,
) {

View file

@ -0,0 +1,33 @@
package com.tangem.core.ui.res
import androidx.compose.animation.*
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.compose.runtime.*
@Immutable
object TangemAnimations {
val transitionSpecs = TransitionSpecs
@Composable
@NonRestartableComposable
fun horizontalIndicatorAsState(targetFraction: Float): State<Float> {
return animateFloatAsState(
targetValue = targetFraction,
animationSpec = tween(durationMillis = 300),
label = "Indicator fraction",
)
}
@Immutable
object TransitionSpecs {
@Stable
val textChange: ContentTransform =
fadeIn(animationSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy)) togetherWith
fadeOut(animationSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy))
}
}

View file

@ -0,0 +1,55 @@
package com.tangem.core.ui.utils
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onGloballyPositioned
import com.tangem.core.ui.components.SpacerH4
import com.tangem.core.ui.res.TangemThemePreview
import kotlinx.coroutines.delay
/**
* A container that shows a shimmer effect on top of the actual content.
* The shimmer effect is toggled every 2 seconds.
* Used for previewing components with shimmer effect and comparing their sizes with the actual content.
*
* If height is changing during the preview, it means that the actual content is not aligned with the shimmer effect.
*
* @param actualContent The actual content to be displayed.
* @param shimmerContent The shimmer effect to be displayed.
*/
@Composable
fun PreviewShimmerContainer(actualContent: @Composable () -> Unit, shimmerContent: @Composable () -> Unit) {
Column {
var height by remember { mutableIntStateOf(0) }
Row {
Text("height = $height")
}
SpacerH4()
var shimmerVisible by remember { mutableStateOf(true) }
TangemThemePreview {
Box(
Modifier.onGloballyPositioned {
height = it.size.height
},
) {
actualContent()
if (shimmerVisible) {
shimmerContent()
}
}
}
LaunchedEffect(Unit) {
while (true) {
delay(timeMillis = 2000)
shimmerVisible = !shimmerVisible
}
}
}
}

View file

@ -107,4 +107,13 @@ internal class DefaultMarketsTokenRepository(
return tokenChartConverter.convert(interval, response.getOrThrow())
}
override suspend fun getTokenInfo(fiatCurrencyCode: String, tokenId: String): TokenMarketInfo {
val response = marketsApi.getCoinMarketData(
currency = fiatCurrencyCode,
coinId = tokenId,
)
return TokenMarketInfoConverter().convert(response.getOrThrow())
}
}

View file

@ -0,0 +1,117 @@
package com.tangem.data.markets.converters
import com.tangem.datasource.api.markets.models.response.TokenMarketInfoResponse
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.utils.converter.Converter
internal class TokenMarketInfoConverter : Converter<TokenMarketInfoResponse, TokenMarketInfo> {
override fun convert(value: TokenMarketInfoResponse): TokenMarketInfo {
return with(value) {
TokenMarketInfo(
id = id,
name = name,
symbol = symbol,
currentPrice = currentPrice,
priceChangePercentage = priceChangePercentage?.convert(),
networks = networks?.convert(),
shortDescription = shortDescription,
fullDescription = fullDescription,
insights = insights?.convert(),
metrics = metrics?.convert(),
links = links?.convert(),
pricePerformance = pricePerformance?.convert(),
)
}
}
private fun TokenMarketInfoResponse.PriceChangePercentage.convert(): TokenMarketInfo.PriceChangePercentage {
return TokenMarketInfo.PriceChangePercentage(
day = day,
week = week,
month = month,
threeMonths = threeMonths,
sixMonths = sixMonths,
year = year,
allTime = allTime,
)
}
@JvmName("convertNetwork")
private fun List<TokenMarketInfoResponse.Network>.convert(): List<TokenMarketInfo.Network> {
return map {
TokenMarketInfo.Network(
networkId = it.networkId,
exchangeable = it.exchangeable,
contractAddress = it.contractAddress,
decimalCount = it.decimalCount,
)
}
}
@JvmName("convertInsight")
private fun List<TokenMarketInfoResponse.Insight>.convert(): List<TokenMarketInfo.Insight> {
return map {
TokenMarketInfo.Insight(
holdersChange = it.holdersChange?.convert(),
liquidityChange = it.liquidityChange?.convert(),
buyPressureChange = it.buyPressureChange?.convert(),
experiencedBuyerChange = it.experiencedBuyerChange?.convert(),
)
}
}
private fun TokenMarketInfoResponse.Change.convert(): TokenMarketInfo.Change {
return TokenMarketInfo.Change(
day = day,
week = week,
month = month,
)
}
private fun TokenMarketInfoResponse.Metrics.convert(): TokenMarketInfo.Metrics {
return TokenMarketInfo.Metrics(
marketRating = marketRating,
circulatingSupply = circulatingSupply,
marketCap = marketCap,
volume24h = volume24h,
totalSupply = totalSupply,
fullyDilutedValuation = fullyDilutedValuation,
)
}
private fun TokenMarketInfoResponse.Links.convert(): TokenMarketInfo.Links {
return TokenMarketInfo.Links(
officialLinks = officialLinks?.convert(),
social = social?.convert(),
repository = repository?.convert(),
blockchainSite = blockchainSite?.convert(),
)
}
@JvmName("convertLink")
private fun List<TokenMarketInfoResponse.Link>.convert(): List<TokenMarketInfo.Link> {
return map {
TokenMarketInfo.Link(
title = it.title,
id = it.id,
link = it.link,
)
}
}
private fun TokenMarketInfoResponse.PricePerformance.convert(): TokenMarketInfo.PricePerformance {
return TokenMarketInfo.PricePerformance(
day = day?.convert(),
month = month?.convert(),
allTime = allTime?.convert(),
)
}
private fun TokenMarketInfoResponse.Range.convert(): TokenMarketInfo.Range {
return TokenMarketInfo.Range(
low = low,
high = high,
)
}
}

View file

@ -0,0 +1,81 @@
package com.tangem.domain.markets
import java.math.BigDecimal
data class TokenMarketInfo(
val id: String,
val name: String,
val symbol: String,
val currentPrice: BigDecimal,
val priceChangePercentage: PriceChangePercentage?,
val networks: List<Network>?,
val shortDescription: String?,
val fullDescription: String?,
val insights: List<Insight>?,
val metrics: Metrics?,
val links: Links?,
val pricePerformance: PricePerformance?,
) {
data class PriceChangePercentage(
val day: Int?,
val week: Int?,
val month: Int?,
val threeMonths: Int?,
val sixMonths: Int?,
val year: Int?,
val allTime: Int?,
)
data class Network(
val networkId: String,
val exchangeable: Boolean,
val contractAddress: String,
val decimalCount: Int,
)
data class Insight(
val holdersChange: Change?,
val liquidityChange: Change?,
val buyPressureChange: Change?,
val experiencedBuyerChange: Change?,
)
data class Change(
val day: Int?,
val week: Int?,
val month: Int?,
)
data class Metrics(
val marketRating: Int?,
val circulatingSupply: BigDecimal?,
val marketCap: BigDecimal?,
val volume24h: BigDecimal?,
val totalSupply: BigDecimal?,
val fullyDilutedValuation: BigDecimal?,
)
data class Links(
val officialLinks: List<Link>?,
val social: List<Link>?,
val repository: List<Link>?,
val blockchainSite: List<Link>?,
)
data class Link(
val title: String,
val id: String?,
val link: String,
)
data class PricePerformance(
val day: Range?,
val month: Range?,
val allTime: Range?,
)
data class Range(
val low: Int?,
val high: Int?,
)
}

View file

@ -0,0 +1,19 @@
package com.tangem.domain.markets
import arrow.core.Either
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.repositories.MarketsTokenRepository
class GetTokenMarketInfoUseCase(
private val marketsTokenRepository: MarketsTokenRepository,
) {
suspend operator fun invoke(appCurrency: AppCurrency, tokenId: String): Either<Unit, TokenMarketInfo> {
return Either.catch {
marketsTokenRepository.getTokenInfo(
fiatCurrencyCode = appCurrency.code,
tokenId = tokenId,
)
}.mapLeft {}
}
}

View file

@ -11,4 +11,6 @@ interface MarketsTokenRepository {
): TokenListBatchFlow
suspend fun getChart(fiatCurrencyCode: String, interval: PriceChangeInterval, tokenId: String): TokenChart
suspend fun getTokenInfo(fiatCurrencyCode: String, tokenId: String): TokenMarketInfo
}

View file

@ -23,6 +23,7 @@ dependencies {
/* Compose */
implementation(deps.compose.coil)
implementation(deps.compose.foundation)
implementation(deps.compose.material)
implementation(deps.compose.material3)
implementation(deps.compose.ui)
implementation(deps.compose.ui.tooling)

View file

@ -0,0 +1,145 @@
package com.tangem.features.markets.details.impl.ui.components
import android.content.res.Configuration
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.background
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.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.components.text.TooltipText
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemAnimations
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.PreviewShimmerContainer
import com.tangem.features.markets.details.impl.ui.entity.InfoPointUM
@Composable
internal fun InfoPoint(infoPointUM: InfoPointUM, modifier: Modifier = Modifier) {
Column(
modifier = modifier.padding(vertical = TangemTheme.dimens.spacing8),
horizontalAlignment = Alignment.Start,
) {
if (infoPointUM.onInfoClick != null) {
TooltipText(
text = infoPointUM.title,
onInfoClick = infoPointUM.onInfoClick,
textStyle = TangemTheme.typography.caption2,
)
} else {
Text(
text = infoPointUM.title.resolveReference(),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
AnimatedContent(
modifier = Modifier,
targetState = infoPointUM.value,
transitionSpec = { TangemAnimations.transitionSpecs.textChange },
label = "insight block",
) {
Text(
text = it,
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.primary1,
)
}
}
}
@Composable
internal fun InfoPointShimmer(modifier: Modifier = Modifier, withTooltip: Boolean = false) {
Column(
modifier = modifier.padding(vertical = TangemTheme.dimens.spacing8),
horizontalAlignment = Alignment.Start,
) {
if (withTooltip) {
Box(
modifier = Modifier
.requiredHeight(TangemTheme.dimens.size16)
.fillMaxWidth(),
contentAlignment = Alignment.CenterStart,
) {
TextShimmer(
modifier = Modifier.fillMaxWidth(),
style = TangemTheme.typography.caption2,
textSizeHeight = false,
)
}
} else {
TextShimmer(
modifier = Modifier.fillMaxWidth(),
style = TangemTheme.typography.caption2,
textSizeHeight = true,
)
}
TextShimmer(
modifier = Modifier.fillMaxWidth(fraction = 0.5f),
style = TangemTheme.typography.body1,
textSizeHeight = true,
)
}
}
@Preview
@Preview("Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun ContentPreview() {
TangemThemePreview {
Column(
modifier = Modifier
.width(150.dp)
.background(TangemTheme.colors.background.tertiary),
) {
InfoPoint(
infoPointUM = InfoPointUM(
title = stringReference("Market Cap"),
value = "$1,000,000,000",
),
)
InfoPoint(
infoPointUM = InfoPointUM(
title = stringReference("Market Cap"),
value = "$1,000,000,000",
onInfoClick = { },
),
)
}
}
}
@Preview
@Preview("Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PreviewShimmer() {
TangemThemePreview {
PreviewShimmerContainer(
shimmerContent = {
Column(
modifier = Modifier
.width(150.dp)
.background(TangemTheme.colors.background.tertiary),
) {
InfoPointShimmer(modifier = Modifier.fillMaxWidth())
InfoPointShimmer(
modifier = Modifier.fillMaxWidth(),
withTooltip = true,
)
}
},
actualContent = {
ContentPreview()
},
)
}
}

View file

@ -0,0 +1,198 @@
package com.tangem.features.markets.details.impl.ui.components
import android.content.res.Configuration
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.block.information.GridItems
import com.tangem.core.ui.components.block.information.InformationBlock
import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.PreviewShimmerContainer
import com.tangem.domain.markets.PriceChangeInterval
import com.tangem.features.markets.details.impl.ui.entity.InfoPointUM
import com.tangem.features.markets.details.impl.ui.entity.InsightsUM
import com.tangem.features.markets.details.impl.ui.getText
import com.tangem.features.markets.impl.R
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
@Composable
internal fun InsightsBlock(state: InsightsUM, modifier: Modifier = Modifier) {
var currentInterval by remember { mutableStateOf(PriceChangeInterval.H24) }
InformationBlock(
modifier = modifier,
title = {
Text(
modifier = Modifier,
text = stringResource(R.string.markets_token_details_insights),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
},
action = {
SegmentedButtons(
config = persistentListOf(
PriceChangeInterval.H24,
PriceChangeInterval.WEEK,
PriceChangeInterval.MONTH,
),
initialSelectedItem = PriceChangeInterval.H24,
onClick = { currentInterval = it },
) {
Box(
Modifier
.fillMaxSize()
.align(Alignment.Center)
.padding(vertical = TangemTheme.dimens.spacing4),
) {
Text(
modifier = Modifier.align(Alignment.Center),
text = it.getText().resolveReference(),
style = TangemTheme.typography.caption1,
color = TangemTheme.colors.text.primary1,
)
}
}
},
content = {
val infoPoints = when (currentInterval) {
PriceChangeInterval.H24 -> state.h24Info
PriceChangeInterval.WEEK -> state.weekInfo
PriceChangeInterval.MONTH -> state.monthInfo
else -> state.h24Info
}
GridItems(
items = infoPoints,
itemContent = {
InfoPoint(
modifier = Modifier.align(Alignment.CenterStart),
infoPointUM = it,
)
},
)
},
)
}
@Composable
internal fun InsightsBlockPlaceholder(modifier: Modifier = Modifier) {
val subtitle2dp = with(LocalDensity.current) { TangemTheme.typography.subtitle2.lineHeight.toDp() }
val caption1dp = with(LocalDensity.current) { TangemTheme.typography.caption1.lineHeight.toDp() }
val headerHeight = maxOf(subtitle2dp, caption1dp) + TangemTheme.dimens.spacing4
InformationBlock(
modifier = modifier,
title = {
RectangleShimmer(
modifier = Modifier
.height(headerHeight)
.fillMaxWidth(),
radius = TangemTheme.dimens.radius3,
)
},
content = {
GridItems(
items = List(size = 4) { it }.toImmutableList(),
horizontalArragement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
itemContent = {
InfoPointShimmer(
modifier = Modifier.fillMaxWidth(),
)
},
)
},
)
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun ContentPreview() {
TangemThemePreview {
InsightsBlock(
state = InsightsUM(
h24Info = persistentListOf(
InfoPointUM(
title = resourceReference(R.string.markets_token_details_experienced_buyers),
value = "1 000 000 000",
),
InfoPointUM(
title = resourceReference(R.string.markets_token_details_buy_pressure),
value = "1 000 000 000",
),
InfoPointUM(
title = resourceReference(R.string.markets_token_details_holders),
value = "1 000 000 000",
),
InfoPointUM(
title = resourceReference(R.string.markets_token_details_liquidity),
value = "1 000 000 000",
),
),
weekInfo = persistentListOf(
InfoPointUM(
title = resourceReference(R.string.markets_token_details_experienced_buyers),
value = "1 000 000",
),
InfoPointUM(
title = resourceReference(R.string.markets_token_details_buy_pressure),
value = "1 000 000",
),
InfoPointUM(
title = resourceReference(R.string.markets_token_details_holders),
value = "1 000 000",
),
InfoPointUM(
title = resourceReference(R.string.markets_token_details_liquidity),
value = "1 000 000",
),
),
monthInfo = persistentListOf(
InfoPointUM(
title = resourceReference(R.string.markets_token_details_experienced_buyers),
value = "1 000",
),
InfoPointUM(
title = resourceReference(R.string.markets_token_details_buy_pressure),
value = "1 000",
),
InfoPointUM(
title = resourceReference(R.string.markets_token_details_holders),
value = "1 000",
),
InfoPointUM(
title = resourceReference(R.string.markets_token_details_liquidity),
value = "1 000",
),
),
),
)
}
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PreviewPlaceholder() {
TangemThemePreview {
PreviewShimmerContainer(
actualContent = { ContentPreview() },
shimmerContent = { InsightsBlockPlaceholder() },
)
}
}

View file

@ -0,0 +1,221 @@
package com.tangem.features.markets.details.impl.ui.components
import android.content.res.Configuration
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.charts.downsample.fastForEach
import com.tangem.core.ui.components.SmallButtonShimmer
import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.components.block.information.InformationBlock
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.inputrow.inner.DividerContainer
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.PreviewShimmerContainer
import com.tangem.features.markets.details.impl.ui.entity.LinksUM
import com.tangem.features.markets.impl.R
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@Composable
internal fun LinksBlock(state: LinksUM, modifier: Modifier = Modifier) {
InformationBlock(
modifier = modifier,
title = {
Text(
text = stringResource(id = R.string.markets_token_details_links),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
},
content = {
Column {
SubBlock(
title = stringResource(id = R.string.markets_token_details_official_links),
links = state.officialLinks,
onLinkClick = state.onLinkClick,
)
SubBlock(
title = stringResource(id = R.string.markets_token_details_social),
links = state.social,
onLinkClick = state.onLinkClick,
)
SubBlock(
title = stringResource(id = R.string.markets_token_details_repository),
links = state.repository,
onLinkClick = state.onLinkClick,
)
SubBlock(
title = stringResource(id = R.string.markets_token_details_blockchain_site),
links = state.blockchainSite,
onLinkClick = state.onLinkClick,
)
}
},
)
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun SubBlock(
links: ImmutableList<LinksUM.LinkUM>,
onLinkClick: (LinksUM.LinkUM) -> Unit,
modifier: Modifier = Modifier,
lastBlock: Boolean = false,
title: String = "Official links",
) {
if (links.isEmpty()) return
DividerContainer(
modifier = modifier,
showDivider = !lastBlock,
) {
Column(
modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing12),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
Text(
text = title,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
FlowRow(
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
links.fastForEach {
SecondarySmallButton(
config = SmallButtonConfig(
text = it.title,
onClick = { onLinkClick(it) },
icon = TangemButtonIconPosition.Start(iconResId = it.iconRes),
),
)
}
}
}
}
}
@Composable
fun LinksBlockPlaceholder(modifier: Modifier = Modifier) {
InformationBlock(
modifier = modifier,
title = {
TextShimmer(
modifier = Modifier.fillMaxWidth(),
style = TangemTheme.typography.subtitle2,
)
},
content = {
Column {
SubBlockPlaceholder()
SubBlockPlaceholder()
SubBlockPlaceholder(lastBlock = true)
}
},
)
}
@Composable
private fun SubBlockPlaceholder(modifier: Modifier = Modifier, lastBlock: Boolean = false) {
DividerContainer(
modifier = modifier,
showDivider = !lastBlock,
) {
Column(
modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing12),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
TextShimmer(
modifier = Modifier.width(78.dp),
style = TangemTheme.typography.caption2,
)
Row(
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
repeat(times = 3) {
SmallButtonShimmer(
modifier = Modifier.weight(1f),
withIcon = true,
)
}
}
}
}
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun ContentPreview() {
TangemThemePreview {
LinksBlock(
state = LinksUM(
officialLinks = persistentListOf(
LinksUM.LinkUM(
title = stringReference("Website"),
iconRes = R.drawable.ic_plus_24,
url = "https://tangem.com",
),
LinksUM.LinkUM(
title = stringReference("Website"),
iconRes = R.drawable.ic_plus_24,
url = "https://tangem.com",
),
LinksUM.LinkUM(
title = stringReference("Website"),
iconRes = R.drawable.ic_plus_24,
url = "https://tangem.com",
),
),
social = persistentListOf(
LinksUM.LinkUM(
title = stringReference("Twitter"),
iconRes = R.drawable.ic_plus_24,
url = "https://tangem.com",
),
LinksUM.LinkUM(
title = stringReference("Facebook"),
iconRes = R.drawable.ic_plus_24,
url = "https://tangem.com",
),
),
repository = persistentListOf(
LinksUM.LinkUM(
title = stringReference("Github"),
iconRes = R.drawable.ic_plus_24,
url = "https://tangem.com",
),
),
blockchainSite = persistentListOf(),
onLinkClick = {},
),
)
}
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PlaceholderPreview() {
TangemThemePreview {
PreviewShimmerContainer(
shimmerContent = { LinksBlockPlaceholder() },
actualContent = { ContentPreview() },
)
}
}

View file

@ -18,7 +18,7 @@ import com.tangem.features.markets.details.impl.ui.entity.MarketsTokenDetailsUM
import com.tangem.features.markets.tokenlist.impl.ui.components.UnableToLoadData
@Composable
fun MarketTokenDetailsChart(state: MarketsTokenDetailsUM.ChartState, modifier: Modifier = Modifier) {
internal fun MarketTokenDetailsChart(state: MarketsTokenDetailsUM.ChartState, modifier: Modifier = Modifier) {
val growingColor = TangemTheme.colors.icon.accent
val fallingColor = TangemTheme.colors.icon.warning

View file

@ -0,0 +1,166 @@
package com.tangem.features.markets.details.impl.ui.components
import android.content.res.Configuration
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.TextButton
import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.components.block.information.GridItems
import com.tangem.core.ui.components.block.information.InformationBlock
import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.PreviewShimmerContainer
import com.tangem.features.markets.details.impl.ui.entity.InfoPointUM
import com.tangem.features.markets.details.impl.ui.entity.MetricsUM
import com.tangem.features.markets.impl.R
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
const val MAX_METRICS_COUNT = 6
@Composable
internal fun MetricsBlock(state: MetricsUM, modifier: Modifier = Modifier) {
var expanded by remember { mutableStateOf(false) }
InformationBlock(
modifier = modifier,
title = {
Text(
text = stringResource(id = R.string.markets_token_details_metrics),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
},
action = {
if (state.metrics.size > MAX_METRICS_COUNT) {
ShowLessMoreButton(expanded = expanded, onClick = { expanded = !expanded })
}
},
content = {
val metrics = if (expanded) {
state.metrics
} else {
state.metrics.take(MAX_METRICS_COUNT).toImmutableList()
}
GridItems(
items = metrics,
itemContent = {
InfoPoint(infoPointUM = it)
},
)
},
)
}
// TODO make TextButton clickable area smaller and remove paddings for an action in InformationBlock
@Composable
private fun ShowLessMoreButton(expanded: Boolean, onClick: () -> Unit) {
// FIXME add string resources
val text = if (expanded) {
"See less"
} else {
"See more"
}
TextButton(
text = text,
onClick = onClick,
colors = TangemButtonsDefaults.positiveButtonColors,
textStyle = TangemTheme.typography.body2,
)
}
@Composable
internal fun MetricsBlockPlaceholder(modifier: Modifier = Modifier) {
InformationBlock(
modifier = modifier,
title = {
TextShimmer(
modifier = Modifier.fillMaxWidth(),
radius = TangemTheme.dimens.radius3,
style = TangemTheme.typography.subtitle2,
)
},
action = {
Box(Modifier)
},
content = {
GridItems(
items = List(size = 6) { it }.toImmutableList(),
horizontalArragement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
itemContent = {
InfoPointShimmer(
modifier = Modifier.fillMaxWidth(),
withTooltip = true,
)
},
)
},
)
}
@Preview
@Preview("Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun BlockPreview() {
TangemThemePreview {
MetricsBlock(
state = MetricsUM(
metrics = persistentListOf(
InfoPointUM(
title = resourceReference(R.string.markets_token_details_market_capitalization),
value = "1.2T",
onInfoClick = {},
),
InfoPointUM(
title = resourceReference(R.string.markets_token_details_market_rating),
value = "A",
onInfoClick = {},
),
InfoPointUM(
title = resourceReference(R.string.markets_token_details_trading_volume),
value = "1.2T",
onInfoClick = {},
),
InfoPointUM(
title = resourceReference(R.string.markets_token_details_fully_diluted_valuation),
value = "1.2T",
onInfoClick = {},
),
InfoPointUM(
title = resourceReference(R.string.markets_token_details_circulating_supply),
value = "1.2T",
onInfoClick = {},
),
InfoPointUM(
title = resourceReference(R.string.markets_token_details_total_supply),
value = "1.2T",
onInfoClick = {},
),
),
),
)
}
}
@Preview
@Preview("Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PreviewPlaceholder() {
TangemThemePreview {
PreviewShimmerContainer(
actualContent = { BlockPreview() },
shimmerContent = { MetricsBlockPlaceholder() },
)
}
}

View file

@ -0,0 +1,260 @@
package com.tangem.features.markets.details.impl.ui.components
import android.content.res.Configuration
import androidx.compose.animation.*
import androidx.compose.foundation.layout.*
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
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.components.RectangleShimmer
import com.tangem.core.ui.components.SpacerW8
import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.components.block.information.InformationBlock
import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemAnimations
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.PreviewShimmerContainer
import com.tangem.domain.markets.PriceChangeInterval
import com.tangem.features.markets.details.impl.ui.entity.PricePerformanceUM
import com.tangem.features.markets.details.impl.ui.getText
import com.tangem.features.markets.impl.R
import kotlinx.collections.immutable.persistentListOf
@Composable
internal fun PricePerformanceBlock(state: PricePerformanceUM, modifier: Modifier = Modifier) {
var currentInterval by remember { mutableStateOf(PriceChangeInterval.H24) }
InformationBlock(
modifier = modifier,
title = {
Text(
text = stringResource(id = R.string.markets_token_details_price_performance),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
},
action = {
SegmentedButtons(
config = persistentListOf(
PriceChangeInterval.H24,
PriceChangeInterval.MONTH,
PriceChangeInterval.ALL_TIME,
),
initialSelectedItem = PriceChangeInterval.H24,
onClick = { currentInterval = it },
) {
Box(
Modifier
.fillMaxSize()
.align(Alignment.Center)
.padding(vertical = TangemTheme.dimens.spacing4),
) {
Text(
modifier = Modifier.align(Alignment.Center),
text = it.getText().resolveReference(),
style = TangemTheme.typography.caption1,
color = TangemTheme.colors.text.primary1,
)
}
}
},
content = {
val value = when (currentInterval) {
PriceChangeInterval.H24 -> state.h24
PriceChangeInterval.MONTH -> state.month
PriceChangeInterval.ALL_TIME -> state.all
else -> error("")
}
Content(
modifier = Modifier.fillMaxWidth(),
state = value,
)
},
)
}
@Composable
private fun Content(state: PricePerformanceUM.Value, modifier: Modifier = Modifier) {
val animatedIndicatorFraction by TangemAnimations.horizontalIndicatorAsState(
targetFraction = state.indicatorFraction,
)
Column(
modifier = modifier
.padding(vertical = TangemTheme.dimens.spacing8),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(
text = stringResource(R.string.markets_token_details_low),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)
SpacerW8()
Text(
text = stringResource(R.string.markets_token_details_high),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)
}
LinearProgressIndicator(
modifier = Modifier
.height(TangemTheme.dimens.size6)
.fillMaxWidth(),
progress = { animatedIndicatorFraction },
color = TangemTheme.colors.text.accent,
trackColor = TangemTheme.colors.background.tertiary,
strokeCap = StrokeCap.Round,
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
) {
AnimatedContent(
targetState = state.low,
transitionSpec = { TangemAnimations.transitionSpecs.textChange },
label = "Low price",
) {
Text(
text = it,
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.primary1,
)
}
SpacerW8()
AnimatedContent(
targetState = state.high,
transitionSpec = { TangemAnimations.transitionSpecs.textChange },
label = "High price",
) {
Text(
text = it,
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.End,
)
}
}
}
}
@Composable
internal fun PricePerformanceBlockPlaceholder(modifier: Modifier = Modifier) {
val subtitle2dp = with(LocalDensity.current) { TangemTheme.typography.subtitle2.lineHeight.toDp() }
val caption1dp = with(LocalDensity.current) { TangemTheme.typography.caption1.lineHeight.toDp() }
val headerHeight = maxOf(subtitle2dp, caption1dp) + TangemTheme.dimens.spacing4
InformationBlock(
modifier = modifier,
title = {
RectangleShimmer(
modifier = Modifier
.height(headerHeight)
.fillMaxWidth(),
radius = TangemTheme.dimens.radius3,
)
},
content = {
Column(
modifier = modifier.padding(vertical = TangemTheme.dimens.spacing8),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
) {
TextShimmer(
modifier = Modifier.width(35.dp),
style = TangemTheme.typography.caption2,
)
SpacerW8()
TextShimmer(
modifier = Modifier.width(35.dp),
style = TangemTheme.typography.caption2,
)
}
RectangleShimmer(
modifier = Modifier
.height(TangemTheme.dimens.size6)
.fillMaxWidth(),
radius = 27.dp,
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
) {
TextShimmer(
modifier = Modifier.width(TangemTheme.dimens.size56),
style = TangemTheme.typography.body1,
)
SpacerW8()
TextShimmer(
modifier = Modifier.width(TangemTheme.dimens.size56),
style = TangemTheme.typography.body1,
)
}
}
},
)
}
@Preview
@Preview("Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun ContentPreview() {
TangemThemePreview {
PricePerformanceBlock(
modifier = Modifier,
state = PricePerformanceUM(
h24 = PricePerformanceUM.Value(
low = "\$38,5K",
high = "\$58,5K",
indicatorFraction = 0.5f,
),
month = PricePerformanceUM.Value(
low = "\$500,5K",
high = "\$5800,5K",
indicatorFraction = 0.8f,
),
all = PricePerformanceUM.Value(
low = "\$58,52",
high = "\$580,5M",
indicatorFraction = 0.2f,
),
),
)
}
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PlaceholderPreview() {
TangemThemePreview {
PreviewShimmerContainer(
shimmerContent = {
PricePerformanceBlockPlaceholder()
},
actualContent = {
ContentPreview()
},
)
}
}

View file

@ -0,0 +1,203 @@
package com.tangem.features.markets.details.impl.ui.components
import android.content.res.Configuration
import androidx.annotation.FloatRange
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Icon
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.drawWithCache
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.CompositingStrategy
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
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.RectangleShimmer
import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.components.block.information.InformationBlock
import com.tangem.core.ui.components.text.TooltipText
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.PreviewShimmerContainer
import com.tangem.features.markets.details.impl.ui.entity.SecurityScoreUM
import com.tangem.features.markets.impl.R
import kotlin.math.round
private const val STARS_COUNT = 5
@Composable
internal fun SecurityScoreBlock(state: SecurityScoreUM, modifier: Modifier = Modifier) {
val rounded = state.score.roundTo1decimal()
val percentage = rounded / STARS_COUNT
InformationBlock(
modifier = modifier,
title = {
Column(
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing6),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4),
) {
TooltipText(
text = resourceReference(R.string.markets_token_details_security_score),
onInfoClick = state.onInfoClick,
textStyle = TangemTheme.typography.subtitle2,
)
Text(
text = state.description,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
},
action = {
Row(
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing6),
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = rounded.toString(),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.primary1,
)
Stars(fraction = percentage)
}
},
)
}
@Suppress("MagicNumber")
@Composable
private fun Stars(@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 ->
Box(
modifier = Modifier.size(TangemTheme.dimens.size16),
contentAlignment = Alignment.Center,
) {
Icon(
modifier = Modifier
.requiredSize(13.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_24),
contentDescription = null,
tint = TangemTheme.colors.icon.accent,
)
}
}
}
}
@Suppress("MagicNumber")
private fun Float.roundTo1decimal(): Float {
return round(this * 10) / 10
}
@Composable
internal fun SecurityScorePlaceHolder(modifier: Modifier = Modifier) {
InformationBlock(
modifier = modifier,
title = {
Column(
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing6),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4),
) {
TextShimmer(
modifier = Modifier.fillMaxWidth(),
style = TangemTheme.typography.subtitle2,
textSizeHeight = true,
)
TextShimmer(
modifier = Modifier.fillMaxWidth(),
style = TangemTheme.typography.body2,
textSizeHeight = true,
)
}
},
action = {
Box(
modifier = Modifier.padding(
start = TangemTheme.dimens.spacing24,
bottom = TangemTheme.dimens.spacing6,
),
contentAlignment = Alignment.CenterEnd,
) {
TextShimmer(
modifier = Modifier.fillMaxWidth(),
style = TangemTheme.typography.body2,
textSizeHeight = true,
)
RectangleShimmer(
modifier = Modifier
.height(TangemTheme.dimens.size16)
.fillMaxWidth(),
radius = TangemTheme.dimens.radius3,
)
}
},
)
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun ContentPreview() {
TangemThemePreview {
SecurityScoreBlock(
state = SecurityScoreUM(
score = 3.5f,
description = "Based on 3 ratings",
onInfoClick = {},
),
)
}
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PreviewPlaceholder() {
TangemThemePreview {
PreviewShimmerContainer(
shimmerContent = {
SecurityScorePlaceHolder(
modifier = Modifier.fillMaxWidth(),
)
},
actualContent = {
ContentPreview()
},
)
}
}

View file

@ -0,0 +1,11 @@
package com.tangem.features.markets.details.impl.ui.entity
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.extensions.TextReference
@Immutable
internal data class InfoPointUM(
val title: TextReference,
val value: String,
val onInfoClick: (() -> Unit)? = null,
)

View file

@ -0,0 +1,11 @@
package com.tangem.features.markets.details.impl.ui.entity
import androidx.compose.runtime.Immutable
import kotlinx.collections.immutable.PersistentList
@Immutable
internal data class InsightsUM(
val h24Info: PersistentList<InfoPointUM>,
val weekInfo: PersistentList<InfoPointUM>,
val monthInfo: PersistentList<InfoPointUM>,
)

View file

@ -0,0 +1,22 @@
package com.tangem.features.markets.details.impl.ui.entity
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.PersistentList
@Immutable
internal data class LinksUM(
val officialLinks: PersistentList<LinkUM>,
val social: PersistentList<LinkUM>,
val repository: PersistentList<LinkUM>,
val blockchainSite: PersistentList<LinkUM>,
val onLinkClick: (LinkUM) -> Unit,
) {
@Immutable
data class LinkUM(
@DrawableRes val iconRes: Int,
val title: TextReference,
val url: String,
)
}

View file

@ -1,5 +1,6 @@
package com.tangem.features.markets.details.impl.ui.entity
import androidx.compose.runtime.Immutable
import com.tangem.common.ui.charts.state.MarketChartDataProducer
import com.tangem.common.ui.charts.state.MarketChartLook
import com.tangem.core.ui.components.marketprice.PriceChangeType
@ -7,7 +8,8 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.markets.PriceChangeInterval
import java.math.BigDecimal
data class MarketsTokenDetailsUM(
@Immutable
internal data class MarketsTokenDetailsUM(
val tokenName: String,
val priceText: String,
val iconUrl: String,
@ -17,8 +19,10 @@ data class MarketsTokenDetailsUM(
val selectedInterval: PriceChangeInterval,
val chartState: ChartState,
val onSelectedIntervalChange: (PriceChangeInterval) -> Unit,
// val info : Information TODO [REDACTED_TASK_KEY]
) {
@Immutable
data class ChartState(
val status: Status,
val dataProducer: MarketChartDataProducer,
@ -26,8 +30,18 @@ data class MarketsTokenDetailsUM(
val onLoadRetryClick: () -> Unit,
val onMarkerPointSelected: (time: BigDecimal?, price: BigDecimal?) -> Unit,
) {
@Immutable
enum class Status {
LOADING, ERROR, DATA
}
}
@Immutable
data class Information(
val insights: InsightsUM?,
val securityScore: SecurityScoreUM?,
val metrics: MetricsUM?,
val pricePerformance: PricePerformanceUM?,
val links: LinksUM?,
)
}

View file

@ -0,0 +1,9 @@
package com.tangem.features.markets.details.impl.ui.entity
import androidx.compose.runtime.Immutable
import kotlinx.collections.immutable.PersistentList
@Immutable
internal data class MetricsUM(
val metrics: PersistentList<InfoPointUM>,
)

View file

@ -0,0 +1,18 @@
package com.tangem.features.markets.details.impl.ui.entity
import androidx.annotation.FloatRange
import androidx.compose.runtime.Immutable
@Immutable
internal data class PricePerformanceUM(
val h24: Value,
val month: Value,
val all: Value,
) {
@Immutable
data class Value(
val low: String,
val high: String,
@FloatRange(from = 0.0, to = 1.0) val indicatorFraction: Float,
)
}

View file

@ -0,0 +1,11 @@
package com.tangem.features.markets.details.impl.ui.entity
import androidx.annotation.FloatRange
import androidx.compose.runtime.Immutable
@Immutable
internal data class SecurityScoreUM(
@FloatRange(from = 0.0, to = 5.0) val score: Float,
val description: String,
val onInfoClick: () -> Unit,
)