Updated on 2026-08-14

This commit is contained in:
Tangem 2024-08-19 22:22:48 +04:00
parent e51c82e615
commit 1f8af50d6a
17 changed files with 143 additions and 154 deletions

View file

@ -2,13 +2,12 @@ package com.tangem.feature.wallet.presentation.common
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.event.consumedEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem
import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState
import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState
@ -66,122 +65,22 @@ internal object WalletPreviewData {
)
}
val coinIconState
get() = CurrencyIconState.CoinIcon(
url = null,
fallbackResId = R.drawable.img_polygon_22,
isGrayscale = false,
showCustomBadge = false,
)
private val tokenIconState
get() = CurrencyIconState.TokenIcon(
url = null,
topBadgeIconResId = R.drawable.img_polygon_22,
fallbackTint = TangemColorPalette.Black,
fallbackBackground = TangemColorPalette.Meadow,
isGrayscale = false,
showCustomBadge = false,
)
private val customTokenIconState
get() = CurrencyIconState.CustomTokenIcon(
tint = TangemColorPalette.Black,
background = TangemColorPalette.Meadow,
topBadgeIconResId = R.drawable.img_polygon_22,
isGrayscale = false,
)
val tokenItemVisibleState by lazy {
TokenItemState.Content(
id = UUID.randomUUID().toString(),
iconState = coinIconState,
titleState = TokenItemState.TitleState.Content(text = "Polygon", hasPending = true),
fiatAmountState = TokenItemState.FiatAmountState.Content(text = "321 $", hasStaked = true),
cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = "5,412 MATIC"),
cryptoPriceState = TokenItemState.CryptoPriceState.Unknown,
onItemClick = {},
onItemLongClick = {},
)
}
val testnetTokenItemVisibleState by lazy {
tokenItemVisibleState.copy(
titleState = TokenItemState.TitleState.Content(text = "Polygon testnet"),
iconState = tokenIconState.copy(isGrayscale = true),
)
}
val tokenItemHiddenState by lazy {
TokenItemState.Content(
id = UUID.randomUUID().toString(),
iconState = tokenIconState,
titleState = TokenItemState.TitleState.Content(text = "Polygon"),
fiatAmountState = TokenItemState.FiatAmountState.Content(text = "321 $", hasStaked = false),
cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = "5,412 MATIC"),
cryptoPriceState = TokenItemState.CryptoPriceState.Content(
price = "312 USD",
priceChangePercent = "2.0%",
type = PriceChangeType.UP,
),
onItemClick = {},
onItemLongClick = {},
)
}
val tokenItemDragState by lazy {
private val tokenItemDragState by lazy {
TokenItemState.Draggable(
id = UUID.randomUUID().toString(),
iconState = tokenIconState,
iconState = CurrencyIconState.TokenIcon(
url = null,
topBadgeIconResId = R.drawable.img_polygon_22,
fallbackTint = TangemColorPalette.Black,
fallbackBackground = TangemColorPalette.Meadow,
isGrayscale = false,
showCustomBadge = false,
),
titleState = TokenItemState.TitleState.Content(text = "Polygon"),
cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = "3 172,14 $"),
)
}
val tokenItemUnreachableState by lazy {
TokenItemState.Unreachable(
id = UUID.randomUUID().toString(),
iconState = tokenIconState,
titleState = TokenItemState.TitleState.Content(text = "Polygon"),
onItemClick = {},
onItemLongClick = {},
)
}
val tokenItemNoAddressState by lazy {
TokenItemState.NoAddress(
id = UUID.randomUUID().toString(),
iconState = tokenIconState,
titleState = TokenItemState.TitleState.Content(text = "Polygon"),
onItemLongClick = {},
)
}
val customTokenItemVisibleState by lazy {
tokenItemVisibleState.copy(
titleState = TokenItemState.TitleState.Content(text = "Polygon"),
iconState = customTokenIconState.copy(
tint = TangemColorPalette.White,
background = TangemColorPalette.Black,
),
)
}
val customTestnetTokenItemVisibleState by lazy {
tokenItemVisibleState.copy(
titleState = TokenItemState.TitleState.Content(text = "Polygon"),
iconState = customTokenIconState.copy(isGrayscale = true),
)
}
val loadingTokenItemState by lazy {
TokenItemState.Loading(
id = "Loading#1",
iconState = customTokenIconState.copy(isGrayscale = true),
titleState = TokenItemState.TitleState.Content(text = "Polygon"),
)
}
private const val networksSize = 10
private const val tokensSize = 3
private val draggableItems by lazy {

View file

@ -1,426 +0,0 @@
package com.tangem.feature.wallet.presentation.common.component
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.unit.Constraints
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.extensions.rememberHapticFeedback
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.feature.wallet.presentation.common.WalletPreviewData
import com.tangem.feature.wallet.presentation.common.component.token.*
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
import org.burnoutcrew.reorderable.ReorderableLazyListState
import kotlin.math.max
private const val TITLE_MIN_WIDTH_COEFFICIENT = 0.3
private const val PRICE_MIN_WIDTH_COEFFICIENT = 0.32
private enum class LayoutId {
ICON, TITLE, FIAT_AMOUNT, CRYPTO_AMOUNT, CRYPTO_PRICE, NON_FIAT_CONTENT
}
@Composable
internal fun TokenItem(
state: TokenItemState,
isBalanceHidden: Boolean,
modifier: Modifier = Modifier,
reorderableTokenListState: ReorderableLazyListState? = null,
) {
val betweenRowsMargin = TangemTheme.dimens.spacing2
CustomContainer(
state = state,
modifier = modifier
.tokenClickable(state = state)
.background(color = TangemTheme.colors.background.primary),
) {
CurrencyIcon(
state = state.iconState,
modifier = Modifier
.layoutId(layoutId = LayoutId.ICON)
.padding(end = TangemTheme.dimens.spacing8),
)
TokenTitle(
state = state.titleState,
modifier = Modifier
.layoutId(layoutId = LayoutId.TITLE)
.padding(end = TangemTheme.dimens.spacing8)
.padding(bottom = betweenRowsMargin),
)
TokenFiatAmount(
state = state.fiatAmountState,
isBalanceHidden = isBalanceHidden,
modifier = Modifier
.layoutId(layoutId = LayoutId.FIAT_AMOUNT)
.padding(bottom = betweenRowsMargin),
)
TokenPrice(
state = state.cryptoPriceState,
modifier = Modifier
.layoutId(layoutId = LayoutId.CRYPTO_PRICE)
.padding(end = TangemTheme.dimens.spacing8),
)
TokenCryptoAmount(
state = state.cryptoAmountState,
isBalanceHidden = isBalanceHidden,
modifier = Modifier.layoutId(layoutId = LayoutId.CRYPTO_AMOUNT),
)
NonFiatContentBlock(
state = state,
reorderableTokenListState = reorderableTokenListState,
modifier = Modifier.layoutId(layoutId = LayoutId.NON_FIAT_CONTENT),
)
}
}
@OptIn(ExperimentalFoundationApi::class)
private fun Modifier.tokenClickable(state: TokenItemState): Modifier = composed {
when (state) {
is TokenItemState.Content -> {
val onLongClick = rememberHapticFeedback(state = state, onAction = state.onItemLongClick)
combinedClickable(onClick = state.onItemClick, onLongClick = onLongClick)
}
is TokenItemState.Unreachable -> {
val onLongClick = rememberHapticFeedback(state = state, onAction = state.onItemLongClick)
combinedClickable(onClick = state.onItemClick, onLongClick = onLongClick)
}
is TokenItemState.NoAddress -> {
val onLongClick = rememberHapticFeedback(state = state, onAction = state.onItemLongClick)
combinedClickable(onClick = {}, onLongClick = onLongClick)
}
is TokenItemState.Draggable,
is TokenItemState.Loading,
is TokenItemState.Locked,
-> this
}
}
/**
* IMPORTANT! All margins that used between children setup like as children paddings.
*/
@Suppress("LongMethod")
@Composable
private fun CustomContainer(state: TokenItemState, modifier: Modifier = Modifier, content: @Composable () -> Unit) {
val density = LocalDensity.current
val dimens = TangemTheme.dimens
Layout(content = content, modifier = modifier) { measurables, constraints ->
val layoutWidth = constraints.maxWidth
val horizontalPadding = with(density) { dimens.size12.roundToPx() }
val verticalPadding = with(density) { dimens.size15.roundToPx() }
val layoutWidthWithoutPaddings = layoutWidth - 2 * horizontalPadding
val titleMinWidth = (layoutWidth * TITLE_MIN_WIDTH_COEFFICIENT).toInt()
val priceChangeMinWidth = (layoutWidth * PRICE_MIN_WIDTH_COEFFICIENT).toInt()
val icon = measurables.measure(layoutId = LayoutId.ICON, constraints = constraints)
/*
* Title width take the whole REMAINING space.
* If FiatAmount took the whole free space, then Title will have min width.
*/
val title: Placeable
// FiatAmount width must take the whole free space but is not greater the Title min size
var fiatAmount: Placeable? = null
// CryptoAmount width must take the whole free space but is not greater the PriceChange min size
var cryptoAmount: Placeable? = null
/*
* PriceChange width take the whole REMAINING space.
* If CryptoAmount took the whole free space, then PriceChange will have min width.
*/
val priceChange: Placeable?
val nonFiatContent = measurables.measure(layoutId = LayoutId.NON_FIAT_CONTENT, constraints = constraints)
var firstRowRemainingFreeSpace: Int? = null
var secondRowRemainingFreeSpace: Int? = null
when (state) {
is TokenItemState.Content,
is TokenItemState.Loading,
is TokenItemState.Locked,
-> {
fiatAmount = measurables.measureFiatAmount(
state = state,
maxWidth = layoutWidthWithoutPaddings - icon.width - titleMinWidth,
defaultConstraints = constraints,
)
cryptoAmount = measurables.measureCryptoAmount(
state = state,
maxWidth = layoutWidthWithoutPaddings - icon.width - priceChangeMinWidth,
defaultConstraints = constraints,
)
firstRowRemainingFreeSpace = layoutWidthWithoutPaddings - icon.width - fiatAmount.width
secondRowRemainingFreeSpace = layoutWidthWithoutPaddings - icon.width - cryptoAmount.width
}
is TokenItemState.Draggable -> {
cryptoAmount = measurables.measureCryptoAmount(
state = state,
maxWidth = layoutWidthWithoutPaddings - icon.width - nonFiatContent.width,
defaultConstraints = constraints,
)
firstRowRemainingFreeSpace = layoutWidthWithoutPaddings - icon.width - nonFiatContent.width
}
is TokenItemState.NoAddress,
is TokenItemState.Unreachable,
-> {
firstRowRemainingFreeSpace = layoutWidthWithoutPaddings - icon.width - nonFiatContent.width
}
}
title = measurables.measureTitle(
state = state,
minWidth = titleMinWidth,
remainingFreeSpace = firstRowRemainingFreeSpace,
defaultConstraints = constraints,
)
priceChange = secondRowRemainingFreeSpace?.let {
measurables.measurePriceChange(
state = state,
minWidth = priceChangeMinWidth,
remainingFreeSpace = secondRowRemainingFreeSpace,
defaultConstraints = constraints,
)
}
val layoutHeight = calculateLayoutHeight(
state = state,
minLayoutHeight = with(density) { dimens.size68.roundToPx() },
layoutPadding = verticalPadding,
title = title,
fiatAmount = fiatAmount,
cryptoAmount = cryptoAmount,
priceChange = priceChange,
)
layout(width = constraints.maxWidth, height = layoutHeight) {
icon.placeRelative(x = horizontalPadding, y = (layoutHeight - icon.height).div(other = 2))
title.placeRelative(
x = horizontalPadding + icon.width,
y = when (state) {
is TokenItemState.NoAddress,
is TokenItemState.Unreachable,
-> (layoutHeight - title.height).div(other = 2)
else -> verticalPadding
},
)
fiatAmount?.placeRelative(x = layoutWidth - fiatAmount.width - horizontalPadding, y = verticalPadding)
priceChange?.placeRelative(
x = horizontalPadding + icon.width,
y = layoutHeight - priceChange.height - verticalPadding,
)
cryptoAmount?.placeRelative(
x = when (state) {
is TokenItemState.Draggable -> horizontalPadding + icon.width
else -> layoutWidth - cryptoAmount.width - horizontalPadding
},
y = layoutHeight - cryptoAmount.height - verticalPadding,
)
nonFiatContent.placeRelative(
x = layoutWidth - nonFiatContent.width - horizontalPadding,
y = (layoutHeight - nonFiatContent.height).div(other = 2),
)
}
}
}
private fun List<Measurable>.measureFiatAmount(
state: TokenItemState,
maxWidth: Int,
defaultConstraints: Constraints,
): Placeable {
return measure(
layoutId = LayoutId.FIAT_AMOUNT,
constraints = when (state) {
is TokenItemState.Content,
is TokenItemState.Draggable,
-> createConstrainsSafely(maxWidth = maxWidth)
else -> defaultConstraints
},
)
}
private fun List<Measurable>.measureCryptoAmount(
state: TokenItemState,
maxWidth: Int,
defaultConstraints: Constraints,
): Placeable {
return measure(
layoutId = LayoutId.CRYPTO_AMOUNT,
constraints = when (state) {
is TokenItemState.Content,
is TokenItemState.Draggable,
-> createConstrainsSafely(maxWidth = maxWidth)
else -> defaultConstraints
},
)
}
private fun List<Measurable>.measureTitle(
state: TokenItemState,
minWidth: Int,
remainingFreeSpace: Int,
defaultConstraints: Constraints,
): Placeable {
return measure(
layoutId = LayoutId.TITLE,
constraints = when (state) {
is TokenItemState.Content,
is TokenItemState.Draggable,
is TokenItemState.NoAddress,
is TokenItemState.Unreachable,
-> createDynamicConstrains(minWidth = minWidth, remainingFreeSpace = remainingFreeSpace)
else -> defaultConstraints
},
)
}
private fun List<Measurable>.measurePriceChange(
state: TokenItemState,
minWidth: Int,
remainingFreeSpace: Int,
defaultConstraints: Constraints,
): Placeable {
return measure(
layoutId = LayoutId.CRYPTO_PRICE,
constraints = when (state) {
is TokenItemState.Content,
-> createDynamicConstrains(minWidth = minWidth, remainingFreeSpace = remainingFreeSpace)
else -> defaultConstraints
},
)
}
private fun List<Measurable>.measure(layoutId: LayoutId, constraints: Constraints): Placeable {
return requireNotNull(
value = firstOrNull { it.layoutId == layoutId },
lazyMessage = { "Measurables[$layoutId] is null" },
).measure(constraints)
}
private fun createDynamicConstrains(minWidth: Int, remainingFreeSpace: Int): Constraints {
return createConstrainsSafely(
minWidth = minWidth,
maxWidth = max(a = minWidth, b = remainingFreeSpace),
)
}
private fun createConstrainsSafely(
minWidth: Int = 0,
maxWidth: Int = Constraints.Infinity,
minHeight: Int = 0,
maxHeight: Int = Constraints.Infinity,
): Constraints {
return Constraints(
minWidth = minWidth.makeNotLessZero(),
maxWidth = maxWidth.makeNotLessZero(),
minHeight = minHeight.makeNotLessZero(),
maxHeight = maxHeight.makeNotLessZero(),
)
}
private fun Int.makeNotLessZero(): Int = max(a = 0, b = this)
@Suppress("LongParameterList")
private fun calculateLayoutHeight(
state: TokenItemState,
minLayoutHeight: Int,
layoutPadding: Int,
title: Placeable,
fiatAmount: Placeable?,
cryptoAmount: Placeable?,
priceChange: Placeable?,
): Int {
val firstColumnHeight: Int
val secondColumnHeight: Int
when (state) {
is TokenItemState.Content,
is TokenItemState.Loading,
is TokenItemState.Locked,
-> {
firstColumnHeight = 2 * layoutPadding + title.height + (cryptoAmount?.height ?: 0)
secondColumnHeight = 2 * layoutPadding + (fiatAmount?.height ?: 0) + (priceChange?.height ?: 0)
}
is TokenItemState.Draggable,
is TokenItemState.NoAddress,
is TokenItemState.Unreachable,
-> {
firstColumnHeight = minLayoutHeight
secondColumnHeight = minLayoutHeight
}
}
return max(firstColumnHeight, secondColumnHeight).coerceAtLeast(minLayoutHeight)
}
@Preview(widthDp = 360)
@Composable
private fun Preview_TokenItem_InLight(@PreviewParameter(TokenItemStateProvider::class) state: TokenItemState) {
TangemThemePreview(isDark = false) {
TokenItem(state = state, isBalanceHidden = false)
}
}
private class TokenItemStateProvider : CollectionPreviewParameterProvider<TokenItemState>(
collection = listOf(
WalletPreviewData.tokenItemVisibleState.copy(
iconState = WalletPreviewData.coinIconState.copy(showCustomBadge = true),
titleState = TokenItemState.TitleState.Content(
text = "PolygonPolygonPolygonPolygonPolygonPolygon",
hasPending = true,
),
fiatAmountState = TokenItemState.FiatAmountState.Content(
text = "3213123123321312312312312312 $",
hasStaked = true,
),
cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = "5,4123123213123123123123123123 MATIC"),
cryptoPriceState = TokenItemState.CryptoPriceState.Content(
price = "312 USD",
priceChangePercent = "42.0%",
type = PriceChangeType.UP,
),
),
WalletPreviewData.tokenItemUnreachableState,
WalletPreviewData.tokenItemNoAddressState,
WalletPreviewData.tokenItemDragState,
WalletPreviewData.tokenItemHiddenState,
WalletPreviewData.loadingTokenItemState,
WalletPreviewData.testnetTokenItemVisibleState,
WalletPreviewData.customTokenItemVisibleState,
WalletPreviewData.customTestnetTokenItemVisibleState,
),
)

View file

@ -1,18 +0,0 @@
package com.tangem.feature.wallet.presentation.common.component.token
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.ui.res.TangemTheme
@Composable
internal fun LockedRectangle(modifier: Modifier = Modifier) {
Box(
modifier = modifier.background(
color = TangemTheme.colors.field.primary,
shape = RoundedCornerShape(TangemTheme.dimens.radius4),
),
)
}

View file

@ -1,76 +0,0 @@
package com.tangem.feature.wallet.presentation.common.component.token
import androidx.annotation.StringRes
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
import org.burnoutcrew.reorderable.ReorderableLazyListState
import org.burnoutcrew.reorderable.detectReorder
@Composable
internal fun NonFiatContentBlock(
state: TokenItemState,
reorderableTokenListState: ReorderableLazyListState?,
modifier: Modifier = Modifier,
) {
AnimatedContent(
targetState = state,
label = "Update non content fiat block",
modifier = modifier,
contentKey = { it::class.java },
) { animatedState ->
when (animatedState) {
is TokenItemState.Draggable -> DraggableImage(reorderableTokenListState = reorderableTokenListState)
is TokenItemState.Unreachable -> NonFiatContentText(text = R.string.common_unreachable)
is TokenItemState.NoAddress -> NonFiatContentText(text = R.string.common_no_address)
is TokenItemState.Content,
is TokenItemState.Loading,
is TokenItemState.Locked,
-> Unit
}
}
}
@Composable
private fun DraggableImage(reorderableTokenListState: ReorderableLazyListState?) {
Box(
modifier = Modifier
.size(size = TangemTheme.dimens.size32)
.then(
other = if (reorderableTokenListState != null) {
Modifier.detectReorder(reorderableTokenListState)
} else {
Modifier
},
),
contentAlignment = Alignment.Center,
) {
Icon(
painter = painterResource(id = R.drawable.ic_drag_24),
tint = TangemTheme.colors.icon.informative,
contentDescription = null,
)
}
}
@Composable
private fun NonFiatContentText(@StringRes text: Int) {
Text(
text = stringResource(id = text),
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = TangemTheme.typography.body2,
)
}

View file

@ -1,59 +0,0 @@
package com.tangem.feature.wallet.presentation.common.component.token
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.wallet.impl.R
import com.tangem.utils.StringsSigns
import com.tangem.feature.wallet.presentation.common.state.TokenItemState.CryptoAmountState as TokenCryptoAmountState
@Composable
internal fun TokenCryptoAmount(
state: TokenCryptoAmountState?,
isBalanceHidden: Boolean,
modifier: Modifier = Modifier,
) {
when (state) {
is TokenCryptoAmountState.Content -> {
CryptoAmountText(
amount = if (isBalanceHidden) StringsSigns.STARS else state.text,
modifier = modifier,
)
}
is TokenCryptoAmountState.Unreachable -> {
CryptoAmountText(amount = stringResource(id = R.string.common_unreachable), modifier = modifier)
}
is TokenCryptoAmountState.Loading -> {
RectangleShimmer(modifier = modifier.placeholderSize(), radius = TangemTheme.dimens.radius4)
}
is TokenCryptoAmountState.Locked -> {
LockedRectangle(modifier = modifier.placeholderSize())
}
null -> Unit
}
}
@Composable
private fun CryptoAmountText(amount: String, modifier: Modifier = Modifier) {
Text(
text = amount,
modifier = modifier,
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = TangemTheme.typography.caption2,
)
}
private fun Modifier.placeholderSize(): Modifier = composed {
return@composed this
.padding(vertical = TangemTheme.dimens.spacing2)
.size(width = TangemTheme.dimens.size40, height = TangemTheme.dimens.size12)
}

View file

@ -1,75 +0,0 @@
package com.tangem.feature.wallet.presentation.common.component.token
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextOverflow
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.wallet.impl.R
import com.tangem.utils.StringsSigns
import com.tangem.feature.wallet.presentation.common.state.TokenItemState.FiatAmountState as TokenFiatAmountState
@Composable
internal fun TokenFiatAmount(state: TokenFiatAmountState?, isBalanceHidden: Boolean, modifier: Modifier = Modifier) {
when (state) {
is TokenFiatAmountState.Content -> {
FiatAmountText(
text = if (isBalanceHidden) StringsSigns.STARS else state.text,
hasStaked = state.hasStaked,
modifier = modifier,
)
}
is TokenFiatAmountState.Loading -> {
RectangleShimmer(modifier = modifier.placeholderSize(), radius = TangemTheme.dimens.radius4)
}
is TokenFiatAmountState.Locked -> {
LockedRectangle(modifier = modifier.placeholderSize())
}
null -> Unit
}
}
@Composable
private fun FiatAmountText(text: String, hasStaked: Boolean, modifier: Modifier = Modifier) {
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
) {
AnimatedVisibility(
visible = hasStaked,
) {
Icon(
painter = rememberVectorPainter(image = ImageVector.vectorResource(R.drawable.ic_staking_24)),
contentDescription = null,
tint = TangemTheme.colors.icon.accent,
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing4)
.size(TangemTheme.dimens.size12),
)
}
Text(
text = text,
color = TangemTheme.colors.text.primary1,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = TangemTheme.typography.body2,
)
}
}
private fun Modifier.placeholderSize(): Modifier = composed {
return@composed this
.padding(vertical = TangemTheme.dimens.spacing4)
.size(width = TangemTheme.dimens.size40, height = TangemTheme.dimens.size12)
}

View file

@ -1,125 +0,0 @@
package com.tangem.feature.wallet.presentation.common.component.token
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.SpacerW4
import com.tangem.core.ui.components.SpacerW6
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.wallet.impl.R
import com.tangem.utils.StringsSigns.DASH_SIGN
import com.tangem.feature.wallet.presentation.common.state.TokenItemState.CryptoPriceState as TokenPriceChangeState
@Composable
internal fun TokenPrice(state: TokenPriceChangeState?, modifier: Modifier = Modifier) {
when (state) {
is TokenPriceChangeState.Content -> {
PriceBlock(
modifier = modifier,
price = state.price,
type = state.type,
priceChangePercent = state.priceChangePercent,
)
}
is TokenPriceChangeState.Unknown -> {
PriceText(text = DASH_SIGN, modifier = modifier)
}
is TokenPriceChangeState.Loading -> {
RectangleShimmer(modifier = modifier.placeholderSize(), radius = TangemTheme.dimens.radius4)
}
is TokenPriceChangeState.Locked -> {
LockedRectangle(modifier = modifier.placeholderSize())
}
null -> Unit
}
}
@Composable
private fun PriceBlock(
price: String,
modifier: Modifier = Modifier,
type: PriceChangeType? = null,
priceChangePercent: String? = null,
) {
Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) {
PriceText(text = price, modifier = Modifier.weight(weight = 1f, fill = false))
SpacerW6()
if (type != null) {
PriceChangeIcon(type = type)
SpacerW4()
}
PriceChangeText(type = type, text = priceChangePercent)
}
}
@Composable
private fun PriceText(text: String, modifier: Modifier = Modifier) {
AnimatedContent(targetState = text, label = "Update the price text", modifier = modifier) { animatedText ->
Text(
text = animatedText,
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = TangemTheme.typography.caption2,
)
}
}
@Composable
private fun PriceChangeIcon(type: PriceChangeType) {
AnimatedContent(targetState = type, label = "Update the price change's arrow") { animatedType ->
Icon(
painter = painterResource(
id = when (animatedType) {
PriceChangeType.UP -> R.drawable.ic_arrow_up_8
PriceChangeType.DOWN -> R.drawable.ic_arrow_down_8
PriceChangeType.NEUTRAL -> R.drawable.ic_elipse_8
},
),
tint = when (animatedType) {
PriceChangeType.UP -> TangemTheme.colors.icon.accent
PriceChangeType.DOWN -> TangemTheme.colors.icon.warning
PriceChangeType.NEUTRAL -> TangemTheme.colors.icon.inactive
},
contentDescription = null,
)
}
}
@Composable
private fun PriceChangeText(type: PriceChangeType?, text: String?, modifier: Modifier = Modifier) {
AnimatedContent(targetState = text, modifier = modifier, label = "Update the price change's text") { animatedText ->
Text(
text = animatedText ?: DASH_SIGN,
color = when (type) {
PriceChangeType.UP -> TangemTheme.colors.text.accent
PriceChangeType.DOWN -> TangemTheme.colors.text.warning
PriceChangeType.NEUTRAL -> TangemTheme.colors.text.disabled
null -> TangemTheme.colors.text.tertiary
},
overflow = TextOverflow.Ellipsis,
maxLines = 1,
style = TangemTheme.typography.caption2,
)
}
}
private fun Modifier.placeholderSize(): Modifier = composed {
return@composed this
.padding(vertical = TangemTheme.dimens.spacing2)
.size(width = TangemTheme.dimens.size52, height = TangemTheme.dimens.size12)
}

View file

@ -1,83 +0,0 @@
package com.tangem.feature.wallet.presentation.common.component.token
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.common.state.TokenItemState.TitleState as TokenTitleState
@Composable
internal fun TokenTitle(state: TokenTitleState?, modifier: Modifier = Modifier) {
when (state) {
is TokenTitleState.Content -> {
ContentTitle(name = state.text, hasPending = state.hasPending, modifier = modifier)
}
is TokenTitleState.Loading -> {
RectangleShimmer(modifier = modifier.placeholderSize(), radius = TangemTheme.dimens.radius4)
}
is TokenTitleState.Locked -> {
LockedRectangle(modifier = modifier.placeholderSize())
}
null -> Unit
}
}
@Composable
private fun ContentTitle(name: String, hasPending: Boolean, modifier: Modifier = Modifier) {
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing6),
verticalAlignment = Alignment.CenterVertically,
) {
/*
* If currency name has a long width, then it will completely displace the image.
* So we need to use [weight] to avoid displacement.
*/
CurrencyNameText(name = name, modifier = Modifier.weight(weight = 1f, fill = false))
PendingTransactionImage(
hasPending = hasPending,
modifier = Modifier.align(alignment = Alignment.CenterVertically),
)
}
}
@Composable
private fun CurrencyNameText(name: String, modifier: Modifier = Modifier) {
Text(
text = name,
modifier = modifier,
color = TangemTheme.colors.text.primary1,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
style = TangemTheme.typography.subtitle2,
)
}
@Composable
private fun PendingTransactionImage(hasPending: Boolean, modifier: Modifier = Modifier) {
AnimatedVisibility(visible = hasPending, modifier = modifier) {
Image(
painter = painterResource(id = R.drawable.img_loader_15),
contentDescription = null,
)
}
}
private fun Modifier.placeholderSize(): Modifier = composed {
return@composed this
.padding(vertical = TangemTheme.dimens.spacing4)
.size(width = TangemTheme.dimens.size70, height = TangemTheme.dimens.size12)
}

View file

@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.common.preview
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.event.consumedEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
@ -9,7 +10,6 @@ import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.common.WalletPreviewData.topBarConfig
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
import com.tangem.feature.wallet.presentation.wallet.state.model.*
import kotlinx.collections.immutable.persistentListOf

View file

@ -1,167 +0,0 @@
package com.tangem.feature.wallet.presentation.common.state
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.marketprice.PriceChangeType
/** Token item state */
@Immutable
internal sealed class TokenItemState {
abstract val id: String
abstract val iconState: CurrencyIconState
abstract val titleState: TitleState
abstract val fiatAmountState: FiatAmountState?
abstract val cryptoAmountState: CryptoAmountState?
abstract val cryptoPriceState: CryptoPriceState?
/** Loading token state */
data class Loading(
override val id: String,
override val iconState: CurrencyIconState,
override val titleState: TitleState.Content,
) : TokenItemState() {
override val fiatAmountState: FiatAmountState = FiatAmountState.Loading
override val cryptoAmountState: CryptoAmountState = CryptoAmountState.Loading
override val cryptoPriceState: CryptoPriceState = CryptoPriceState.Loading
}
/** Locked token state */
data class Locked(override val id: String) : TokenItemState() {
override val iconState: CurrencyIconState = CurrencyIconState.Locked
override val titleState: TitleState = TitleState.Locked
override val fiatAmountState: FiatAmountState = FiatAmountState.Locked
override val cryptoAmountState: CryptoAmountState = CryptoAmountState.Locked
override val cryptoPriceState: CryptoPriceState = CryptoPriceState.Locked
}
/**
* Content token state
*
* @property id unique id
* @property iconState token icon state
* @property titleState token name
* @property onItemClick callback which will be called when an item is clicked
* @property onItemLongClick callback which will be called when an item is long clicked
*/
data class Content(
override val id: String,
override val iconState: CurrencyIconState,
override val titleState: TitleState,
override val fiatAmountState: FiatAmountState,
override val cryptoAmountState: CryptoAmountState.Content,
override val cryptoPriceState: CryptoPriceState,
val onItemClick: () -> Unit,
val onItemLongClick: () -> Unit,
) : TokenItemState()
/**
* Draggable token state
*
* @property id unique id
* @property iconState token icon state
* @property titleState token name
*/
data class Draggable(
override val id: String,
override val iconState: CurrencyIconState,
override val titleState: TitleState,
override val cryptoAmountState: CryptoAmountState,
) : TokenItemState() {
override val fiatAmountState: FiatAmountState? = null
override val cryptoPriceState: CryptoPriceState? = null
}
/**
* Unreachable token state
*
* @property id token id
* @property iconState token icon state
* @property titleState token name
* @property onItemClick callback which will be called when an item is clicked
* @property onItemLongClick callback which will be called when an item is long clicked
*/
data class Unreachable(
override val id: String,
override val iconState: CurrencyIconState,
override val titleState: TitleState,
val onItemClick: () -> Unit,
val onItemLongClick: () -> Unit,
) : TokenItemState() {
override val fiatAmountState: FiatAmountState? = null
override val cryptoAmountState: CryptoAmountState? = null
override val cryptoPriceState: CryptoPriceState? = null
}
/**
* No derivation address state
*
* @property id token id
* @property iconState token icon state
* @property titleState token name
* @property onItemLongClick callback which will be called when an item is long clicked
*/
data class NoAddress(
override val id: String,
override val iconState: CurrencyIconState,
override val titleState: TitleState,
val onItemLongClick: () -> Unit,
) : TokenItemState() {
override val fiatAmountState: FiatAmountState? = null
override val cryptoAmountState: CryptoAmountState? = null
override val cryptoPriceState: CryptoPriceState? = null
}
@Immutable
sealed class TitleState {
data class Content(val text: String, val hasPending: Boolean = false) : TitleState()
object Loading : TitleState()
object Locked : TitleState()
}
@Immutable
sealed class FiatAmountState {
data class Content(
val text: String,
val hasStaked: Boolean = false,
) : FiatAmountState()
object Loading : FiatAmountState()
object Locked : FiatAmountState()
}
@Immutable
sealed class CryptoAmountState {
data class Content(val text: String) : CryptoAmountState()
object Unreachable : CryptoAmountState()
object Loading : CryptoAmountState()
object Locked : CryptoAmountState()
}
sealed class CryptoPriceState {
data class Content(
val price: String,
val priceChangePercent: String?,
val type: PriceChangeType?,
) : CryptoPriceState()
object Unknown : CryptoPriceState()
object Loading : CryptoPriceState()
object Locked : CryptoPriceState()
}
}

View file

@ -32,6 +32,7 @@ import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.SecondaryButton
import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig
import com.tangem.core.ui.components.buttons.actions.RoundedActionButton
import com.tangem.core.ui.components.token.TokenItem
import com.tangem.core.ui.event.EventEffect
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.res.TangemTheme
@ -40,7 +41,6 @@ import com.tangem.core.ui.utils.WindowInsetsZero
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.common.WalletPreviewData
import com.tangem.feature.wallet.presentation.common.component.DraggableNetworkGroupItem
import com.tangem.feature.wallet.presentation.common.component.TokenItem
import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem
import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState
import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState

View file

@ -1,7 +1,7 @@
package com.tangem.feature.wallet.presentation.organizetokens.model
import androidx.compose.runtime.Immutable
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem.RoundingMode
/**

View file

@ -1,10 +1,10 @@
package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem
import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getGroupHeaderId
import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getTokenItemId

View file

@ -1,9 +1,9 @@
package com.tangem.feature.wallet.presentation.wallet.state.model
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf

View file

@ -4,11 +4,11 @@ import com.tangem.common.extensions.isZero
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.components.marketprice.utils.PriceChangeConverter
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.staking.model.stakekit.YieldBalance
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents
import com.tangem.utils.Provider
import com.tangem.utils.StringsSigns.DASH_SIGN

View file

@ -2,9 +2,9 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrenc
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.ui.components.token.TokenItem
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.feature.wallet.presentation.common.component.NetworkGroupItem
import com.tangem.feature.wallet.presentation.common.component.TokenItem
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState.TokensListItemState
/**