Updated on 2026-08-14

This commit is contained in:
Tangem 2026-02-13 16:29:23 +05:00
parent e49dc700ac
commit 9d273fb556
9 changed files with 673 additions and 12 deletions

View file

@ -47,7 +47,7 @@ class AccountCryptoPortfolioItemStateConverter(
)
return TokenItemState.Content(
id = account.accountId.toItemId(),
iconState = AccountIconItemStateConverter.convert(this),
iconState = AccountIconItemStateConverter().convert(this),
titleState = TokenItemState.TitleState.Content(
text = accountName.toUM().value,
),
@ -73,7 +73,7 @@ class AccountCryptoPortfolioItemStateConverter(
private fun Account.CryptoPortfolio.mapToLoadingState(): TokenItemState.Content {
return TokenItemState.Content(
id = account.accountId.toItemId(),
iconState = AccountIconItemStateConverter.convert(account),
iconState = AccountIconItemStateConverter().convert(account),
titleState = TokenItemState.TitleState.Content(
text = accountName.toUM().value,
),
@ -95,7 +95,7 @@ class AccountCryptoPortfolioItemStateConverter(
private fun Account.CryptoPortfolio.mapToUnreachableState(): TokenItemState.Unreachable {
return TokenItemState.Unreachable(
id = account.accountId.toItemId(),
iconState = AccountIconItemStateConverter.convert(account),
iconState = AccountIconItemStateConverter().convert(account),
titleState = TokenItemState.TitleState.Content(
text = accountName.toUM().value,
),

View file

@ -1,11 +1,14 @@
package com.tangem.common.ui.account
import com.tangem.core.ui.components.account.AccountIconSize
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.CryptoPortfolioIcon
import com.tangem.utils.converter.Converter
object AccountIconItemStateConverter : Converter<Account, CurrencyIconState.CryptoPortfolio> {
class AccountIconItemStateConverter(
val size: AccountIconSize = AccountIconSize.Default,
) : Converter<Account, CurrencyIconState.CryptoPortfolio> {
override fun convert(value: Account): CurrencyIconState.CryptoPortfolio = when (value) {
is Account.CryptoPortfolio -> when {
@ -13,11 +16,13 @@ object AccountIconItemStateConverter : Converter<Account, CurrencyIconState.Cryp
char = value.accountName.toUM().value,
color = value.icon.color.getUiColor(),
isGrayscale = false,
size = size,
)
else -> CurrencyIconState.CryptoPortfolio.Icon(
resId = value.icon.value.getResId(),
color = value.icon.color.getUiColor(),
isGrayscale = false,
size = size,
)
}
is Account.Payment -> TODO("[REDACTED_JIRA]")

View file

@ -1,17 +1,35 @@
package com.tangem.common.ui.notifications
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.ds.TangemPagerIndicator
import com.tangem.core.ui.ds.message.TangemMessage
import com.tangem.core.ui.ds.message.TangemMessageEffect
import com.tangem.core.ui.ds.message.TangemMessageUM
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
fun LazyListScope.notifications(
notifications: ImmutableList<NotificationUM>,
@ -110,4 +128,103 @@ fun LazyListScope.notifications(
)
},
)
}
}
/**
* Displays a list of notifications in a stacked manner using a HorizontalPager.
* If there are multiple notifications, a PagerIndicator is shown below the notifications.
*
* @param notifications List of TangemMessageUM objects to be displayed.
* @param containerColor Color to be used for the background of the notifications.
* @param modifier Optional Modifier for the notifications.
*/
fun LazyListScope.stackedNotifications(
notifications: ImmutableList<TangemMessageUM>?,
containerColor: Color,
modifier: Modifier = Modifier,
) {
item {
if (!notifications.isNullOrEmpty()) {
val notificationsPagerState = rememberPagerState(
pageCount = { notifications.size },
)
Column(
modifier = Modifier
.fillMaxSize()
.padding(top = TangemTheme.dimens2.x2),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2),
) {
HorizontalPager(
state = notificationsPagerState,
modifier = Modifier
.fillMaxSize()
.animateItem(null, null, null),
) { page ->
TangemMessage(
messageUM = notifications[page],
contentColor = containerColor,
modifier = modifier,
)
}
if (notifications.size > 1) {
TangemPagerIndicator(
pagerState = notificationsPagerState,
)
}
}
}
}
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun StackedNotifications_Preview(
@PreviewParameter(StackedNotificationsPreviewProvider::class) params: ImmutableList<TangemMessageUM>,
) {
TangemThemePreviewRedesign {
val contentColor = TangemTheme.colors2.surface.level1
LazyColumn(
modifier = Modifier
.background(contentColor)
.padding(16.dp),
) {
stackedNotifications(
notifications = params,
containerColor = contentColor,
)
}
}
}
private class StackedNotificationsPreviewProvider : PreviewParameterProvider<ImmutableList<TangemMessageUM>> {
override val values: Sequence<ImmutableList<TangemMessageUM>>
get() = sequenceOf(
persistentListOf(
TangemMessageUM(
id = "0",
title = stringReference("First notification"),
subtitle = stringReference("This is the first notification"),
messageEffect = TangemMessageEffect.Magic,
),
),
persistentListOf(
TangemMessageUM(
id = "0",
title = stringReference("First notification"),
subtitle = stringReference("This is the first notification"),
messageEffect = TangemMessageEffect.Magic,
),
TangemMessageUM(
id = "1",
title = stringReference("Second notification"),
subtitle = stringReference("This is the second notification"),
messageEffect = TangemMessageEffect.Card,
),
),
)
}
// endregion

View file

@ -0,0 +1,370 @@
package com.tangem.core.ui.ds
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlin.math.abs
import kotlin.math.min
import kotlin.math.roundToInt
private const val ANIMATION_DURATION = 300
private const val MAX_VISIBLE_DOTS = 5
private const val MIN_HIDDEN_FOR_SMALL_DOT = 2
private const val MIN_DISTANCE_FOR_SMALL_DOT = 3
private const val MIN_DISTANCE_FOR_HINT_DOT = 2
private val SPACING = 4.dp
private val CURRENT_DOT_SIZE = DpSize(16.dp, 8.dp)
private val NORMAL_DOT_SIZE = DpSize(8.dp, 8.dp)
private val HINT_DOT_SIZE = DpSize(6.dp, 6.dp)
private val SMALL_DOT_SIZE = DpSize(4.dp, 4.dp)
/**
* // TODO Cleanup and document this code, it's quite complex and has some "magic numbers" that need explanation.
*
* A pager indicator that adapts to the number of pages and the current page index.
*
* For 5 or fewer pages, it shows all dots with the current page highlighted.
* For more than 5 pages, it shows a sliding window of 5 dots with size and opacity indicating position.
*
* @param pagerState state of the pager to observe
* @param activeIndicatorColor color for the active page indicator
* @param inactiveIndicatorColor color for the inactive page indicators
* @param modifier modifier for styling
*/
@Suppress("LongMethod", "CyclomaticComplexMethod")
@Composable
fun TangemPagerIndicator(
pagerState: PagerState,
modifier: Modifier = Modifier,
activeIndicatorColor: Color = TangemTheme.colors2.graphic.neutral.primary,
inactiveIndicatorColor: Color = TangemTheme.colors2.graphic.neutral.tertiary,
) {
val totalPages = pagerState.pageCount
val currentIndex = pagerState.currentPage
if (totalPages == 0) return
val density = LocalDensity.current
val (targetLower, targetUpper) = getWindowBounds(totalPages, currentIndex)
var displayLower by remember { mutableIntStateOf(targetLower) }
var displayUpper by remember { mutableIntStateOf(targetUpper) }
var prevTargetLower by remember { mutableIntStateOf(targetLower) }
val slideOffset = remember { Animatable(0f) }
var isSliding by remember { mutableStateOf(false) }
var slideDirection by remember { mutableIntStateOf(0) }
val fadeProgress = remember { Animatable(0f) }
var fadeJob by remember { mutableStateOf<Job?>(null) }
LaunchedEffect(targetLower) {
if (targetLower != prevTargetLower && totalPages > MAX_VISIBLE_DOTS) {
fadeJob?.cancel()
slideOffset.stop()
fadeProgress.stop()
val dir = if (targetLower > prevTargetLower) 1 else -1
val edgeDotSize = with(density) { (HINT_DOT_SIZE.width + SPACING).toPx() }
val halfEdge = edgeDotSize / 2
isSliding = true
slideDirection = dir
fadeProgress.snapTo(0f)
if (dir > 0) {
displayLower = prevTargetLower
displayUpper = targetUpper
slideOffset.snapTo(halfEdge)
} else {
displayLower = targetLower
displayUpper = prevTargetLower + MAX_VISIBLE_DOTS
slideOffset.snapTo(-halfEdge)
}
prevTargetLower = targetLower
fadeJob = launch {
fadeProgress.animateTo(1f, tween(ANIMATION_DURATION))
}
slideOffset.animateTo(
if (dir > 0) -halfEdge else halfEdge,
tween(ANIMATION_DURATION),
)
displayLower = targetLower
displayUpper = targetUpper
slideOffset.snapTo(0f)
isSliding = false
slideDirection = 0
}
}
val visibleIndices = (displayLower until displayUpper).toList()
Box(
modifier = modifier,
contentAlignment = Alignment.Center,
) {
Row(
modifier = Modifier.offset {
IntOffset(slideOffset.value.roundToInt(), 0)
},
horizontalArrangement = Arrangement.spacedBy(SPACING),
verticalAlignment = Alignment.CenterVertically,
) {
visibleIndices.forEach { index ->
val dotAlpha = when {
!isSliding -> 1f
slideDirection > 0 && index == displayLower -> 1f - fadeProgress.value
slideDirection > 0 && index == displayUpper - 1 -> fadeProgress.value
slideDirection < 0 && index == displayUpper - 1 -> 1f - fadeProgress.value
slideDirection < 0 && index == displayLower -> fadeProgress.value
else -> 1f
}
key(index) {
Dot(
index = index,
currentIndex = currentIndex,
totalPages = totalPages,
activeColor = activeIndicatorColor,
inactiveColor = inactiveIndicatorColor,
modifier = Modifier.graphicsLayer { alpha = dotAlpha },
)
}
}
}
}
}
private fun getWindowBounds(totalPages: Int, currentIndex: Int): Pair<Int, Int> {
if (totalPages <= MAX_VISIBLE_DOTS) {
return 0 to totalPages
}
val lowerBound = when {
currentIndex <= 1 -> 0
currentIndex >= totalPages - 2 -> totalPages - MAX_VISIBLE_DOTS
else -> currentIndex - 2
}
val upperBound = min(lowerBound + MAX_VISIBLE_DOTS, totalPages)
return lowerBound to upperBound
}
private fun getDotSize(index: Int, currentIndex: Int, totalPages: Int): DpSize {
if (index == currentIndex) {
return CURRENT_DOT_SIZE
}
if (totalPages <= MAX_VISIBLE_DOTS) {
return NORMAL_DOT_SIZE
}
val params = DotSizeParams.create(index, currentIndex, totalPages)
return params.calculateSize()
}
private class DotSizeParams private constructor(
val posInWindow: Int,
val currentPosInWindow: Int,
val hiddenLeft: Int,
val hiddenRight: Int,
val distanceFromCurrent: Int,
) {
private val lastPos = MAX_VISIBLE_DOTS - 1
private val isCentered = currentPosInWindow == 2 && hiddenLeft >= 1 && hiddenRight >= 1
fun calculateSize(): DpSize = when {
isCentered -> getCenteredSize()
hiddenRight >= 1 -> getRightEdgeSize()
hiddenLeft >= 1 -> getLeftEdgeSize()
else -> NORMAL_DOT_SIZE
}
private fun getCenteredSize(): DpSize = when (posInWindow) {
0, lastPos -> HINT_DOT_SIZE
else -> NORMAL_DOT_SIZE
}
private fun getRightEdgeSize(): DpSize {
val isLastPos = posInWindow == lastPos
val isSecondToLast = posInWindow == lastPos - 1
val hasExtraHidden = hiddenRight >= MIN_HIDDEN_FOR_SMALL_DOT
val isFarFromCurrent = distanceFromCurrent >= MIN_DISTANCE_FOR_SMALL_DOT
val isModerateDistance = distanceFromCurrent >= MIN_DISTANCE_FOR_HINT_DOT
return when {
isLastPos && hasExtraHidden && isFarFromCurrent -> SMALL_DOT_SIZE
isLastPos && isModerateDistance -> HINT_DOT_SIZE
isSecondToLast && hasExtraHidden && isModerateDistance -> HINT_DOT_SIZE
else -> NORMAL_DOT_SIZE
}
}
private fun getLeftEdgeSize(): DpSize {
val isFirstPos = posInWindow == 0
val isSecondPos = posInWindow == 1
val hasExtraHidden = hiddenLeft >= MIN_HIDDEN_FOR_SMALL_DOT
val isFarFromCurrent = distanceFromCurrent >= MIN_DISTANCE_FOR_SMALL_DOT
val isModerateDistance = distanceFromCurrent >= MIN_DISTANCE_FOR_HINT_DOT
return when {
isFirstPos && hasExtraHidden && isFarFromCurrent -> SMALL_DOT_SIZE
isFirstPos && isModerateDistance -> HINT_DOT_SIZE
isSecondPos && hasExtraHidden && isModerateDistance -> HINT_DOT_SIZE
else -> NORMAL_DOT_SIZE
}
}
companion object {
fun create(index: Int, currentIndex: Int, totalPages: Int): DotSizeParams {
val (windowStart, windowEnd) = getWindowBounds(totalPages, currentIndex)
val posInWindow = index - windowStart
val currentPosInWindow = currentIndex - windowStart
return DotSizeParams(
posInWindow = posInWindow,
currentPosInWindow = currentPosInWindow,
hiddenLeft = windowStart,
hiddenRight = totalPages - windowEnd,
distanceFromCurrent = abs(posInWindow - currentPosInWindow),
)
}
}
}
@Composable
private fun Dot(
index: Int,
currentIndex: Int,
totalPages: Int,
activeColor: Color,
inactiveColor: Color,
modifier: Modifier = Modifier,
) {
val isActive = index == currentIndex
val size = getDotSize(index, currentIndex, totalPages)
val animSpec = tween<Dp>(ANIMATION_DURATION)
val colorSpec = tween<Color>(ANIMATION_DURATION)
val animatedWidth by animateDpAsState(size.width, animSpec, label = "w$index")
val animatedHeight by animateDpAsState(size.height, animSpec, label = "h$index")
val animatedColor by animateColorAsState(
targetValue = if (isActive) activeColor else inactiveColor,
animationSpec = colorSpec,
label = "c$index",
)
val shape = RoundedCornerShape(animatedHeight / 2)
Box(
modifier = modifier
.width(animatedWidth)
.height(animatedHeight)
.background(animatedColor, shape),
)
}
@Preview(showBackground = true)
@Composable
private fun PagerIndicatorPreview() {
TangemThemePreviewRedesign {
Column(
Modifier
.background(TangemTheme.colors.background.primary)
.padding(20.dp),
verticalArrangement = Arrangement.spacedBy(10.dp),
) {
listOf(0, 1, 2, 3, 4).forEach { page ->
TangemPagerIndicator(rememberPagerState(page) { 5 })
}
}
}
}
@Preview(showBackground = true)
@Composable
private fun PagerIndicator6ItemsPreview() {
TangemThemePreviewRedesign {
Column(
Modifier
.background(TangemTheme.colors.background.primary)
.padding(20.dp),
verticalArrangement = Arrangement.spacedBy(10.dp),
) {
listOf(0, 1, 2, 3, 4, 5).forEach { page ->
TangemPagerIndicator(rememberPagerState(page) { 6 })
}
}
}
}
@Preview(showBackground = true)
@Composable
private fun PagerIndicator7ItemsPreview() {
TangemThemePreviewRedesign {
Column(
Modifier
.background(TangemTheme.colors.background.primary)
.padding(20.dp),
verticalArrangement = Arrangement.spacedBy(10.dp),
) {
listOf(0, 1, 2, 3, 4, 5, 6).forEach { page ->
TangemPagerIndicator(rememberPagerState(page) { 7 })
}
}
}
}
@Preview(showBackground = true)
@Composable
private fun PagerIndicator10ItemsPreview() {
TangemThemePreviewRedesign {
Column(
Modifier
.background(TangemTheme.colors.background.primary)
.padding(20.dp),
verticalArrangement = Arrangement.spacedBy(10.dp),
) {
listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9).forEach { page ->
TangemPagerIndicator(rememberPagerState(page) { 10 })
}
}
}
}
@Preview(showBackground = true)
@Composable
private fun PagerIndicatorSmallCountsPreview() {
TangemThemePreviewRedesign {
Column(
Modifier
.background(TangemTheme.colors.background.primary)
.padding(20.dp),
verticalArrangement = Arrangement.spacedBy(10.dp),
) {
TangemPagerIndicator(rememberPagerState(0) { 1 })
TangemPagerIndicator(rememberPagerState(1) { 2 })
TangemPagerIndicator(rememberPagerState(1) { 3 })
}
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.core.ui.format.bigdecimal
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CAN_BE_LOWER_SIGN
import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CRYPTO_FEE_FORMAT_THRESHOLD
import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CURRENCY_SPACE
@ -9,6 +10,7 @@ import com.tangem.utils.StringsSigns.NON_BREAKING_SPACE
import com.tangem.utils.extensions.isNotWhitespace
import java.math.BigDecimal
import java.math.RoundingMode
import java.text.DecimalFormat
import java.text.NumberFormat
import java.util.Currency
import java.util.Locale
@ -34,6 +36,17 @@ class BigDecimalCryptoFormatFull(
override fun invoke(value: BigDecimal): String = defaultAmount()(value)
}
open class BigDecimalCryptoFormatStyled(
val symbol: String,
val decimals: Int,
val spanStyleReference: SpanStyleReference,
val locale: Locale = Locale.getDefault(),
val shouldIgnoreSymbolPosition: Boolean = false,
) : BigDecimalFormatStyled {
override fun invoke(value: BigDecimal): TextReference = defaultAmount(spanStyleReference)(value)
}
// == Initializers ==
fun BigDecimalFormatScope.crypto(
@ -61,6 +74,35 @@ fun BigDecimalFormatScope.crypto(
)
}
fun BigDecimalFormatScope.cryptoStyled(
symbol: String,
decimals: Int,
spanStyleReference: SpanStyleReference,
locale: Locale = Locale.getDefault(),
): BigDecimalCryptoFormatStyled {
return BigDecimalCryptoFormatStyled(
symbol = symbol,
decimals = decimals,
spanStyleReference = spanStyleReference,
locale = locale,
)
}
fun BigDecimalFormatScope.cryptoStyled(
cryptoCurrency: CryptoCurrency,
spanStyleReference: SpanStyleReference,
ignoreSymbolPosition: Boolean = false,
locale: Locale = Locale.getDefault(),
): BigDecimalCryptoFormatStyled {
return BigDecimalCryptoFormatStyled(
symbol = cryptoCurrency.symbol,
decimals = cryptoCurrency.decimals,
spanStyleReference = spanStyleReference,
shouldIgnoreSymbolPosition = ignoreSymbolPosition,
locale = locale,
)
}
// == Formatters ==
fun BigDecimalCryptoFormat.defaultAmount() = BigDecimalFormat { value ->
@ -88,6 +130,51 @@ fun BigDecimalCryptoFormat.defaultAmount() = BigDecimalFormat { value ->
}
}
fun BigDecimalCryptoFormatStyled.defaultAmount(spanStyleReference: SpanStyleReference) =
BigDecimalFormatStyled { value ->
if (shouldIgnoreSymbolPosition) {
val formatter = NumberFormat.getInstance(locale).apply {
maximumFractionDigits = decimals.coerceAtMost(maximumValue = 8)
minimumFractionDigits = 2
isGroupingUsed = true
roundingMode = RoundingMode.HALF_UP
}
val formattedAmount = formatter.format(value)
val decimalSeparator = (formatter as? DecimalFormat)?.decimalFormatSymbols?.decimalSeparator
val separatorIndex = decimalSeparator?.let { formattedAmount.indexOf(it) } ?: formattedAmount.length
combinedReference(
stringReference(formattedAmount.take(separatorIndex)),
styledStringReference(formattedAmount.drop(separatorIndex), spanStyleReference),
stringReference(NON_BREAKING_SPACE + symbol),
)
} else {
val formatter = NumberFormat.getCurrencyInstance(locale).apply {
currency = usdCurrency
maximumFractionDigits = decimals.coerceAtMost(maximumValue = 8)
minimumFractionDigits = 2
isGroupingUsed = true
roundingMode = RoundingMode.HALF_UP
}
val formattedAmount = formatter.format(value)
.replaceFiatSymbolWithCrypto(
fiatCurrencySymbol = usdCurrency.getSymbol(locale),
cryptoCurrencySymbol = symbol,
)
val decimalSeparator = (formatter as? DecimalFormat)?.decimalFormatSymbols?.decimalSeparator
val separatorIndex = decimalSeparator?.let { formattedAmount.indexOf(it) } ?: formattedAmount.length
combinedReference(
stringReference(formattedAmount.take(separatorIndex)),
styledStringReference(formattedAmount.drop(separatorIndex), spanStyleReference),
)
}
}
fun BigDecimalCryptoFormat.shorted() = BigDecimalFormat { value ->
val formatter = if (value.isMoreThanThreshold()) {
NumberFormat.getCurrencyInstance(locale).apply {

View file

@ -1,9 +1,11 @@
package com.tangem.core.ui.format.bigdecimal
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CAN_BE_LOWER_SIGN
import com.tangem.utils.StringsSigns.TILDE_SIGN
import java.math.BigDecimal
import java.math.RoundingMode
import java.text.DecimalFormat
import java.text.NumberFormat
import java.util.Locale
@ -15,8 +17,16 @@ open class BigDecimalFiatFormat(
override fun invoke(value: BigDecimal): String = defaultAmount()(value)
}
// == Initializers ==
open class BigDecimalFiatFormatStyled(
val fiatCurrencyCode: String,
val fiatCurrencySymbol: String,
val spanStyleReference: SpanStyleReference,
val locale: Locale = Locale.getDefault(),
) : BigDecimalFormatStyled {
override fun invoke(value: BigDecimal): TextReference = defaultAmount(spanStyleReference)(value)
}
//region == Initializers ==
fun BigDecimalFormatScope.fiat(
fiatCurrencyCode: String,
fiatCurrencySymbol: String,
@ -29,7 +39,20 @@ fun BigDecimalFormatScope.fiat(
)
}
// == Formatters ==
fun BigDecimalFormatScope.fiat(
fiatCurrencyCode: String,
fiatCurrencySymbol: String,
spanStyleReference: SpanStyleReference,
locale: Locale = Locale.getDefault(),
): BigDecimalFiatFormatStyled {
return BigDecimalFiatFormatStyled(
fiatCurrencyCode = fiatCurrencyCode,
fiatCurrencySymbol = fiatCurrencySymbol,
spanStyleReference = spanStyleReference,
locale = locale,
)
}
// endregion == Formatters ==
/**
* Formats fiat amount with default precision.
@ -58,6 +81,38 @@ fun BigDecimalFiatFormat.defaultAmount(): BigDecimalFormat = BigDecimalFormat {
}
}
fun BigDecimalFiatFormatStyled.defaultAmount(spanStyleReference: SpanStyleReference) = BigDecimalFormatStyled { value ->
val formatterCurrency = getJavaCurrencyByCode(fiatCurrencyCode)
val formatter = NumberFormat.getCurrencyInstance(locale).apply {
currency = formatterCurrency
maximumFractionDigits = FIAT_MARKET_DEFAULT_DIGITS
minimumFractionDigits = FIAT_MARKET_DEFAULT_DIGITS
roundingMode = RoundingMode.HALF_UP
}
val formattingAmount = if (value.isLessThanThreshold()) {
FIAT_FORMAT_THRESHOLD
} else {
value
}
val decimalSeparator = (formatter as? DecimalFormat)?.decimalFormatSymbols?.decimalSeparator
val formattedAmount = formatter.format(formattingAmount)
.replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol)
val separatorIndex = decimalSeparator?.let { formattedAmount.indexOf(it) } ?: formattedAmount.length
val wholePart = formattedAmount.take(separatorIndex)
val fractionalPart = formattedAmount.drop(separatorIndex)
combinedReference(
if (formattingAmount.isLessThanThreshold()) stringReference(CAN_BE_LOWER_SIGN) else TextReference.EMPTY,
stringReference(wholePart),
styledStringReference(fractionalPart, spanStyleReference),
)
}
/**
* Formats fiat amount with default precision and adds tilde sign
*/

View file

@ -1,17 +1,27 @@
package com.tangem.core.ui.format.bigdecimal
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import java.math.BigDecimal
interface BigDecimalFormatScope {
companion object { val Empty = object : BigDecimalFormatScope {} }
companion object {
val Empty = object : BigDecimalFormatScope {}
}
}
fun interface BigDecimalFormat : (BigDecimal) -> String, BigDecimalFormatScope
fun interface BigDecimalFormatStyled : (BigDecimal) -> TextReference, BigDecimalFormatScope
inline fun BigDecimal.format(block: BigDecimalFormatScope.() -> BigDecimalFormat): String {
return BigDecimalFormatScope.Empty.block()(this)
}
inline fun BigDecimal.formatStyled(block: BigDecimalFormatScope.() -> BigDecimalFormatStyled): TextReference {
return BigDecimalFormatScope.Empty.block()(this)
}
inline fun BigDecimal?.format(
fallbackString: String = BigDecimalFormatConstants.EMPTY_BALANCE_SIGN,
block: BigDecimalFormatScope.() -> BigDecimalFormat,
@ -20,10 +30,26 @@ inline fun BigDecimal?.format(
return BigDecimalFormatScope.Empty.block()(this)
}
inline fun BigDecimal?.formatStyled(
fallbackString: String = BigDecimalFormatConstants.EMPTY_BALANCE_SIGN,
block: BigDecimalFormatScope.() -> BigDecimalFormatStyled,
): TextReference {
if (this == null) return stringReference(fallbackString)
return BigDecimalFormatScope.Empty.block()(this)
}
fun BigDecimal?.format(
format: BigDecimalFormat,
fallbackString: String = BigDecimalFormatConstants.EMPTY_BALANCE_SIGN,
): String {
if (this == null) return fallbackString
return format(this)
}
fun BigDecimal?.format(
format: BigDecimalFormatStyled,
fallbackString: String = BigDecimalFormatConstants.EMPTY_BALANCE_SIGN,
): TextReference {
if (this == null) return stringReference(fallbackString)
return format(this)
}

View file

@ -16,6 +16,7 @@ object TangemColorPalette {
val Dark4 = Color(0xFF3B3B3B)
val Dark5 = Color(0xFF303030)
val Dark6 = Color(0xFF1E1E1E)
val Dark7 = Color(0xFF171717)
// endregion Dark
// region Dark Alpha

View file

@ -122,8 +122,8 @@ private fun lightThemeColors2(): TangemColors2 {
val surface = TangemColors2.Surface(
level1 = TangemColorPalette.White,
level2 = TangemColorPalette.Light1V2,
level3 = TangemColorPalette.Light1V2,
level4 = TangemColorPalette.White,
level3 = TangemColorPalette.White,
level4 = TangemColorPalette.Light1V2,
)
val controls = TangemColors2.Controls(
backgroundChecked = TangemColorPalette.Dark6,
@ -270,8 +270,8 @@ private fun darkThemeColors2(): TangemColors2 {
borderPrimary = TangemColorPalette.Light4,
)
val surface = TangemColors2.Surface(
level1 = TangemColorPalette.Dark6,
level2 = TangemColorPalette.Black,
level1 = TangemColorPalette.Black,
level2 = TangemColorPalette.Dark7,
level3 = TangemColorPalette.Dark6,
level4 = TangemColorPalette.Dark5,
)