Updated on 2026-08-14
This commit is contained in:
parent
459db3d0df
commit
4b21bf989e
29 changed files with 1759 additions and 75 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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?,
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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?,
|
||||
)
|
||||
}
|
||||
|
|
@ -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 {}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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>,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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?,
|
||||
)
|
||||
}
|
||||
|
|
@ -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>,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue