Updated on 2026-08-14
This commit is contained in:
parent
700731a408
commit
c500f45307
59 changed files with 1338 additions and 278 deletions
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.tap.di.domain
|
||||
|
||||
import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase
|
||||
import com.tangem.domain.markets.GetTokenMarketInfoUseCase
|
||||
import com.tangem.domain.markets.GetTokenPriceChartUseCase
|
||||
import com.tangem.domain.markets.repositories.MarketsTokenRepository
|
||||
import dagger.Module
|
||||
|
|
@ -26,4 +27,10 @@ object MarketsDomainModule {
|
|||
fun provideGetTokenPriceChartUseCase(marketsTokenRepository: MarketsTokenRepository): GetTokenPriceChartUseCase {
|
||||
return GetTokenPriceChartUseCase(marketsTokenRepository = marketsTokenRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetTokenMarketInfoUseCase(marketsTokenRepository: MarketsTokenRepository): GetTokenMarketInfoUseCase {
|
||||
return GetTokenMarketInfoUseCase(marketsTokenRepository = marketsTokenRepository)
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ import androidx.compose.material3.Button
|
|||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
|
|
@ -20,6 +21,7 @@ import androidx.compose.ui.text.font.FontWeight
|
|||
import androidx.compose.ui.text.font.resolveAsTypeface
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.patrykandpatrick.vico.compose.cartesian.CartesianChartHost
|
||||
import com.patrykandpatrick.vico.compose.cartesian.axis.rememberAxisGuidelineComponent
|
||||
|
|
@ -71,7 +73,6 @@ private const val GUIDELINES_COUNT = 3
|
|||
* @param splitChartSegmentColor The color of the grayed by marker chart segment.
|
||||
* @param backgroundSplitChartSegmentColorAlpha The alpha of the background the [splitChartSegmentColor]
|
||||
* @param backgroundColorAlpha The alpha of the background color of the chart.
|
||||
* @param noChartContent A composable function that defines the content to be displayed when there is no data to display.
|
||||
*/
|
||||
@Composable
|
||||
fun MarketChart(
|
||||
|
|
@ -108,7 +109,7 @@ fun MarketChart(
|
|||
|
||||
// we need to calculate what the overall height should be in order to get the correct height of the graph
|
||||
val bottomAxisHeight = with(LocalDensity.current) {
|
||||
TangemTheme.typography.caption2.fontSize.toPx().toInt() + TangemTheme.dimens.spacing26.toPx().toInt()
|
||||
getMarketChartBottomAxisHeight().toPx().toInt()
|
||||
}
|
||||
|
||||
CartesianChartHost(
|
||||
|
|
@ -120,6 +121,10 @@ fun MarketChart(
|
|||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
// Sometimes the chart is not drawn correctly (ex. in LazyLayout), so we need to force the redraw
|
||||
.drawBehind {
|
||||
state.markerFraction
|
||||
},
|
||||
chart = chart,
|
||||
modelProducer = state.modelProducer,
|
||||
|
|
@ -129,6 +134,13 @@ fun MarketChart(
|
|||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getMarketChartBottomAxisHeight(): Dp {
|
||||
return with(LocalDensity.current) {
|
||||
TangemTheme.typography.caption2.fontSize.toDp() + TangemTheme.dimens.spacing26
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun rememberMarketVisibilityListener(
|
||||
canvasWidth: Int,
|
||||
|
|
|
|||
|
|
@ -238,7 +238,7 @@ private data class Area(
|
|||
}
|
||||
}
|
||||
|
||||
inline fun <T> List<T>.fastForEach(action: (T) -> Unit) {
|
||||
private inline fun <T> List<T>.fastForEach(action: (T) -> Unit) {
|
||||
for (index in indices) {
|
||||
val item = get(index)
|
||||
action(item)
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ interface TangemTechMarketsApi {
|
|||
suspend fun getCoinMarketData(
|
||||
@Path("coin_id") coinId: String,
|
||||
@Query("currency") currency: String,
|
||||
@Query("language") language: String,
|
||||
): ApiResponse<TokenMarketInfoResponse>
|
||||
|
||||
@GET("coins/{coin_id}/history")
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ data class TokenMarketInfoResponse(
|
|||
@Json(name = "full_description")
|
||||
val fullDescription: String?,
|
||||
@Json(name = "insights")
|
||||
val insights: List<Insight>?,
|
||||
val insights: Insights?,
|
||||
@Json(name = "metrics")
|
||||
val metrics: Metrics?,
|
||||
@Json(name = "links")
|
||||
|
|
@ -32,33 +32,33 @@ data class TokenMarketInfoResponse(
|
|||
|
||||
data class PriceChangePercentage(
|
||||
@Json(name = "24h")
|
||||
val day: Int?,
|
||||
val day: BigDecimal?,
|
||||
@Json(name = "1w")
|
||||
val week: Int?,
|
||||
val week: BigDecimal?,
|
||||
@Json(name = "1m")
|
||||
val month: Int?,
|
||||
val month: BigDecimal?,
|
||||
@Json(name = "3m")
|
||||
val threeMonths: Int?,
|
||||
val threeMonths: BigDecimal?,
|
||||
@Json(name = "6m")
|
||||
val sixMonths: Int?,
|
||||
val sixMonths: BigDecimal?,
|
||||
@Json(name = "1y")
|
||||
val year: Int?,
|
||||
val year: BigDecimal?,
|
||||
@Json(name = "all_time")
|
||||
val allTime: Int?,
|
||||
val allTime: BigDecimal?,
|
||||
)
|
||||
|
||||
data class Network(
|
||||
@Json(name = "network_id")
|
||||
val networkId: String,
|
||||
@Json(name = "exchangeable")
|
||||
val exchangeable: Boolean,
|
||||
val exchangeable: Boolean = false,
|
||||
@Json(name = "contract_address")
|
||||
val contractAddress: String,
|
||||
val contractAddress: String?,
|
||||
@Json(name = "decimal_count")
|
||||
val decimalCount: Int,
|
||||
val decimalCount: Int?,
|
||||
)
|
||||
|
||||
data class Insight(
|
||||
data class Insights(
|
||||
@Json(name = "holders_change")
|
||||
val holdersChange: Change?,
|
||||
@Json(name = "liquidity_change")
|
||||
|
|
@ -71,11 +71,11 @@ data class TokenMarketInfoResponse(
|
|||
|
||||
data class Change(
|
||||
@Json(name = "24h")
|
||||
val day: Int?,
|
||||
val day: BigDecimal?,
|
||||
@Json(name = "1w")
|
||||
val week: Int?,
|
||||
val week: BigDecimal?,
|
||||
@Json(name = "1m")
|
||||
val month: Int?,
|
||||
val month: BigDecimal?,
|
||||
)
|
||||
|
||||
data class Metrics(
|
||||
|
|
@ -123,9 +123,9 @@ data class TokenMarketInfoResponse(
|
|||
)
|
||||
|
||||
data class Range(
|
||||
@Json(name = "low")
|
||||
val low: Int?,
|
||||
@Json(name = "high")
|
||||
val high: Int?,
|
||||
@Json(name = "low_price")
|
||||
val low: BigDecimal?,
|
||||
@Json(name = "high_price")
|
||||
val high: BigDecimal?,
|
||||
)
|
||||
}
|
||||
|
|
@ -30,6 +30,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> TangemBottomSheet(
|
|||
titleAction: TopAppBarButtonUM? = null,
|
||||
containerColor: Color = TangemTheme.colors.background.primary,
|
||||
addBottomInsets: Boolean = true,
|
||||
skipPartiallyExpanded: Boolean = true,
|
||||
crossinline content: @Composable ColumnScope.(T) -> Unit,
|
||||
) {
|
||||
TangemBottomSheet(
|
||||
|
|
@ -37,6 +38,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> TangemBottomSheet(
|
|||
containerColor = containerColor,
|
||||
addBottomInsets = addBottomInsets,
|
||||
title = { TangemBottomSheetTitle(title = titleText, endButton = titleAction) },
|
||||
skipPartiallyExpanded = skipPartiallyExpanded,
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
|
@ -49,6 +51,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> TangemBottomSheet(
|
|||
config: TangemBottomSheetConfig,
|
||||
containerColor: Color = TangemTheme.colors.background.primary,
|
||||
addBottomInsets: Boolean = true,
|
||||
skipPartiallyExpanded: Boolean = true,
|
||||
crossinline title: @Composable BoxScope.(T) -> Unit = {},
|
||||
crossinline content: @Composable ColumnScope.(T) -> Unit,
|
||||
) {
|
||||
|
|
@ -61,6 +64,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> TangemBottomSheet(
|
|||
addBottomInsets = addBottomInsets,
|
||||
title = title,
|
||||
content = content,
|
||||
skipPartiallyExpanded = skipPartiallyExpanded,
|
||||
)
|
||||
} else {
|
||||
DefaultBottomSheet<T>(
|
||||
|
|
@ -69,6 +73,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> TangemBottomSheet(
|
|||
addBottomInsets = addBottomInsets,
|
||||
title = title,
|
||||
content = content,
|
||||
skipPartiallyExpanded = skipPartiallyExpanded,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -79,11 +84,12 @@ inline fun <reified T : TangemBottomSheetConfigContent> DefaultBottomSheet(
|
|||
config: TangemBottomSheetConfig,
|
||||
containerColor: Color,
|
||||
addBottomInsets: Boolean,
|
||||
skipPartiallyExpanded: Boolean = true,
|
||||
crossinline title: @Composable (BoxScope.(T) -> Unit),
|
||||
crossinline content: @Composable (ColumnScope.(T) -> Unit),
|
||||
) {
|
||||
var isVisible by remember { mutableStateOf(value = config.isShow) }
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = skipPartiallyExpanded)
|
||||
|
||||
if (isVisible && config.content is T) {
|
||||
BasicBottomSheet<T>(
|
||||
|
|
@ -111,6 +117,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> PreviewBottomSheet(
|
|||
config: TangemBottomSheetConfig,
|
||||
containerColor: Color,
|
||||
addBottomInsets: Boolean,
|
||||
skipPartiallyExpanded: Boolean = true,
|
||||
crossinline title: @Composable (BoxScope.(T) -> Unit),
|
||||
crossinline content: @Composable (ColumnScope.(T) -> Unit),
|
||||
) {
|
||||
|
|
@ -118,7 +125,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> PreviewBottomSheet(
|
|||
modifier = Modifier.width(360.dp),
|
||||
config = config,
|
||||
sheetState = SheetState(
|
||||
skipPartiallyExpanded = true,
|
||||
skipPartiallyExpanded = skipPartiallyExpanded,
|
||||
initialValue = Expanded,
|
||||
density = LocalDensity.current,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
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.animation.core.*
|
||||
import androidx.compose.runtime.*
|
||||
|
||||
@Immutable
|
||||
|
|
@ -24,10 +21,6 @@ object TangemAnimations {
|
|||
|
||||
@Immutable
|
||||
object TransitionSpecs {
|
||||
|
||||
@Stable
|
||||
val textChange: ContentTransform =
|
||||
fadeIn(animationSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy)) togetherWith
|
||||
fadeOut(animationSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy))
|
||||
// TODO add more transition specs
|
||||
}
|
||||
}
|
||||
|
|
@ -117,6 +117,7 @@ object BigDecimalFormatter {
|
|||
fiatAmount: BigDecimal?,
|
||||
fiatCurrencyCode: String,
|
||||
fiatCurrencySymbol: String,
|
||||
decimals: Int = FIAT_MARKET_DEFAULT_DIGITS,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
): String {
|
||||
if (fiatAmount == null) return EMPTY_BALANCE_SIGN
|
||||
|
|
@ -124,8 +125,8 @@ object BigDecimalFormatter {
|
|||
val formatterCurrency = getCurrency(fiatCurrencyCode)
|
||||
val formatter = NumberFormat.getCurrencyInstance(locale).apply {
|
||||
currency = formatterCurrency
|
||||
maximumFractionDigits = FIAT_MARKET_DEFAULT_DIGITS
|
||||
minimumFractionDigits = FIAT_MARKET_DEFAULT_DIGITS
|
||||
maximumFractionDigits = decimals
|
||||
minimumFractionDigits = decimals
|
||||
roundingMode = RoundingMode.HALF_UP
|
||||
}
|
||||
|
||||
|
|
@ -250,40 +251,27 @@ object BigDecimalFormatter {
|
|||
/**
|
||||
* "123456.6" -> "$123.457K"
|
||||
* "12345.6" -> "$123.046K"
|
||||
* Negative amount is not supported
|
||||
* @param threeDigitsMethod if true, will format the amount always with 3 significant digits
|
||||
* @param scale the number of digits to the right of the decimal point
|
||||
*/
|
||||
@Suppress("MagicNumber")
|
||||
fun formatCompactAmount(
|
||||
amount: BigDecimal,
|
||||
fun formatCompactFiatAmount(
|
||||
amount: BigDecimal?,
|
||||
fiatCurrencyCode: String,
|
||||
fiatCurrencySymbol: String,
|
||||
threeDigitsMethod: Boolean = false,
|
||||
scale: Int = 0,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
): String {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||
return BigDecimalFormatterCompat.formatCompactAmountNoLocaleContext(
|
||||
amount = amount,
|
||||
fiatCurrencyCode = fiatCurrencyCode,
|
||||
fiatCurrencySymbol = fiatCurrencySymbol,
|
||||
locale = locale,
|
||||
)
|
||||
}
|
||||
if (amount == null) return EMPTY_BALANCE_SIGN
|
||||
|
||||
val scaledAmount = amount.setScale(0, RoundingMode.HALF_UP)
|
||||
val digitsCount = scaledAmount.longValueExact().toString().count()
|
||||
val digitsToFormat = 6 - when (digitsCount % 3) {
|
||||
0 -> 0
|
||||
1 -> 2
|
||||
else -> 1
|
||||
}
|
||||
|
||||
val formatter = CompactDecimalFormat.getInstance(
|
||||
locale,
|
||||
CompactDecimalFormat.CompactStyle.SHORT,
|
||||
).apply {
|
||||
minimumSignificantDigits = 4
|
||||
maximumSignificantDigits = digitsToFormat
|
||||
}
|
||||
|
||||
val rawAmount = formatter.format(amount.setScale(0, RoundingMode.HALF_UP))
|
||||
val rawAmount = formatCompactAmount(
|
||||
amount = amount,
|
||||
locale = locale,
|
||||
threeDigitsMethod = threeDigitsMethod,
|
||||
scale = scale,
|
||||
)
|
||||
|
||||
return addCurrencySymbolToStringAmount(
|
||||
amount = rawAmount,
|
||||
|
|
@ -293,5 +281,53 @@ object BigDecimalFormatter {
|
|||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* "123456.6" -> "123.457K"
|
||||
* "12345.6" -> "123.046K"
|
||||
* Negative amount is not supported
|
||||
* @param threeDigitsMethod if true, will format the amount always with 3 significant digits
|
||||
* @param scale the number of digits to the right of the decimal point
|
||||
*/
|
||||
@Suppress("MagicNumber")
|
||||
fun formatCompactAmount(
|
||||
amount: BigDecimal,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
threeDigitsMethod: Boolean = false,
|
||||
scale: Int = 0,
|
||||
): String {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||
return BigDecimalFormatterCompat.formatCompactAmountNoLocaleContext(amount = amount)
|
||||
}
|
||||
|
||||
if (threeDigitsMethod) {
|
||||
val scaledAmount = amount.setScale(scale, RoundingMode.HALF_UP)
|
||||
val digitsCount = scaledAmount.longValueExact().toString().count()
|
||||
val digitsToFormat = 6 - when (digitsCount % 3) {
|
||||
0 -> 0
|
||||
1 -> 2
|
||||
else -> 1
|
||||
}
|
||||
|
||||
val formatter = CompactDecimalFormat.getInstance(
|
||||
locale,
|
||||
CompactDecimalFormat.CompactStyle.SHORT,
|
||||
).apply {
|
||||
minimumSignificantDigits = 4
|
||||
maximumSignificantDigits = digitsToFormat
|
||||
}
|
||||
|
||||
return formatter.format(amount.setScale(scale, RoundingMode.HALF_UP))
|
||||
} else {
|
||||
val value = amount.setScale(scale, RoundingMode.HALF_UP)
|
||||
|
||||
val formatter = CompactDecimalFormat.getInstance(
|
||||
locale,
|
||||
CompactDecimalFormat.CompactStyle.SHORT,
|
||||
)
|
||||
|
||||
return formatter.format(value)
|
||||
}
|
||||
}
|
||||
|
||||
private fun BigDecimal.isLessThanThreshold() = this > BigDecimal.ZERO && this < FIAT_FORMAT_THRESHOLD
|
||||
}
|
||||
|
|
@ -7,16 +7,32 @@ import java.util.Locale
|
|||
internal object BigDecimalFormatterCompat {
|
||||
|
||||
/**
|
||||
* Formats value as [BigDecimalFormatter.formatCompactAmount] does using only "T","B","M","K" suffixes
|
||||
* Formats value as [BigDecimalFormatter.formatCompactFiatAmount] does using only "T","B","M","K" suffixes
|
||||
* Used for < API24 compatibility
|
||||
*/
|
||||
@Suppress("MagicNumber", "UnnecessaryParentheses")
|
||||
fun formatCompactAmountNoLocaleContext(
|
||||
fun formatCompactFiatAmountNoLocaleContext(
|
||||
amount: BigDecimal,
|
||||
fiatCurrencyCode: String,
|
||||
fiatCurrencySymbol: String,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
): String {
|
||||
val formatted = formatCompactAmountNoLocaleContext(amount)
|
||||
|
||||
return BigDecimalFormatter.addCurrencySymbolToStringAmount(
|
||||
amount = formatted,
|
||||
fiatCurrencyCode = fiatCurrencyCode,
|
||||
fiatCurrencySymbol = fiatCurrencySymbol,
|
||||
locale = locale,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats value as [BigDecimalFormatter.formatCompactAmount] does using only "T","B","M","K" suffixes
|
||||
* Used for < API24 compatibility
|
||||
*/
|
||||
@Suppress("MagicNumber", "UnnecessaryParentheses")
|
||||
fun formatCompactAmountNoLocaleContext(amount: BigDecimal): String {
|
||||
val value = amount.setScale(0, RoundingMode.HALF_UP).longValueExact()
|
||||
|
||||
val formatted = when {
|
||||
|
|
@ -42,11 +58,6 @@ internal object BigDecimalFormatterCompat {
|
|||
else -> return value.toString()
|
||||
}
|
||||
|
||||
return BigDecimalFormatter.addCurrencySymbolToStringAmount(
|
||||
amount = formatted,
|
||||
fiatCurrencyCode = fiatCurrencyCode,
|
||||
fiatCurrencySymbol = fiatCurrencySymbol,
|
||||
locale = locale,
|
||||
)
|
||||
return formatted
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.core.ui.utils
|
||||
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
|
||||
fun Modifier.disableNestedScroll(): Modifier = nestedScroll(DisableParentConnection)
|
||||
|
||||
private object DisableParentConnection : NestedScrollConnection {
|
||||
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
|
||||
return available.copy(x = 0f)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package com.tangem.utils
|
||||
|
||||
import java.util.Locale
|
||||
|
||||
object SupportedLanguages {
|
||||
const val ENGLISH = "en"
|
||||
const val RUSSIAN = "ru"
|
||||
const val GERMAN = "de"
|
||||
const val FRANCH = "fr"
|
||||
const val ITALIAN = "it"
|
||||
const val JAPANESE = "ja"
|
||||
const val UKRAINIAN = "uk"
|
||||
const val CHINESE = "uk"
|
||||
|
||||
val supportedLangugeCodes = listOf(
|
||||
ENGLISH,
|
||||
RUSSIAN,
|
||||
GERMAN,
|
||||
FRANCH,
|
||||
ITALIAN,
|
||||
JAPANESE,
|
||||
UKRAINIAN,
|
||||
CHINESE,
|
||||
)
|
||||
|
||||
fun getCurrentSupportedLanguageCode(): String {
|
||||
val locale = Locale.getDefault()
|
||||
|
||||
return if (supportedLangugeCodes.contains(locale.language)) {
|
||||
locale.language
|
||||
} else {
|
||||
ENGLISH
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -108,10 +108,15 @@ internal class DefaultMarketsTokenRepository(
|
|||
return tokenChartConverter.convert(interval, response.getOrThrow())
|
||||
}
|
||||
|
||||
override suspend fun getTokenInfo(fiatCurrencyCode: String, tokenId: String): TokenMarketInfo {
|
||||
override suspend fun getTokenInfo(
|
||||
fiatCurrencyCode: String,
|
||||
tokenId: String,
|
||||
languageCode: String,
|
||||
): TokenMarketInfo {
|
||||
val response = marketsApi.getCoinMarketData(
|
||||
currency = fiatCurrencyCode,
|
||||
coinId = tokenId,
|
||||
language = languageCode,
|
||||
)
|
||||
|
||||
return TokenMarketInfoConverter().convert(response.getOrThrow())
|
||||
|
|
|
|||
|
|
@ -50,15 +50,13 @@ internal class TokenMarketInfoConverter : Converter<TokenMarketInfoResponse, Tok
|
|||
}
|
||||
|
||||
@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.Insights.convert(): TokenMarketInfo.Insights {
|
||||
return TokenMarketInfo.Insights(
|
||||
holdersChange = holdersChange?.convert(),
|
||||
liquidityChange = liquidityChange?.convert(),
|
||||
buyPressureChange = buyPressureChange?.convert(),
|
||||
experiencedBuyerChange = experiencedBuyerChange?.convert(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun TokenMarketInfoResponse.Change.convert(): TokenMarketInfo.Change {
|
||||
|
|
|
|||
|
|
@ -18,4 +18,5 @@ dependencies {
|
|||
|
||||
implementation(deps.kotlin.serialization)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.core.utils)
|
||||
}
|
||||
|
|
@ -11,29 +11,29 @@ data class TokenMarketInfo(
|
|||
val networks: List<Network>?,
|
||||
val shortDescription: String?,
|
||||
val fullDescription: String?,
|
||||
val insights: List<Insight>?,
|
||||
val insights: Insights?,
|
||||
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?,
|
||||
val day: BigDecimal?,
|
||||
val week: BigDecimal?,
|
||||
val month: BigDecimal?,
|
||||
val threeMonths: BigDecimal?,
|
||||
val sixMonths: BigDecimal?,
|
||||
val year: BigDecimal?,
|
||||
val allTime: BigDecimal?,
|
||||
)
|
||||
|
||||
data class Network(
|
||||
val networkId: String,
|
||||
val exchangeable: Boolean,
|
||||
val contractAddress: String,
|
||||
val decimalCount: Int,
|
||||
val contractAddress: String?,
|
||||
val decimalCount: Int?,
|
||||
)
|
||||
|
||||
data class Insight(
|
||||
data class Insights(
|
||||
val holdersChange: Change?,
|
||||
val liquidityChange: Change?,
|
||||
val buyPressureChange: Change?,
|
||||
|
|
@ -41,9 +41,9 @@ data class TokenMarketInfo(
|
|||
)
|
||||
|
||||
data class Change(
|
||||
val day: Int?,
|
||||
val week: Int?,
|
||||
val month: Int?,
|
||||
val day: BigDecimal?,
|
||||
val week: BigDecimal?,
|
||||
val month: BigDecimal?,
|
||||
)
|
||||
|
||||
data class Metrics(
|
||||
|
|
@ -75,7 +75,7 @@ data class TokenMarketInfo(
|
|||
)
|
||||
|
||||
data class Range(
|
||||
val low: Int?,
|
||||
val high: Int?,
|
||||
val low: BigDecimal?,
|
||||
val high: BigDecimal?,
|
||||
)
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package com.tangem.domain.markets
|
|||
import arrow.core.Either
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.repositories.MarketsTokenRepository
|
||||
import com.tangem.utils.SupportedLanguages
|
||||
|
||||
class GetTokenMarketInfoUseCase(
|
||||
private val marketsTokenRepository: MarketsTokenRepository,
|
||||
|
|
@ -13,6 +14,7 @@ class GetTokenMarketInfoUseCase(
|
|||
marketsTokenRepository.getTokenInfo(
|
||||
fiatCurrencyCode = appCurrency.code,
|
||||
tokenId = tokenId,
|
||||
languageCode = SupportedLanguages.getCurrentSupportedLanguageCode(),
|
||||
)
|
||||
}.mapLeft {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,5 +12,5 @@ interface MarketsTokenRepository {
|
|||
|
||||
suspend fun getChart(fiatCurrencyCode: String, interval: PriceChangeInterval, tokenId: String): TokenChart
|
||||
|
||||
suspend fun getTokenInfo(fiatCurrencyCode: String, tokenId: String): TokenMarketInfo
|
||||
suspend fun getTokenInfo(fiatCurrencyCode: String, tokenId: String, languageCode: String): TokenMarketInfo
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@ android {
|
|||
dependencies {
|
||||
/* Project - API */
|
||||
api(projects.features.markets.api)
|
||||
implementation(projects.core.navigation)
|
||||
|
||||
/* Domain */
|
||||
implementation(projects.domain.markets)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@ import arrow.core.getOrElse
|
|||
import com.tangem.common.ui.charts.state.*
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
|
|
@ -13,11 +16,16 @@ import com.tangem.core.ui.utils.DateTimeFormatters
|
|||
import com.tangem.core.ui.utils.toTimeFormat
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.GetTokenMarketInfoUseCase
|
||||
import com.tangem.domain.markets.GetTokenPriceChartUseCase
|
||||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import com.tangem.features.markets.details.api.MarketsTokenDetailsComponent
|
||||
import com.tangem.features.markets.details.impl.ui.entity.MarketsTokenDetailsUM
|
||||
import com.tangem.features.markets.details.impl.model.converters.DescriptionConverter
|
||||
import com.tangem.features.markets.details.impl.model.converters.TokenMarketInfoConverter
|
||||
import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent
|
||||
import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM
|
||||
import com.tangem.features.markets.impl.R
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
import com.tangem.utils.coroutines.saveIn
|
||||
|
|
@ -28,12 +36,15 @@ import java.math.BigDecimal
|
|||
import java.math.RoundingMode
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LargeClass")
|
||||
@Stable
|
||||
internal class MarketsTokenDetailsModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
private val getTokenPriceChartUseCase: GetTokenPriceChartUseCase,
|
||||
private val getTokenMarketInfoUseCase: GetTokenMarketInfoUseCase,
|
||||
private val urlOpener: UrlOpener,
|
||||
) : Model() {
|
||||
|
||||
val params = paramsContainer.require<MarketsTokenDetailsComponent.Params>()
|
||||
|
|
@ -47,6 +58,21 @@ internal class MarketsTokenDetailsModel @Inject constructor(
|
|||
initialValue = params.appCurrency,
|
||||
)
|
||||
|
||||
private val infoConverter = TokenMarketInfoConverter(
|
||||
appCurrency = Provider { currentAppCurrency.value },
|
||||
onInfoClick = {
|
||||
showInfoBottomSheet(it)
|
||||
},
|
||||
onLinkClick = {
|
||||
urlOpener.openUrl(it.url)
|
||||
},
|
||||
)
|
||||
private val descriptionConverter = DescriptionConverter(
|
||||
onReadModeClicked = {
|
||||
showInfoBottomSheet(it)
|
||||
},
|
||||
)
|
||||
|
||||
private val chartDataProducer = MarketChartDataProducer.build(dispatcher = dispatchers.default) {
|
||||
chartData = MarketChartData.NoData.Loading
|
||||
|
||||
|
|
@ -89,19 +115,39 @@ internal class MarketsTokenDetailsModel @Inject constructor(
|
|||
chartState = MarketsTokenDetailsUM.ChartState(
|
||||
dataProducer = chartDataProducer,
|
||||
chartLook = MarketChartLook(),
|
||||
onLoadRetryClick = {},
|
||||
onLoadRetryClick = ::onLoadRetryClicked,
|
||||
status = MarketsTokenDetailsUM.ChartState.Status.LOADING,
|
||||
onMarkerPointSelected = ::onMarkerPointSelected,
|
||||
),
|
||||
selectedInterval = PriceChangeInterval.H24,
|
||||
onSelectedIntervalChange = ::onSelectedIntervalChange,
|
||||
body = MarketsTokenDetailsUM.Body.Loading,
|
||||
infoBottomSheet = TangemBottomSheetConfig(
|
||||
isShow = false,
|
||||
onDismissRequest = {},
|
||||
content = TangemBottomSheetConfigContent.Empty,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
private val loadChartJobHolder = JobHolder()
|
||||
|
||||
init {
|
||||
loadChart(PriceChangeInterval.H24)
|
||||
// reload screen if currency changed
|
||||
modelScope.launch {
|
||||
currentAppCurrency
|
||||
.filter { it != params.appCurrency }
|
||||
.collectLatest {
|
||||
initialLoad()
|
||||
}
|
||||
}
|
||||
|
||||
initialLoad()
|
||||
}
|
||||
|
||||
private fun initialLoad() {
|
||||
loadChart(state.value.selectedInterval)
|
||||
loadInfo()
|
||||
}
|
||||
|
||||
private fun onSelectedIntervalChange(interval: PriceChangeInterval) {
|
||||
|
|
@ -175,12 +221,60 @@ internal class MarketsTokenDetailsModel @Inject constructor(
|
|||
chartState = it.chartState.copy(
|
||||
status = MarketsTokenDetailsUM.ChartState.Status.ERROR,
|
||||
),
|
||||
body = if (it.body is MarketsTokenDetailsUM.Body.Error) {
|
||||
MarketsTokenDetailsUM.Body.Nothing
|
||||
} else {
|
||||
it.body
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}.saveIn(loadChartJobHolder)
|
||||
}
|
||||
|
||||
private fun loadInfo() {
|
||||
state.update {
|
||||
it.copy(
|
||||
body = MarketsTokenDetailsUM.Body.Loading,
|
||||
)
|
||||
}
|
||||
|
||||
modelScope.launch {
|
||||
val tokenMarketInfo = getTokenMarketInfoUseCase(
|
||||
appCurrency = currentAppCurrency.value,
|
||||
tokenId = params.token.id,
|
||||
)
|
||||
|
||||
tokenMarketInfo.fold(
|
||||
ifRight = { result ->
|
||||
state.update {
|
||||
it.copy(
|
||||
body = MarketsTokenDetailsUM.Body.Content(
|
||||
description = descriptionConverter.convert(result),
|
||||
infoBlocks = infoConverter.convert(result),
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
ifLeft = {
|
||||
state.update {
|
||||
if (it.chartState.status == MarketsTokenDetailsUM.ChartState.Status.DATA) {
|
||||
it.copy(
|
||||
body = MarketsTokenDetailsUM.Body.Error(
|
||||
onLoadRetryClick = ::onLoadRetryClicked,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
it.copy(
|
||||
body = MarketsTokenDetailsUM.Body.Nothing,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getFormatterByInterval(interval: PriceChangeInterval): (BigDecimal) -> String {
|
||||
return when (interval) {
|
||||
PriceChangeInterval.H24 -> { value: BigDecimal ->
|
||||
|
|
@ -251,4 +345,40 @@ internal class MarketsTokenDetailsModel @Inject constructor(
|
|||
MarketChartLook.Type.Falling
|
||||
}
|
||||
}
|
||||
|
||||
private fun showInfoBottomSheet(content: InfoBottomSheetContent) {
|
||||
state.update { stateToUpdate ->
|
||||
stateToUpdate.copy(
|
||||
infoBottomSheet = stateToUpdate.infoBottomSheet.copy(
|
||||
isShow = true,
|
||||
onDismissRequest = ::hideInfoBottomSheet,
|
||||
content = content,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun hideInfoBottomSheet() {
|
||||
state.update { stateToUpdate ->
|
||||
stateToUpdate.copy(
|
||||
infoBottomSheet = stateToUpdate.infoBottomSheet.copy(
|
||||
isShow = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onLoadRetryClicked() {
|
||||
val currentState = state.value
|
||||
|
||||
if (currentState.chartState.status == MarketsTokenDetailsUM.ChartState.Status.ERROR) {
|
||||
loadChart(currentState.selectedInterval)
|
||||
}
|
||||
|
||||
if (currentState.body is MarketsTokenDetailsUM.Body.Error ||
|
||||
currentState.body is MarketsTokenDetailsUM.Body.Nothing
|
||||
) {
|
||||
loadInfo()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package com.tangem.features.markets.details.impl.model.converters
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent
|
||||
import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM
|
||||
import com.tangem.features.markets.impl.R
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
@Stable
|
||||
internal class DescriptionConverter(
|
||||
val onReadModeClicked: (InfoBottomSheetContent) -> Unit,
|
||||
) : Converter<TokenMarketInfo, MarketsTokenDetailsUM.Description?> {
|
||||
|
||||
override fun convert(value: TokenMarketInfo): MarketsTokenDetailsUM.Description? {
|
||||
return value.shortDescription?.let { desc ->
|
||||
MarketsTokenDetailsUM.Description(
|
||||
shortDescription = stringReference(desc),
|
||||
fullDescription = value.fullDescription?.let { fullDescription ->
|
||||
stringReference(fullDescription)
|
||||
},
|
||||
onReadMoreClick = {
|
||||
onReadModeClicked(
|
||||
InfoBottomSheetContent(
|
||||
title = resourceReference(
|
||||
R.string.markets_token_details_about_token_title,
|
||||
wrappedList(
|
||||
value.name,
|
||||
),
|
||||
),
|
||||
body = stringReference(value.fullDescription ?: ""),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
package com.tangem.features.markets.details.impl.model.converters
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent
|
||||
import com.tangem.features.markets.details.impl.ui.state.InfoPointUM
|
||||
import com.tangem.features.markets.details.impl.ui.state.InsightsUM
|
||||
import com.tangem.features.markets.impl.R
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.StringsSigns
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Stable
|
||||
internal class InsightsConverter(
|
||||
private val appCurrency: Provider<AppCurrency>,
|
||||
private val onInfoClick: (InfoBottomSheetContent) -> Unit,
|
||||
) : Converter<TokenMarketInfo.Insights, InsightsUM> {
|
||||
|
||||
override fun convert(value: TokenMarketInfo.Insights): InsightsUM {
|
||||
return with(value) {
|
||||
InsightsUM(
|
||||
h24Info = createInfoPointList(
|
||||
experiencedBuyerChange = experiencedBuyerChange?.day,
|
||||
holdersChange = holdersChange?.day,
|
||||
liquidityChange = liquidityChange?.day,
|
||||
buyPressureChange = buyPressureChange?.day,
|
||||
),
|
||||
weekInfo = createInfoPointList(
|
||||
experiencedBuyerChange = experiencedBuyerChange?.week,
|
||||
holdersChange = holdersChange?.week,
|
||||
liquidityChange = liquidityChange?.week,
|
||||
buyPressureChange = buyPressureChange?.week,
|
||||
),
|
||||
monthInfo = createInfoPointList(
|
||||
experiencedBuyerChange = experiencedBuyerChange?.month,
|
||||
holdersChange = holdersChange?.month,
|
||||
liquidityChange = liquidityChange?.month,
|
||||
buyPressureChange = buyPressureChange?.month,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createInfoPointList(
|
||||
experiencedBuyerChange: BigDecimal?,
|
||||
holdersChange: BigDecimal?,
|
||||
liquidityChange: BigDecimal?,
|
||||
buyPressureChange: BigDecimal?,
|
||||
): ImmutableList<InfoPointUM> {
|
||||
return persistentListOf(
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_experienced_buyers),
|
||||
value = experiencedBuyerChange.convertChange(),
|
||||
onInfoClick = {
|
||||
onInfoClick(
|
||||
InfoBottomSheetContent(
|
||||
title = resourceReference(R.string.markets_token_details_experienced_buyers),
|
||||
body = resourceReference(R.string.markets_token_details_experienced_buyers_description),
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_buy_pressure),
|
||||
value = buyPressureChange.convertChange(isFiatValue = true),
|
||||
onInfoClick = {
|
||||
onInfoClick(
|
||||
InfoBottomSheetContent(
|
||||
title = resourceReference(R.string.markets_token_details_buy_pressure),
|
||||
body = resourceReference(R.string.markets_token_details_buy_pressure_description),
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_holders),
|
||||
value = holdersChange.convertChange(),
|
||||
onInfoClick = {
|
||||
onInfoClick(
|
||||
InfoBottomSheetContent(
|
||||
title = resourceReference(R.string.markets_token_details_holders),
|
||||
body = resourceReference(R.string.markets_token_details_holders_description),
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_liquidity),
|
||||
value = liquidityChange.convertChange(),
|
||||
onInfoClick = {
|
||||
onInfoClick(
|
||||
InfoBottomSheetContent(
|
||||
title = resourceReference(R.string.markets_token_details_liquidity),
|
||||
body = resourceReference(R.string.markets_token_details_liquidity_description),
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun BigDecimal?.convertChange(isFiatValue: Boolean = false): String {
|
||||
if (this == null) return StringsSigns.DASH_SIGN
|
||||
|
||||
val value = if (isFiatValue) {
|
||||
val currency = appCurrency()
|
||||
BigDecimalFormatter.formatCompactFiatAmount(
|
||||
amount = this.abs(),
|
||||
fiatCurrencyCode = currency.code,
|
||||
fiatCurrencySymbol = currency.symbol,
|
||||
)
|
||||
} else {
|
||||
BigDecimalFormatter.formatCompactAmount(amount = this.abs())
|
||||
}
|
||||
|
||||
val spacing = if (isFiatValue) " " else ""
|
||||
|
||||
return when {
|
||||
this > BigDecimal.ZERO -> StringsSigns.PLUS + spacing + value
|
||||
this < BigDecimal.ZERO -> StringsSigns.MINUS + spacing + value
|
||||
this == BigDecimal.ZERO -> value
|
||||
else -> StringsSigns.DASH_SIGN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
package com.tangem.features.markets.details.impl.model.converters
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.features.markets.details.impl.ui.state.LinksUM
|
||||
import com.tangem.features.markets.impl.R
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@Stable
|
||||
internal class LinksConverter(
|
||||
private val onLinkClick: (LinksUM.Link) -> Unit,
|
||||
) : Converter<TokenMarketInfo.Links, LinksUM> {
|
||||
|
||||
override fun convert(value: TokenMarketInfo.Links): LinksUM {
|
||||
return LinksUM(
|
||||
officialLinks = value.officialLinks?.map { it.convert() }.orEmpty().toImmutableList(),
|
||||
social = value.social?.map { it.convert() }.orEmpty().toImmutableList(),
|
||||
repository = value.repository?.map { it.convert() }.orEmpty().toImmutableList(),
|
||||
blockchainSite = value.blockchainSite?.map { it.convert() }.orEmpty().toImmutableList(),
|
||||
onLinkClick = onLinkClick,
|
||||
)
|
||||
}
|
||||
|
||||
private fun TokenMarketInfo.Link.convert(): LinksUM.Link {
|
||||
return LinksUM.Link(
|
||||
title = stringReference(title),
|
||||
iconRes = getIconById(id),
|
||||
url = link,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getIconById(id: String?): Int {
|
||||
return when (id) {
|
||||
"linkedin" -> R.drawable.ic_linkedin_24
|
||||
"discord" -> R.drawable.ic_discord_24
|
||||
"youtube" -> R.drawable.ic_youtube_24
|
||||
"telegram" -> R.drawable.ic_telegram_24
|
||||
"github" -> R.drawable.ic_github_24
|
||||
"twitter" -> R.drawable.ic_twitter_24
|
||||
"facebook" -> R.drawable.ic_facebook_24
|
||||
"reddit" -> R.drawable.ic_reddit_24
|
||||
"instagram" -> R.drawable.ic_instagram_24
|
||||
"whitepaper" -> R.drawable.ic_doc_24
|
||||
else -> R.drawable.ic_arrow_top_right_24
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
package com.tangem.features.markets.details.impl.model.converters
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent
|
||||
import com.tangem.features.markets.details.impl.ui.state.InfoPointUM
|
||||
import com.tangem.features.markets.details.impl.ui.state.MetricsUM
|
||||
import com.tangem.features.markets.impl.R
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.StringsSigns
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
import java.text.NumberFormat
|
||||
import java.util.Locale
|
||||
|
||||
@Stable
|
||||
internal class MetricsConverter(
|
||||
private val appCurrency: Provider<AppCurrency>,
|
||||
private val onInfoClick: (InfoBottomSheetContent) -> Unit,
|
||||
) : Converter<TokenMarketInfo.Metrics, MetricsUM> {
|
||||
|
||||
@Suppress("LongMethod")
|
||||
override fun convert(value: TokenMarketInfo.Metrics): MetricsUM {
|
||||
return with(value) {
|
||||
MetricsUM(
|
||||
metrics = persistentListOf(
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_market_capitalization),
|
||||
value = marketCap.formatAmount(),
|
||||
onInfoClick = {
|
||||
onInfoClick(
|
||||
InfoBottomSheetContent(
|
||||
title = resourceReference(R.string.markets_token_details_market_capitalization),
|
||||
body = resourceReference(
|
||||
R.string.markets_token_details_market_capitalization_description,
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_market_rating),
|
||||
value = marketRating?.toString() ?: StringsSigns.DASH_SIGN,
|
||||
onInfoClick = {
|
||||
onInfoClick(
|
||||
InfoBottomSheetContent(
|
||||
title = resourceReference(R.string.markets_token_details_market_rating),
|
||||
body = resourceReference(R.string.markets_token_details_market_rating_description),
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_trading_volume),
|
||||
value = volume24h.formatAmount(),
|
||||
onInfoClick = {
|
||||
onInfoClick(
|
||||
InfoBottomSheetContent(
|
||||
title = resourceReference(R.string.markets_token_details_trading_volume),
|
||||
body = resourceReference(
|
||||
R.string.markets_token_details_trading_volume_24h_description,
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_fully_diluted_valuation),
|
||||
value = fullyDilutedValuation.formatAmount(),
|
||||
onInfoClick = {
|
||||
onInfoClick(
|
||||
InfoBottomSheetContent(
|
||||
title = resourceReference(R.string.markets_token_details_fully_diluted_valuation),
|
||||
body = resourceReference(
|
||||
R.string.markets_token_details_fully_diluted_valuation_description,
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_circulating_supply),
|
||||
value = circulatingSupply.formatAmount(crypto = true),
|
||||
onInfoClick = {
|
||||
onInfoClick(
|
||||
InfoBottomSheetContent(
|
||||
title = resourceReference(R.string.markets_token_details_circulating_supply),
|
||||
body = resourceReference(
|
||||
R.string.markets_token_details_circulating_supply_description,
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_total_supply),
|
||||
value = totalSupply.formatAmount(crypto = true),
|
||||
onInfoClick = {
|
||||
onInfoClick(
|
||||
InfoBottomSheetContent(
|
||||
title = resourceReference(R.string.markets_token_details_total_supply),
|
||||
body = resourceReference(R.string.markets_token_details_total_supply_description),
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun BigDecimal?.formatAmount(crypto: Boolean = false): String {
|
||||
return if (crypto) {
|
||||
val formatter = NumberFormat.getNumberInstance(Locale.getDefault()).apply {
|
||||
maximumFractionDigits = 0
|
||||
isGroupingUsed = true
|
||||
roundingMode = RoundingMode.HALF_UP
|
||||
}
|
||||
formatter.format(this)
|
||||
} else {
|
||||
val currency = appCurrency()
|
||||
BigDecimalFormatter.formatFiatAmount(
|
||||
fiatAmount = this,
|
||||
fiatCurrencyCode = currency.code,
|
||||
fiatCurrencySymbol = currency.symbol,
|
||||
decimals = 0,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
package com.tangem.features.markets.details.impl.model.converters
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.features.markets.details.impl.ui.state.PricePerformanceUM
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.StringsSigns
|
||||
import com.tangem.utils.converter.Converter
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
|
||||
@Stable
|
||||
internal class PricePerformanceConverter(
|
||||
private val appCurrency: Provider<AppCurrency>,
|
||||
) : Converter<TokenMarketInfo.PricePerformance, PricePerformanceUM> {
|
||||
|
||||
override fun convert(value: TokenMarketInfo.PricePerformance): PricePerformanceUM {
|
||||
return PricePerformanceUM(
|
||||
h24 = value.day.convert(),
|
||||
month = value.month.convert(),
|
||||
all = value.allTime.convert(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun TokenMarketInfo.Range?.convert(): PricePerformanceUM.Value {
|
||||
if (this == null) {
|
||||
return PricePerformanceUM.Value(
|
||||
low = StringsSigns.DASH_SIGN,
|
||||
high = StringsSigns.DASH_SIGN,
|
||||
indicatorFraction = 0f,
|
||||
)
|
||||
}
|
||||
|
||||
return PricePerformanceUM.Value(
|
||||
low = low.convert(),
|
||||
high = high.convert(),
|
||||
indicatorFraction = calculateFraction(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun BigDecimal?.convert(): String {
|
||||
val currency = appCurrency()
|
||||
return BigDecimalFormatter.formatCompactFiatAmount(
|
||||
amount = this,
|
||||
fiatCurrencyCode = currency.code,
|
||||
fiatCurrencySymbol = currency.symbol,
|
||||
)
|
||||
}
|
||||
|
||||
private fun TokenMarketInfo.Range.calculateFraction(): Float {
|
||||
if (low == null || high == null || low == BigDecimal.ZERO) return 0f
|
||||
return (high!! - low!!).divide(low!!, RoundingMode.HALF_UP)
|
||||
.setScale(2, RoundingMode.HALF_UP)
|
||||
.toFloat().coerceAtMost(1f)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package com.tangem.features.markets.details.impl.model.converters
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent
|
||||
import com.tangem.features.markets.details.impl.ui.state.SecurityScoreUM
|
||||
import com.tangem.features.markets.impl.R
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
// TODO implement when backend is ready
|
||||
@Stable
|
||||
internal class SecurityScoreConverter(
|
||||
private val onInfoClick: (InfoBottomSheetContent) -> Unit,
|
||||
) : Converter<Unit, SecurityScoreUM> {
|
||||
|
||||
override fun convert(value: Unit): SecurityScoreUM {
|
||||
return with(value) {
|
||||
SecurityScoreUM(
|
||||
score = 4.7f,
|
||||
description = "Based on 3 ratings",
|
||||
onInfoClick = {
|
||||
onInfoClick(
|
||||
InfoBottomSheetContent(
|
||||
title = resourceReference(R.string.markets_token_details_security_score),
|
||||
body = stringReference("markets_token_details_security_score_description"),
|
||||
// FIXME
|
||||
// resourceReference(R.string.markets_token_details_security_score_description)
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package com.tangem.features.markets.details.impl.model.converters
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent
|
||||
import com.tangem.features.markets.details.impl.ui.state.LinksUM
|
||||
import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
@Stable
|
||||
internal class TokenMarketInfoConverter(
|
||||
appCurrency: Provider<AppCurrency>,
|
||||
onInfoClick: (InfoBottomSheetContent) -> Unit,
|
||||
onLinkClick: (LinksUM.Link) -> Unit,
|
||||
) : Converter<TokenMarketInfo, MarketsTokenDetailsUM.InformationBlocks> {
|
||||
|
||||
private val insightsConverter = InsightsConverter(appCurrency = appCurrency, onInfoClick = onInfoClick)
|
||||
private val securityScoreConverter = SecurityScoreConverter(onInfoClick = onInfoClick)
|
||||
private val metricsConverter = MetricsConverter(appCurrency = appCurrency, onInfoClick = onInfoClick)
|
||||
private val pricePerformanceConverter = PricePerformanceConverter(appCurrency = appCurrency)
|
||||
private val linksConverter = LinksConverter(onLinkClick = onLinkClick)
|
||||
|
||||
override fun convert(value: TokenMarketInfo): MarketsTokenDetailsUM.InformationBlocks {
|
||||
return MarketsTokenDetailsUM.InformationBlocks(
|
||||
insights = value.insights?.let { insightsConverter.convert(it) },
|
||||
securityScore = securityScoreConverter.convert(Unit),
|
||||
metrics = value.metrics?.let { metricsConverter.convert(it) },
|
||||
pricePerformance = value.pricePerformance?.let { pricePerformanceConverter.convert(it) },
|
||||
links = value.links?.let { linksConverter.convert(it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
package com.tangem.features.markets.details.impl.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
|
|
@ -12,12 +14,11 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||
import androidx.compose.ui.unit.Dp
|
||||
import com.tangem.common.ui.charts.state.MarketChartDataProducer
|
||||
import com.tangem.common.ui.charts.state.MarketChartLook
|
||||
import com.tangem.core.ui.components.SpacerH16
|
||||
import com.tangem.core.ui.components.SpacerH32
|
||||
import com.tangem.core.ui.components.SpacerH4
|
||||
import com.tangem.core.ui.components.SpacerW4
|
||||
import com.tangem.core.ui.components.*
|
||||
import com.tangem.core.ui.components.appbar.TangemTopAppBar
|
||||
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons
|
||||
import com.tangem.core.ui.components.currency.icon.CoinIcon
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeInPercent
|
||||
|
|
@ -29,9 +30,12 @@ import com.tangem.core.ui.extensions.stringReference
|
|||
import com.tangem.core.ui.res.LocalMainBottomSheetColor
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.utils.disableNestedScroll
|
||||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import com.tangem.features.markets.details.impl.ui.components.InfoBottomSheet
|
||||
import com.tangem.features.markets.details.impl.ui.components.MarketTokenDetailsChart
|
||||
import com.tangem.features.markets.details.impl.ui.entity.MarketsTokenDetailsUM
|
||||
import com.tangem.features.markets.details.impl.ui.components.tokenMarketDetailsBody
|
||||
import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM
|
||||
import com.tangem.features.markets.impl.R
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
|
|
@ -44,11 +48,13 @@ internal fun MarketsTokenDetailsContent(
|
|||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Content(
|
||||
modifier = modifier,
|
||||
state = state,
|
||||
onBackClick = onBackClick,
|
||||
onHeaderSizeChange = onHeaderSizeChange,
|
||||
modifier = modifier,
|
||||
)
|
||||
|
||||
InfoBottomSheet(config = state.infoBottomSheet)
|
||||
}
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
|
|
@ -61,6 +67,7 @@ private fun Content(
|
|||
) {
|
||||
val backgroundColor = LocalMainBottomSheetColor.current.value
|
||||
val density = LocalDensity.current
|
||||
val bottomBarHeight = with(density) { WindowInsets.systemBars.getBottom(this).toDp() }
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
|
|
@ -78,31 +85,46 @@ private fun Content(
|
|||
title = state.tokenName,
|
||||
startButton = TopAppBarButtonUM.Back(onBackClick),
|
||||
)
|
||||
|
||||
SpacerH4()
|
||||
|
||||
Header(
|
||||
state = state,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
LazyColumn(
|
||||
modifier = Modifier.disableNestedScroll(),
|
||||
contentPadding = PaddingValues(bottom = bottomBarHeight),
|
||||
) {
|
||||
item("header") {
|
||||
Header(
|
||||
state = state,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
item { SpacerH16() }
|
||||
item("intervalSelector") {
|
||||
IntervalSelector(
|
||||
trendInterval = state.selectedInterval,
|
||||
onIntervalClick = state.onSelectedIntervalChange,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
item { SpacerH32() }
|
||||
item(
|
||||
contentType = "chart",
|
||||
) {
|
||||
MarketTokenDetailsChart(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
state = state.chartState,
|
||||
)
|
||||
}
|
||||
item { SpacerH16() }
|
||||
|
||||
SpacerH16()
|
||||
|
||||
IntervalSelector(
|
||||
trendInterval = state.selectedInterval,
|
||||
onIntervalClick = state.onSelectedIntervalChange,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
|
||||
SpacerH32()
|
||||
|
||||
MarketTokenDetailsChart(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
state = state.chartState,
|
||||
)
|
||||
tokenMarketDetailsBody(
|
||||
state = state.body,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -199,6 +221,7 @@ fun PriceChangeInterval.getText(): TextReference {
|
|||
private fun Preview() {
|
||||
TangemThemePreview {
|
||||
Content(
|
||||
modifier = Modifier.background(TangemTheme.colors.background.tertiary),
|
||||
state = MarketsTokenDetailsUM(
|
||||
tokenName = "Token Name",
|
||||
priceText = "Price",
|
||||
|
|
@ -215,6 +238,12 @@ private fun Preview() {
|
|||
),
|
||||
selectedInterval = PriceChangeInterval.H24,
|
||||
onSelectedIntervalChange = { },
|
||||
body = MarketsTokenDetailsUM.Body.Loading,
|
||||
infoBottomSheet = TangemBottomSheetConfig(
|
||||
isShow = false,
|
||||
onDismissRequest = {},
|
||||
content = TangemBottomSheetConfigContent.Empty,
|
||||
),
|
||||
),
|
||||
onHeaderSizeChange = {},
|
||||
onBackClick = {},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,112 @@
|
|||
package com.tangem.features.markets.details.impl.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
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.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.components.TextShimmer
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
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.impl.R
|
||||
|
||||
@Composable
|
||||
internal fun Description(
|
||||
description: TextReference,
|
||||
hasFullDescription: Boolean,
|
||||
onReadMoreClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (hasFullDescription) {
|
||||
val text = buildAnnotatedString {
|
||||
withStyle(SpanStyle(color = TangemTheme.colors.text.secondary)) {
|
||||
append(description.resolveReference())
|
||||
}
|
||||
withStyle(SpanStyle(color = TangemTheme.colors.text.accent)) {
|
||||
append(" " + stringResource(R.string.common_read_more))
|
||||
}
|
||||
}
|
||||
|
||||
ClickableText(
|
||||
modifier = modifier,
|
||||
text = text,
|
||||
style = TangemTheme.typography.body2,
|
||||
) {
|
||||
text.spanStyles.getOrNull(1)?.let { spanStyle ->
|
||||
if (it in spanStyle.start..spanStyle.end) {
|
||||
onReadMoreClick()
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Text(
|
||||
modifier = modifier,
|
||||
text = description.resolveReference(),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun DescriptionPlaceholder(modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
) {
|
||||
TextShimmer(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
style = TangemTheme.typography.body2,
|
||||
textSizeHeight = true,
|
||||
)
|
||||
TextShimmer(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
style = TangemTheme.typography.body2,
|
||||
textSizeHeight = true,
|
||||
)
|
||||
TextShimmer(
|
||||
modifier = Modifier.fillMaxWidth(fraction = 0.8f),
|
||||
style = TangemTheme.typography.body2,
|
||||
textSizeHeight = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun ContentPreview() {
|
||||
TangemThemePreview {
|
||||
Description(
|
||||
description = stringReference(
|
||||
"XRP (XRP) is a cryptocurrency launched in January 2009, where the first " +
|
||||
"genesis block was mined on 9th January 2009",
|
||||
),
|
||||
hasFullDescription = true,
|
||||
onReadMoreClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun PreviewPlaceholder() {
|
||||
TangemThemePreview {
|
||||
PreviewShimmerContainer(
|
||||
actualContent = {
|
||||
ContentPreview()
|
||||
},
|
||||
shimmerContent = {
|
||||
DescriptionPlaceholder()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
package com.tangem.features.markets.details.impl.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.systemBars
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetTitle
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent
|
||||
|
||||
@Composable
|
||||
internal fun InfoBottomSheet(config: TangemBottomSheetConfig) {
|
||||
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
|
||||
|
||||
TangemBottomSheet<InfoBottomSheetContent>(
|
||||
config = config,
|
||||
skipPartiallyExpanded = false,
|
||||
addBottomInsets = false,
|
||||
title = {
|
||||
TangemBottomSheetTitle(title = it.title)
|
||||
},
|
||||
content = {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = TangemTheme.dimens.spacing28),
|
||||
) {
|
||||
Text(
|
||||
text = it.body.resolveReference(),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
)
|
||||
SpacerH(bottomBarHeight)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
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
|
||||
|
|
@ -15,11 +14,10 @@ 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
|
||||
import com.tangem.features.markets.details.impl.ui.state.InfoPointUM
|
||||
|
||||
@Composable
|
||||
internal fun InfoPoint(infoPointUM: InfoPointUM, modifier: Modifier = Modifier) {
|
||||
|
|
@ -42,18 +40,11 @@ internal fun InfoPoint(infoPointUM: InfoPointUM, modifier: Modifier = Modifier)
|
|||
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,
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = infoPointUM.value,
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@ 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.state.InfoPointUM
|
||||
import com.tangem.features.markets.details.impl.ui.state.InsightsUM
|
||||
import com.tangem.features.markets.details.impl.ui.getText
|
||||
import com.tangem.features.markets.impl.R
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ 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 androidx.compose.ui.util.fastForEach
|
||||
import com.tangem.core.ui.components.SmallButtonShimmer
|
||||
import com.tangem.core.ui.components.TextShimmer
|
||||
import com.tangem.core.ui.components.block.information.InformationBlock
|
||||
|
|
@ -21,7 +21,7 @@ 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.details.impl.ui.state.LinksUM
|
||||
import com.tangem.features.markets.impl.R
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
|
@ -69,8 +69,8 @@ internal fun LinksBlock(state: LinksUM, modifier: Modifier = Modifier) {
|
|||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
private fun SubBlock(
|
||||
links: ImmutableList<LinksUM.LinkUM>,
|
||||
onLinkClick: (LinksUM.LinkUM) -> Unit,
|
||||
links: ImmutableList<LinksUM.Link>,
|
||||
onLinkClick: (LinksUM.Link) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
lastBlock: Boolean = false,
|
||||
title: String = "Official links",
|
||||
|
|
@ -166,36 +166,36 @@ private fun ContentPreview() {
|
|||
LinksBlock(
|
||||
state = LinksUM(
|
||||
officialLinks = persistentListOf(
|
||||
LinksUM.LinkUM(
|
||||
LinksUM.Link(
|
||||
title = stringReference("Website"),
|
||||
iconRes = R.drawable.ic_plus_24,
|
||||
url = "https://tangem.com",
|
||||
),
|
||||
LinksUM.LinkUM(
|
||||
LinksUM.Link(
|
||||
title = stringReference("Website"),
|
||||
iconRes = R.drawable.ic_plus_24,
|
||||
url = "https://tangem.com",
|
||||
),
|
||||
LinksUM.LinkUM(
|
||||
LinksUM.Link(
|
||||
title = stringReference("Website"),
|
||||
iconRes = R.drawable.ic_plus_24,
|
||||
url = "https://tangem.com",
|
||||
),
|
||||
),
|
||||
social = persistentListOf(
|
||||
LinksUM.LinkUM(
|
||||
LinksUM.Link(
|
||||
title = stringReference("Twitter"),
|
||||
iconRes = R.drawable.ic_plus_24,
|
||||
url = "https://tangem.com",
|
||||
),
|
||||
LinksUM.LinkUM(
|
||||
LinksUM.Link(
|
||||
title = stringReference("Facebook"),
|
||||
iconRes = R.drawable.ic_plus_24,
|
||||
url = "https://tangem.com",
|
||||
),
|
||||
),
|
||||
repository = persistentListOf(
|
||||
LinksUM.LinkUM(
|
||||
LinksUM.Link(
|
||||
title = stringReference("Github"),
|
||||
iconRes = R.drawable.ic_plus_24,
|
||||
url = "https://tangem.com",
|
||||
|
|
|
|||
|
|
@ -10,11 +10,12 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import com.tangem.common.ui.charts.MarketChart
|
||||
import com.tangem.common.ui.charts.getMarketChartBottomAxisHeight
|
||||
import com.tangem.common.ui.charts.state.MarketChartLook
|
||||
import com.tangem.common.ui.charts.state.rememberMarketChartState
|
||||
import com.tangem.core.ui.res.LocalMainBottomSheetColor
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.markets.details.impl.ui.entity.MarketsTokenDetailsUM
|
||||
import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.components.UnableToLoadData
|
||||
|
||||
@Composable
|
||||
|
|
@ -34,6 +35,7 @@ internal fun MarketTokenDetailsChart(state: MarketsTokenDetailsUM.ChartState, mo
|
|||
)
|
||||
|
||||
val backgroundColor = LocalMainBottomSheetColor.current.value
|
||||
val bottomChartAxisHeight = getMarketChartBottomAxisHeight()
|
||||
|
||||
Box(modifier) {
|
||||
MarketChart(
|
||||
|
|
@ -45,7 +47,8 @@ internal fun MarketTokenDetailsChart(state: MarketsTokenDetailsUM.ChartState, mo
|
|||
Box(
|
||||
Modifier
|
||||
.drawBehind { drawRect(backgroundColor) }
|
||||
.matchParentSize(),
|
||||
.matchParentSize()
|
||||
.padding(bottom = bottomChartAxisHeight),
|
||||
) {
|
||||
when (state.status) {
|
||||
MarketsTokenDetailsUM.ChartState.Status.LOADING -> {
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@ 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.details.impl.ui.state.InfoPointUM
|
||||
import com.tangem.features.markets.details.impl.ui.state.MetricsUM
|
||||
import com.tangem.features.markets.impl.R
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ 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.state.PricePerformanceUM
|
||||
import com.tangem.features.markets.details.impl.ui.getText
|
||||
import com.tangem.features.markets.impl.R
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
|
@ -127,30 +127,18 @@ private fun Content(state: PricePerformanceUM.Value, modifier: Modifier = Modifi
|
|||
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,
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = state.low,
|
||||
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,
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = state.high,
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.End,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ 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.details.impl.ui.state.SecurityScoreUM
|
||||
import com.tangem.features.markets.impl.R
|
||||
import kotlin.math.round
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,142 @@
|
|||
package com.tangem.features.markets.details.impl.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.components.UnableToLoadData
|
||||
|
||||
internal fun LazyListScope.tokenMarketDetailsBody(state: MarketsTokenDetailsUM.Body) {
|
||||
when (state) {
|
||||
MarketsTokenDetailsUM.Body.Loading -> {
|
||||
loading()
|
||||
}
|
||||
is MarketsTokenDetailsUM.Body.Content -> {
|
||||
if (state.description != null) {
|
||||
description(state.description)
|
||||
}
|
||||
|
||||
infoBlocksList(state.infoBlocks)
|
||||
}
|
||||
is MarketsTokenDetailsUM.Body.Error -> {
|
||||
error(state)
|
||||
}
|
||||
MarketsTokenDetailsUM.Body.Nothing -> {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.error(state: MarketsTokenDetailsUM.Body.Error) {
|
||||
item("body-error") {
|
||||
Box(Modifier.fillMaxWidth()) {
|
||||
UnableToLoadData(
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.padding(
|
||||
horizontal = TangemTheme.dimens.spacing16,
|
||||
vertical = TangemTheme.dimens.spacing40,
|
||||
),
|
||||
onRetryClick = state.onLoadRetryClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.description(description: MarketsTokenDetailsUM.Description) {
|
||||
item("description") {
|
||||
Description(
|
||||
modifier = Modifier.blockPaddings(),
|
||||
description = description.shortDescription,
|
||||
hasFullDescription = description.fullDescription != null,
|
||||
onReadMoreClick = description.onReadMoreClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun LazyListScope.infoBlocksList(state: MarketsTokenDetailsUM.InformationBlocks) {
|
||||
if (state.insights != null) {
|
||||
item("insights") {
|
||||
InsightsBlock(
|
||||
modifier = Modifier.blockPaddings(),
|
||||
state = state.insights,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (state.securityScore != null) {
|
||||
item("securityScore") {
|
||||
SecurityScoreBlock(
|
||||
modifier = Modifier.blockPaddings(),
|
||||
state = state.securityScore,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (state.metrics != null) {
|
||||
item("metrics") {
|
||||
MetricsBlock(
|
||||
modifier = Modifier.blockPaddings(),
|
||||
state = state.metrics,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (state.pricePerformance != null) {
|
||||
item("pricePerformance") {
|
||||
PricePerformanceBlock(
|
||||
modifier = Modifier.blockPaddings(),
|
||||
state = state.pricePerformance,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (state.links != null) {
|
||||
item("links") {
|
||||
LinksBlock(
|
||||
modifier = Modifier.blockPaddings(),
|
||||
state = state.links,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.loading() {
|
||||
item("description-loading") {
|
||||
DescriptionPlaceholder(modifier = Modifier.blockPaddings())
|
||||
}
|
||||
|
||||
item("insights-loading") {
|
||||
InsightsBlockPlaceholder(modifier = Modifier.blockPaddings())
|
||||
}
|
||||
|
||||
item("securityScore-loading") {
|
||||
SecurityScorePlaceHolder(modifier = Modifier.blockPaddings())
|
||||
}
|
||||
|
||||
item("metrics-loading") {
|
||||
MetricsBlockPlaceholder(modifier = Modifier.blockPaddings())
|
||||
}
|
||||
|
||||
item("pricePerformance-loading") {
|
||||
PricePerformanceBlockPlaceholder(modifier = Modifier.blockPaddings())
|
||||
}
|
||||
|
||||
item("links-loading") {
|
||||
LinksBlockPlaceholder(modifier = Modifier.blockPaddings())
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Modifier.blockPaddings(): Modifier {
|
||||
return this.padding(
|
||||
start = TangemTheme.dimens.spacing16,
|
||||
end = TangemTheme.dimens.spacing16,
|
||||
bottom = TangemTheme.dimens.spacing12,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
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>,
|
||||
)
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
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,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.tangem.features.markets.details.impl.ui.state
|
||||
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
internal data class InfoBottomSheetContent(
|
||||
val title: TextReference,
|
||||
val body: TextReference,
|
||||
) : TangemBottomSheetConfigContent
|
||||
|
|
@ -1,9 +1,7 @@
|
|||
package com.tangem.features.markets.details.impl.ui.entity
|
||||
package com.tangem.features.markets.details.impl.ui.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
@Immutable
|
||||
internal data class InfoPointUM(
|
||||
val title: TextReference,
|
||||
val value: String,
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.tangem.features.markets.details.impl.ui.state
|
||||
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
internal data class InsightsUM(
|
||||
val h24Info: ImmutableList<InfoPointUM>,
|
||||
val weekInfo: ImmutableList<InfoPointUM>,
|
||||
val monthInfo: ImmutableList<InfoPointUM>,
|
||||
)
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.tangem.features.markets.details.impl.ui.state
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
internal data class LinksUM(
|
||||
val officialLinks: ImmutableList<Link>,
|
||||
val social: ImmutableList<Link>,
|
||||
val repository: ImmutableList<Link>,
|
||||
val blockchainSite: ImmutableList<Link>,
|
||||
val onLinkClick: (Link) -> Unit,
|
||||
) {
|
||||
data class Link(
|
||||
@DrawableRes val iconRes: Int,
|
||||
val title: TextReference,
|
||||
val url: String,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,14 +1,14 @@
|
|||
package com.tangem.features.markets.details.impl.ui.entity
|
||||
package com.tangem.features.markets.details.impl.ui.state
|
||||
|
||||
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.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Immutable
|
||||
internal data class MarketsTokenDetailsUM(
|
||||
val tokenName: String,
|
||||
val priceText: String,
|
||||
|
|
@ -19,10 +19,10 @@ internal data class MarketsTokenDetailsUM(
|
|||
val selectedInterval: PriceChangeInterval,
|
||||
val chartState: ChartState,
|
||||
val onSelectedIntervalChange: (PriceChangeInterval) -> Unit,
|
||||
// val info : Information TODO [REDACTED_TASK_KEY]
|
||||
val infoBottomSheet: TangemBottomSheetConfig,
|
||||
val body: Body,
|
||||
) {
|
||||
|
||||
@Immutable
|
||||
data class ChartState(
|
||||
val status: Status,
|
||||
val dataProducer: MarketChartDataProducer,
|
||||
|
|
@ -30,18 +30,39 @@ internal data class MarketsTokenDetailsUM(
|
|||
val onLoadRetryClick: () -> Unit,
|
||||
val onMarkerPointSelected: (time: BigDecimal?, price: BigDecimal?) -> Unit,
|
||||
) {
|
||||
@Immutable
|
||||
enum class Status {
|
||||
LOADING, ERROR, DATA
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class Information(
|
||||
data class InformationBlocks(
|
||||
val insights: InsightsUM?,
|
||||
val securityScore: SecurityScoreUM?,
|
||||
val metrics: MetricsUM?,
|
||||
val pricePerformance: PricePerformanceUM?,
|
||||
val links: LinksUM?,
|
||||
)
|
||||
|
||||
@Immutable
|
||||
sealed interface Body {
|
||||
|
||||
data class Error(
|
||||
val onLoadRetryClick: () -> Unit,
|
||||
) : Body
|
||||
|
||||
data object Loading : Body
|
||||
|
||||
data class Content(
|
||||
val description: Description?,
|
||||
val infoBlocks: InformationBlocks,
|
||||
) : Body
|
||||
|
||||
data object Nothing : Body
|
||||
}
|
||||
|
||||
data class Description(
|
||||
val shortDescription: TextReference,
|
||||
val fullDescription: TextReference?,
|
||||
val onReadMoreClick: () -> Unit,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,9 +1,7 @@
|
|||
package com.tangem.features.markets.details.impl.ui.entity
|
||||
package com.tangem.features.markets.details.impl.ui.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
|
||||
@Immutable
|
||||
internal data class MetricsUM(
|
||||
val metrics: PersistentList<InfoPointUM>,
|
||||
)
|
||||
|
|
@ -1,15 +1,12 @@
|
|||
package com.tangem.features.markets.details.impl.ui.entity
|
||||
package com.tangem.features.markets.details.impl.ui.state
|
||||
|
||||
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,
|
||||
|
|
@ -1,9 +1,7 @@
|
|||
package com.tangem.features.markets.details.impl.ui.entity
|
||||
package com.tangem.features.markets.details.impl.ui.state
|
||||
|
||||
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,
|
||||
|
|
@ -11,9 +11,9 @@ import com.tangem.domain.markets.TokenMarket
|
|||
import com.tangem.features.markets.component.BottomSheetState
|
||||
import com.tangem.features.markets.tokenlist.impl.model.statemanager.MarketsListUMStateManager
|
||||
import com.tangem.features.markets.tokenlist.impl.model.statemanager.MarketsListBatchFlowManager
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.entity.ListUM
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.entity.MarketsListItemUM
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.entity.SortByTypeUM
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.state.ListUM
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListItemUM
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.state.SortByTypeUM
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
|
|
@ -200,10 +200,12 @@ internal class MarketsListModel @Inject constructor(
|
|||
|
||||
modelScope.launch {
|
||||
marketsListUMStateManager.searchQueryFlow
|
||||
.filter { it.isNotEmpty() }
|
||||
.debounce(timeoutMillis = SEARCH_QUERY_DEBOUNCE_MILLIS)
|
||||
.distinctUntilChanged()
|
||||
.filter { activeListManager == searchMarketsListManager }
|
||||
.onEach {
|
||||
if (it.isEmpty()) searchMarketsListManager.clearStateAndStopAllActions()
|
||||
}
|
||||
.filter { it.isNotEmpty() && activeListManager == searchMarketsListManager }
|
||||
.collectLatest {
|
||||
searchMarketsListManager.reload(searchText = it)
|
||||
}
|
||||
|
|
@ -229,6 +231,7 @@ internal class MarketsListModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun CoroutineScope.loadQuotesWithTimer(timeMillis: Long) {
|
||||
launch {
|
||||
while (true) {
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ import com.tangem.core.ui.components.marketprice.PriceChangeType
|
|||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.TokenMarket
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.entity.MarketsListItemUM
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.entity.MarketsListUM.TrendInterval
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListItemUM
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListUM.TrendInterval
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import java.math.BigDecimal
|
||||
|
|
@ -70,10 +70,11 @@ internal class MarketsTokenItemConverter(
|
|||
private fun TokenMarket.getMarketCap(): String? {
|
||||
val value = marketCap?.takeIf { marketCap != BigDecimal.ZERO } ?: return null
|
||||
|
||||
return BigDecimalFormatter.formatCompactAmount(
|
||||
value,
|
||||
return BigDecimalFormatter.formatCompactFiatAmount(
|
||||
amount = value,
|
||||
fiatCurrencyCode = appCurrency.code,
|
||||
fiatCurrencySymbol = appCurrency.symbol,
|
||||
threeDigitsMethod = true,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@ import com.tangem.features.markets.tokenlist.impl.model.converters.MarketsTokenI
|
|||
import com.tangem.features.markets.tokenlist.impl.model.utils.logAction
|
||||
import com.tangem.features.markets.tokenlist.impl.model.utils.logStatus
|
||||
import com.tangem.features.markets.tokenlist.impl.model.utils.logUpdateResults
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.entity.MarketsListItemUM
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.entity.MarketsListUM.TrendInterval
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.entity.SortByTypeUM
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListItemUM
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListUM.TrendInterval
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.state.SortByTypeUM
|
||||
import com.tangem.pagination.Batch
|
||||
import com.tangem.pagination.BatchAction
|
||||
import com.tangem.pagination.BatchFetchResult
|
||||
|
|
|
|||
|
|
@ -7,11 +7,11 @@ import com.tangem.core.ui.event.consumedEvent
|
|||
import com.tangem.core.ui.event.triggeredEvent
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.features.markets.impl.R
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.entity.SortByBottomSheetContentUM
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.entity.ListUM
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.entity.MarketsListItemUM
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.entity.MarketsListUM
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.entity.SortByTypeUM
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.state.SortByBottomSheetContentUM
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.state.ListUM
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListItemUM
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListUM
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.state.SortByTypeUM
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
|
|
|||
|
|
@ -38,10 +38,10 @@ import com.tangem.features.markets.component.BottomSheetState
|
|||
import com.tangem.features.markets.impl.R
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.components.MarketsListLazyColumn
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.components.MarketsListSortByBottomSheet
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.entity.ListUM
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.entity.MarketsListUM
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.entity.SortByBottomSheetContentUM
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.entity.SortByTypeUM
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.state.ListUM
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListUM
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.state.SortByBottomSheetContentUM
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.state.SortByTypeUM
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.preview.MarketChartListItemPreviewDataProvider
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ import com.tangem.core.ui.res.TangemTheme
|
|||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.windowsize.WindowSizeType
|
||||
import com.tangem.features.markets.impl.R
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.entity.MarketsListItemUM
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListItemUM
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.preview.MarketChartListItemPreviewDataProvider
|
||||
import com.tangem.utils.StringsSigns.MINUS
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
|
|||
|
|
@ -9,10 +9,6 @@ import androidx.compose.material3.Text
|
|||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.tangem.core.ui.components.buttons.SecondarySmallButton
|
||||
|
|
@ -20,8 +16,9 @@ import com.tangem.core.ui.components.buttons.SmallButtonConfig
|
|||
import com.tangem.core.ui.event.EventEffect
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.utils.disableNestedScroll
|
||||
import com.tangem.features.markets.impl.R
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.entity.ListUM
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.state.ListUM
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
private const val LOAD_NEXT_PAGE_ON_END_INDEX = 50
|
||||
|
|
@ -54,7 +51,7 @@ internal fun MarketsListLazyColumn(
|
|||
|
||||
if (state is ListUM.Loading) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.nestedScroll(DisableParentConnection),
|
||||
modifier = Modifier.disableNestedScroll(),
|
||||
state = rememberLazyListState(),
|
||||
contentPadding = PaddingValues(bottom = bottomBarHeight),
|
||||
userScrollEnabled = false,
|
||||
|
|
@ -65,7 +62,7 @@ internal fun MarketsListLazyColumn(
|
|||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = modifier.nestedScroll(DisableParentConnection),
|
||||
modifier = modifier.disableNestedScroll(),
|
||||
state = lazyListState,
|
||||
contentPadding = PaddingValues(bottom = bottomBarHeight),
|
||||
userScrollEnabled = true,
|
||||
|
|
@ -221,10 +218,4 @@ fun InfiniteListHandler(listState: LazyListState, onLoadMore: () -> Boolean, buf
|
|||
emitted = onLoadMore()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private object DisableParentConnection : NestedScrollConnection {
|
||||
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
|
||||
return available.copy(x = 0f)
|
||||
}
|
||||
}
|
||||
|
|
@ -19,8 +19,8 @@ import com.tangem.core.ui.extensions.resourceReference
|
|||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.markets.impl.R
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.entity.SortByBottomSheetContentUM
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.entity.SortByTypeUM
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.state.SortByBottomSheetContentUM
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.state.SortByTypeUM
|
||||
|
||||
@Composable
|
||||
fun MarketsListSortByBottomSheet(config: TangemBottomSheetConfig) {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ package com.tangem.features.markets.tokenlist.impl.ui.preview
|
|||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import com.tangem.common.ui.charts.state.MarketChartRawData
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.entity.MarketsListItemUM
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListItemUM
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParameterProvider<MarketsListItemUM>(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.features.markets.tokenlist.impl.ui.entity
|
||||
package com.tangem.features.markets.tokenlist.impl.ui.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.common.ui.charts.state.MarketChartLook
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.features.markets.tokenlist.impl.ui.entity
|
||||
package com.tangem.features.markets.tokenlist.impl.ui.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.features.markets.tokenlist.impl.ui.entity
|
||||
package com.tangem.features.markets.tokenlist.impl.ui.state
|
||||
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue