Updated on 2026-08-14
This commit is contained in:
parent
3da4771d66
commit
3baed1e815
57 changed files with 1797 additions and 370 deletions
|
|
@ -0,0 +1,170 @@
|
|||
package com.tangem.common.ui.markets.tokenselector
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.components.SpacerW
|
||||
import com.tangem.core.ui.components.flicker
|
||||
import com.tangem.core.ui.extensions.resolveAnnotatedReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.utils.StringsSigns
|
||||
|
||||
@Composable
|
||||
fun BalanceColumn(balanceState: BalanceDisplayState, isBalanceHidden: Boolean, modifier: Modifier = Modifier) {
|
||||
if (isBalanceHidden) {
|
||||
HiddenBalance(modifier)
|
||||
return
|
||||
}
|
||||
when (balanceState) {
|
||||
is BalanceDisplayState.Loading -> LoadingBalanceColumn(modifier)
|
||||
is BalanceDisplayState.Flickering -> FlickeringBalanceColumn(balanceState, modifier)
|
||||
is BalanceDisplayState.Stale -> StaleBalanceColumn(balanceState, modifier)
|
||||
is BalanceDisplayState.Unreachable -> UnreachableBalanceColumn(modifier)
|
||||
is BalanceDisplayState.Loaded -> LoadedBalanceColumn(balanceState, modifier)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BalanceColumnLayout(modifier: Modifier = Modifier, content: @Composable ColumnScope.() -> Unit) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
horizontalAlignment = Alignment.End,
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LoadingBalanceColumn(modifier: Modifier = Modifier) {
|
||||
BalanceColumnLayout(modifier) {
|
||||
RectangleShimmer(modifier = Modifier.size(width = 108.dp, height = 20.dp), radius = 20.dp)
|
||||
RectangleShimmer(modifier = Modifier.size(width = 64.dp, height = 16.dp), radius = 20.dp)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FlickeringBalanceColumn(state: BalanceDisplayState.Flickering, modifier: Modifier = Modifier) {
|
||||
val flickerModifier = Modifier.flicker(isFlickering = true)
|
||||
BalanceColumnLayout(modifier) {
|
||||
Text(
|
||||
modifier = flickerModifier,
|
||||
text = state.fiatBalance.resolveAnnotatedReference(),
|
||||
style = TangemTheme.typography2.bodyMedium16,
|
||||
color = TangemTheme.colors2.text.neutral.primary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Text(
|
||||
modifier = flickerModifier,
|
||||
text = state.cryptoBalance.resolveReference(),
|
||||
style = TangemTheme.typography2.captionMedium12,
|
||||
color = TangemTheme.colors2.text.neutral.secondary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StaleBalanceColumn(state: BalanceDisplayState.Stale, modifier: Modifier = Modifier) {
|
||||
BalanceColumnLayout(modifier) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
modifier = Modifier.size(16.dp),
|
||||
painter = painterResource(id = R.drawable.ic_error_sync_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors2.markers.iconGray,
|
||||
)
|
||||
SpacerW(TangemTheme.dimens2.x0_5)
|
||||
Text(
|
||||
text = state.fiatBalance.resolveAnnotatedReference(),
|
||||
style = TangemTheme.typography2.bodyMedium16,
|
||||
color = TangemTheme.colors2.text.neutral.primary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = state.cryptoBalance.resolveReference(),
|
||||
style = TangemTheme.typography2.captionMedium12,
|
||||
color = TangemTheme.colors2.text.neutral.secondary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UnreachableBalanceColumn(modifier: Modifier = Modifier) {
|
||||
BalanceColumnLayout(modifier) {
|
||||
Text(
|
||||
text = StringsSigns.DASH_SIGN,
|
||||
style = TangemTheme.typography2.bodyMedium16,
|
||||
color = TangemTheme.colors2.text.neutral.primary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.common_unreachable),
|
||||
style = TangemTheme.typography2.captionMedium12,
|
||||
color = TangemTheme.colors2.text.status.attention,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
SpacerW(TangemTheme.dimens2.x0_5)
|
||||
Icon(
|
||||
modifier = Modifier.size(16.dp),
|
||||
painter = painterResource(id = R.drawable.ic_alert_triange_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors2.graphic.status.attention,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LoadedBalanceColumn(state: BalanceDisplayState.Loaded, modifier: Modifier = Modifier) {
|
||||
BalanceColumnLayout(modifier) {
|
||||
Text(
|
||||
text = state.fiatBalance.resolveAnnotatedReference(),
|
||||
style = TangemTheme.typography2.bodySemibold16,
|
||||
color = TangemTheme.colors2.text.neutral.primary,
|
||||
maxLines = 1,
|
||||
)
|
||||
Text(
|
||||
text = state.cryptoBalance.resolveReference(),
|
||||
style = TangemTheme.typography2.captionMedium12,
|
||||
color = TangemTheme.colors2.text.neutral.secondary,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HiddenBalance(modifier: Modifier = Modifier) {
|
||||
BalanceColumnLayout(modifier) {
|
||||
Text(
|
||||
text = StringsSigns.THREE_STARS,
|
||||
style = TangemTheme.typography2.bodySemibold16,
|
||||
color = TangemTheme.colors2.text.neutral.primary,
|
||||
maxLines = 1,
|
||||
)
|
||||
Text(
|
||||
text = StringsSigns.THREE_STARS,
|
||||
style = TangemTheme.typography2.captionMedium12,
|
||||
color = TangemTheme.colors2.text.neutral.secondary,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
package com.tangem.common.ui.markets.tokenselector
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.layoutId
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.ds.button.*
|
||||
import com.tangem.core.ui.ds.image.TangemIconUM
|
||||
import com.tangem.core.ui.ds.row.TangemRowContainer
|
||||
import com.tangem.core.ui.ds.row.TangemRowLayoutId
|
||||
import com.tangem.core.ui.extensions.pluralStringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
@Composable
|
||||
fun GroupedUserAssetItem(item: UserAssetItemUM.Grouped, modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier = modifier.background(
|
||||
color = TangemTheme.colors2.surface.level3,
|
||||
shape = RoundedCornerShape(TangemTheme.dimens2.x5),
|
||||
),
|
||||
) {
|
||||
TangemRowContainer(
|
||||
modifier = Modifier.clickable(onClick = item.onClick),
|
||||
content = {
|
||||
LayeringIcons(
|
||||
modifier = Modifier
|
||||
.layoutId(layoutId = TangemRowLayoutId.HEAD)
|
||||
.padding(end = TangemTheme.dimens2.x1),
|
||||
tangemIconUM = item.icon,
|
||||
count = item.tokensCount,
|
||||
)
|
||||
|
||||
Text(
|
||||
modifier = Modifier.layoutId(TangemRowLayoutId.START_TOP),
|
||||
text = item.tokenName,
|
||||
style = TangemTheme.typography2.bodyMedium16,
|
||||
color = TangemTheme.colors2.text.neutral.primary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.MiddleEllipsis,
|
||||
)
|
||||
|
||||
Text(
|
||||
modifier = Modifier.layoutId(TangemRowLayoutId.START_BOTTOM),
|
||||
text = pluralStringResourceSafe(R.plurals.common_tokens_count, item.tokensCount, item.tokensCount),
|
||||
style = TangemTheme.typography2.captionMedium12,
|
||||
color = TangemTheme.colors2.text.neutral.secondary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
|
||||
BalanceColumn(
|
||||
modifier = Modifier.layoutId(layoutId = TangemRowLayoutId.END_TOP),
|
||||
balanceState = item.balanceState,
|
||||
isBalanceHidden = item.isBalanceHidden,
|
||||
)
|
||||
|
||||
TangemButton(
|
||||
modifier = Modifier
|
||||
.padding(start = TangemTheme.dimens2.x2)
|
||||
.layoutId(TangemRowLayoutId.TAIL),
|
||||
buttonUM = TangemButtonUM(
|
||||
type = TangemButtonType.Secondary,
|
||||
tangemIconUM = TangemIconUM.Icon(
|
||||
iconRes = R.drawable.ic_chevron_24,
|
||||
tintReference = { TangemTheme.colors2.graphic.neutral.primary },
|
||||
),
|
||||
shape = TangemButtonShape.Rounded,
|
||||
size = TangemButtonSize.X10,
|
||||
onClick = item.onClick,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
package com.tangem.common.ui.markets.tokenselector
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.ds.image.TangemIcon
|
||||
import com.tangem.core.ui.ds.image.TangemIconUM
|
||||
import com.tangem.core.ui.res.TangemColorPalette
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
|
||||
private const val MIN_STACKED_ICON_COUNT = 1
|
||||
private const val MAX_STACKED_ICON_COUNT = 3
|
||||
private const val FIRST_BACK_LAYER_HORIZONTAL_OFFSET_MULTIPLIER = 1
|
||||
private const val SECOND_BACK_LAYER_HORIZONTAL_OFFSET_MULTIPLIER = 2
|
||||
private const val MIN_STACKED_COUNT_FOR_FIRST_BACK_LAYER = 2
|
||||
private const val FIRST_BACK_LAYER_ICON_ALPHA = 0.4f
|
||||
private const val SECOND_BACK_LAYER_ICON_ALPHA = 0.2f
|
||||
|
||||
@Composable
|
||||
fun LayeringIcons(
|
||||
tangemIconUM: TangemIconUM,
|
||||
modifier: Modifier = Modifier,
|
||||
count: Int = MIN_STACKED_ICON_COUNT,
|
||||
layerHorizontalShift: Dp = TangemTheme.dimens2.x1,
|
||||
iconSize: Dp = TangemTheme.dimens2.x10,
|
||||
) {
|
||||
require(count > 0)
|
||||
|
||||
val stackTrailingWidth = layerHorizontalShift * SECOND_BACK_LAYER_HORIZONTAL_OFFSET_MULTIPLIER
|
||||
|
||||
Box(
|
||||
modifier = modifier.size(
|
||||
width = iconSize + stackTrailingWidth,
|
||||
height = iconSize,
|
||||
),
|
||||
) {
|
||||
val baseIconModifier = Modifier
|
||||
.align(Alignment.TopStart)
|
||||
.size(iconSize)
|
||||
|
||||
if (count >= MAX_STACKED_ICON_COUNT) {
|
||||
TangemIcon(
|
||||
tangemIconUM = tangemIconUM,
|
||||
modifier = baseIconModifier
|
||||
.offset(x = layerHorizontalShift * SECOND_BACK_LAYER_HORIZONTAL_OFFSET_MULTIPLIER)
|
||||
.alpha(SECOND_BACK_LAYER_ICON_ALPHA),
|
||||
)
|
||||
}
|
||||
if (count >= MIN_STACKED_COUNT_FOR_FIRST_BACK_LAYER) {
|
||||
TangemIcon(
|
||||
tangemIconUM = tangemIconUM,
|
||||
modifier = baseIconModifier
|
||||
.offset(x = layerHorizontalShift * FIRST_BACK_LAYER_HORIZONTAL_OFFSET_MULTIPLIER)
|
||||
.alpha(FIRST_BACK_LAYER_ICON_ALPHA),
|
||||
)
|
||||
}
|
||||
TangemIcon(
|
||||
tangemIconUM = tangemIconUM,
|
||||
modifier = baseIconModifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Composable
|
||||
private fun LayeringIconsPreview() {
|
||||
val previewCurrencyIcon = TangemIconUM.Currency(
|
||||
currencyIconState = CurrencyIconState.TokenIcon(
|
||||
url = null,
|
||||
topBadgeIconResId = null,
|
||||
fallbackTint = TangemColorPalette.Black,
|
||||
fallbackBackground = TangemColorPalette.Meadow,
|
||||
isGrayscale = false,
|
||||
shouldShowCustomBadge = false,
|
||||
),
|
||||
)
|
||||
TangemThemePreview {
|
||||
Column(horizontalAlignment = Alignment.End) {
|
||||
LayeringIcons(count = MIN_STACKED_ICON_COUNT, tangemIconUM = previewCurrencyIcon)
|
||||
LayeringIcons(
|
||||
count = MAX_STACKED_ICON_COUNT,
|
||||
tangemIconUM = previewCurrencyIcon,
|
||||
)
|
||||
LayeringIcons(
|
||||
count = MIN_STACKED_COUNT_FOR_FIRST_BACK_LAYER,
|
||||
tangemIconUM = previewCurrencyIcon,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
package com.tangem.common.ui.markets.tokenselector
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.layoutId
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeInPercent
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeState
|
||||
import com.tangem.core.ui.ds.image.TangemIcon
|
||||
import com.tangem.core.ui.ds.row.TangemRowContainer
|
||||
import com.tangem.core.ui.ds.row.TangemRowLayoutId
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
@Composable
|
||||
fun SingleUserAssetItem(shouldUsePriceBlock: Boolean, item: UserAssetItemUM.Single, modifier: Modifier = Modifier) {
|
||||
TangemRowContainer(
|
||||
modifier = modifier.clickable(onClick = item.onClick),
|
||||
content = {
|
||||
TangemIcon(
|
||||
modifier = Modifier
|
||||
.layoutId(layoutId = TangemRowLayoutId.HEAD)
|
||||
.size(40.dp)
|
||||
.padding(end = TangemTheme.dimens2.x1),
|
||||
tangemIconUM = item.icon,
|
||||
)
|
||||
|
||||
Text(
|
||||
modifier = Modifier.layoutId(TangemRowLayoutId.START_TOP),
|
||||
text = item.tokenName,
|
||||
style = TangemTheme.typography2.bodyMedium16,
|
||||
color = TangemTheme.colors2.text.neutral.primary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.MiddleEllipsis,
|
||||
)
|
||||
|
||||
if (shouldUsePriceBlock) {
|
||||
PriceBlock(
|
||||
modifier = Modifier.layoutId(TangemRowLayoutId.START_BOTTOM),
|
||||
priceChangeState = item.priceChangeState,
|
||||
fiatRate = item.fiatRate,
|
||||
balanceState = item.balanceState,
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
modifier = Modifier.layoutId(TangemRowLayoutId.START_BOTTOM),
|
||||
text = item.networkName,
|
||||
style = TangemTheme.typography2.captionMedium12,
|
||||
color = TangemTheme.colors2.text.neutral.secondary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.MiddleEllipsis,
|
||||
)
|
||||
}
|
||||
|
||||
BalanceColumn(
|
||||
modifier = Modifier.layoutId(layoutId = TangemRowLayoutId.END_TOP),
|
||||
balanceState = item.balanceState,
|
||||
isBalanceHidden = item.isBalanceHidden,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PriceBlock(
|
||||
priceChangeState: PriceChangeState,
|
||||
fiatRate: String?,
|
||||
balanceState: BalanceDisplayState,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val isDisabled = remember(balanceState) {
|
||||
balanceState is BalanceDisplayState.Unreachable
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = modifier,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1),
|
||||
) {
|
||||
if (fiatRate != null) {
|
||||
Text(
|
||||
text = fiatRate,
|
||||
style = TangemTheme.typography2.captionMedium12,
|
||||
color = if (isDisabled) {
|
||||
TangemTheme.colors2.text.status.disabled
|
||||
} else {
|
||||
TangemTheme.colors2.text.neutral.secondary
|
||||
},
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
|
||||
AnimatedContent(
|
||||
targetState = priceChangeState,
|
||||
contentKey = { it::class },
|
||||
) { animatedState ->
|
||||
when (animatedState) {
|
||||
is PriceChangeState.Content -> {
|
||||
PriceChangeInPercent(
|
||||
valueInPercent = animatedState.valueInPercent,
|
||||
type = animatedState.type,
|
||||
textStyle = TangemTheme.typography2.captionMedium12,
|
||||
isDisabled = isDisabled,
|
||||
)
|
||||
}
|
||||
PriceChangeState.Unknown -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,201 @@
|
|||
package com.tangem.common.ui.markets.tokenselector
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.core.EaseOut
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
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.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.Fade
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetType
|
||||
import com.tangem.core.ui.components.haze.hazeEffectTangem
|
||||
import com.tangem.core.ui.components.haze.hazeSourceTangem
|
||||
import com.tangem.core.ui.ds.button.*
|
||||
import com.tangem.core.ui.ds.topbar.TangemTopBar
|
||||
import com.tangem.core.ui.ds.topbar.TangemTopBarType
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.clickableSingle
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||
import dev.chrisbanes.haze.HazeProgressive
|
||||
import dev.chrisbanes.haze.HazeState
|
||||
import dev.chrisbanes.haze.rememberHazeState
|
||||
|
||||
@Composable
|
||||
fun TokenSelectorBottomSheet(config: TangemBottomSheetConfig, stickyFooter: StickyFooter? = null) {
|
||||
TangemBottomSheet<TokenSelectorContentUM>(
|
||||
config = config,
|
||||
type = TangemBottomSheetType.Modal,
|
||||
containerColor = TangemTheme.colors2.surface.level2,
|
||||
content = { content ->
|
||||
TokenSelectorContent(
|
||||
content = content,
|
||||
onDismiss = config.onDismissRequest,
|
||||
stickyFooter = stickyFooter,
|
||||
embedded = false,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
data class StickyFooter(val buttonText: TextReference, val isEnabled: Boolean = true, val onClick: () -> Unit)
|
||||
|
||||
@Composable
|
||||
fun TokenSelectorEmbeddedContent(
|
||||
content: TokenSelectorContentUM,
|
||||
stickyFooter: StickyFooter?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
TokenSelectorContent(
|
||||
content = content,
|
||||
onDismiss = {},
|
||||
stickyFooter = stickyFooter,
|
||||
embedded = true,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TokenSelectorContent(
|
||||
content: TokenSelectorContentUM,
|
||||
onDismiss: () -> Unit,
|
||||
stickyFooter: StickyFooter?,
|
||||
embedded: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val hazeState = rememberHazeState()
|
||||
var topBarHeight by remember { mutableStateOf(0.dp) }
|
||||
val topContentPadding = if (embedded) {
|
||||
TangemTheme.dimens2.x4
|
||||
} else {
|
||||
topBarHeight
|
||||
}
|
||||
val footerHeight = TangemTheme.dimens2.x14 + TangemTheme.dimens2.x4
|
||||
val listBottomPadding = TangemTheme.dimens2.x10 + if (stickyFooter != null) footerHeight else 0.dp
|
||||
|
||||
Box(modifier = modifier.fillMaxWidth()) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.hazeSourceTangem(state = hazeState, 1f),
|
||||
contentPadding = PaddingValues(
|
||||
start = TangemTheme.dimens2.x4,
|
||||
end = TangemTheme.dimens2.x4,
|
||||
top = topContentPadding,
|
||||
bottom = listBottomPadding,
|
||||
),
|
||||
) {
|
||||
tokenSelectorSectionItems(content.sections)
|
||||
}
|
||||
if (!embedded) {
|
||||
TokenSelectorSheetTopBar(
|
||||
modifier = Modifier.align(Alignment.TopEnd),
|
||||
onDismiss = onDismiss,
|
||||
hazeState = hazeState,
|
||||
onChangeHeight = { topBarHeight = it },
|
||||
)
|
||||
}
|
||||
Fade(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.align(Alignment.BottomCenter)
|
||||
.padding(bottom = if (stickyFooter != null) footerHeight else 0.dp),
|
||||
height = TangemTheme.dimens2.x10,
|
||||
)
|
||||
if (stickyFooter != null) {
|
||||
TangemButton(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.navigationBarsPadding()
|
||||
.padding(horizontal = TangemTheme.dimens2.x4, vertical = TangemTheme.dimens2.x2)
|
||||
.fillMaxWidth(),
|
||||
buttonUM = TangemButtonUM(
|
||||
type = TangemButtonType.Primary,
|
||||
text = stickyFooter.buttonText,
|
||||
shape = TangemButtonShape.Rounded,
|
||||
size = TangemButtonSize.X15,
|
||||
isEnabled = stickyFooter.isEnabled,
|
||||
onClick = stickyFooter.onClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TokenSelectorSheetTopBar(
|
||||
hazeState: HazeState,
|
||||
onDismiss: () -> Unit,
|
||||
onChangeHeight: (Dp) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val bgColor = TangemTheme.colors2.surface.level2
|
||||
val density = LocalDensity.current
|
||||
TangemTopBar(
|
||||
modifier = modifier
|
||||
.onGloballyPositioned { coordinates ->
|
||||
if (coordinates.size.height > 0) {
|
||||
with(density) {
|
||||
onChangeHeight(coordinates.size.height.toDp())
|
||||
}
|
||||
}
|
||||
}
|
||||
.hazeEffectTangem(state = hazeState) {
|
||||
backgroundColor = bgColor
|
||||
progressive = HazeProgressive.verticalGradient(
|
||||
startIntensity = .55f,
|
||||
endIntensity = 0f,
|
||||
preferPerformance = true,
|
||||
easing = EaseOut,
|
||||
)
|
||||
},
|
||||
type = TangemTopBarType.BottomSheet,
|
||||
title = resourceReference(R.string.markets_search_portfolio_header),
|
||||
endContent = {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(id = R.drawable.ic_close_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors2.graphic.neutral.primary,
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens2.x11)
|
||||
.background(
|
||||
color = TangemTheme.colors2.button.backgroundSecondary,
|
||||
shape = CircleShape,
|
||||
)
|
||||
.clickableSingle(onClick = onDismiss)
|
||||
.padding(TangemTheme.dimens2.x2_5),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview(name = "Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun TokenSelectorBottomSheetPreview(
|
||||
@PreviewParameter(TokenSelectorContentPreviewProvider::class) content: TokenSelectorContentUM,
|
||||
) {
|
||||
TangemThemePreviewRedesign {
|
||||
TokenSelectorBottomSheet(
|
||||
config = TangemBottomSheetConfig(
|
||||
onDismissRequest = {},
|
||||
content = content,
|
||||
isShown = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
package com.tangem.common.ui.markets.tokenselector
|
||||
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeState
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.ds.image.TangemIconUM
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.models.account.CryptoPortfolioIcon
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@Suppress("StringLiteralDuplication")
|
||||
class TokenSelectorContentPreviewProvider :
|
||||
CollectionPreviewParameterProvider<TokenSelectorContentUM>(
|
||||
listOf(
|
||||
tokenSelectorPreviewSimple(),
|
||||
tokenSelectorPreviewWithAccountHeaders(),
|
||||
tokenSelectorPreviewMultiWallet(),
|
||||
),
|
||||
)
|
||||
|
||||
private fun tokenSelectorPreviewSimple(): TokenSelectorContentUM {
|
||||
return TokenSelectorContentUM(
|
||||
sections = persistentListOf(
|
||||
TokenSelectorSectionUM.TokenGroup(
|
||||
accountHeader = null,
|
||||
items = persistentListOf(
|
||||
previewTokenItem(id = "eth", name = "Ethereum", symbol = "ETH"),
|
||||
previewTokenItem(id = "btc", name = "Bitcoin", symbol = "BTC"),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun tokenSelectorPreviewWithAccountHeaders(): TokenSelectorContentUM {
|
||||
val accountIcon = CryptoPortfolioIcon.ofCustomAccount(
|
||||
value = CryptoPortfolioIcon.Icon.Wallet,
|
||||
color = CryptoPortfolioIcon.Color.CaribbeanBlue,
|
||||
)
|
||||
return TokenSelectorContentUM(
|
||||
sections = persistentListOf(
|
||||
TokenSelectorSectionUM.TokenGroup(
|
||||
accountHeader = AccountHeaderData(
|
||||
accountName = stringReference(value = "Main account"),
|
||||
cryptoPortfolioIcon = accountIcon,
|
||||
),
|
||||
items = persistentListOf(
|
||||
previewTokenItem(id = "eth_main", name = "Ethereum", symbol = "ETH"),
|
||||
),
|
||||
),
|
||||
TokenSelectorSectionUM.TokenGroup(
|
||||
accountHeader = AccountHeaderData(
|
||||
accountName = stringReference(value = "Trading"),
|
||||
cryptoPortfolioIcon = accountIcon,
|
||||
),
|
||||
items = persistentListOf(
|
||||
previewTokenItem(id = "sol_trade", name = "Solana", symbol = "SOL"),
|
||||
previewTokenItem(id = "avax_trade", name = "Avalanche", symbol = "AVAX"),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun tokenSelectorPreviewMultiWallet(): TokenSelectorContentUM {
|
||||
return TokenSelectorContentUM(
|
||||
sections = persistentListOf(
|
||||
TokenSelectorSectionUM.WalletHeader(walletName = "Cold wallet"),
|
||||
TokenSelectorSectionUM.TokenGroup(
|
||||
accountHeader = null,
|
||||
items = persistentListOf(
|
||||
previewTokenItem(id = "btc_cold", name = "Bitcoin", symbol = "BTC"),
|
||||
),
|
||||
),
|
||||
TokenSelectorSectionUM.WalletHeader(walletName = "Hot wallet"),
|
||||
TokenSelectorSectionUM.TokenGroup(
|
||||
accountHeader = null,
|
||||
items = persistentListOf(
|
||||
previewTokenItem(id = "eth_hot", name = "Ethereum", symbol = "ETH"),
|
||||
previewTokenItem(id = "usdt_hot", name = "Tether", symbol = "USDT"),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun previewTokenItem(id: String, name: String, symbol: String): UserAssetItemUM.Single {
|
||||
val cryptoRef = stringReference(value = "1.234 $symbol")
|
||||
val fiatRef = stringReference(value = "$1,234.56")
|
||||
return UserAssetItemUM.Single(
|
||||
id = id,
|
||||
icon = TangemIconUM.Currency(
|
||||
CurrencyIconState.CoinIcon(
|
||||
url = null,
|
||||
fallbackResId = R.drawable.ic_ethereumpow_22,
|
||||
isGrayscale = false,
|
||||
shouldShowCustomBadge = false,
|
||||
),
|
||||
),
|
||||
tokenName = name,
|
||||
tokenSymbol = symbol,
|
||||
fiatRate = "$98,765.43",
|
||||
priceChangeState = PriceChangeState.Content(
|
||||
valueInPercent = "+2.34%",
|
||||
type = PriceChangeType.UP,
|
||||
),
|
||||
balanceState = BalanceDisplayState.Loaded(
|
||||
cryptoBalance = cryptoRef,
|
||||
fiatBalance = fiatRef,
|
||||
),
|
||||
isBalanceHidden = false,
|
||||
onClick = {},
|
||||
networkName = "Ethereum",
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package com.tangem.common.ui.markets.tokenselector
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.models.account.CryptoPortfolioIcon
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Immutable
|
||||
data class TokenSelectorContentUM(
|
||||
val sections: ImmutableList<TokenSelectorSectionUM>,
|
||||
) : TangemBottomSheetConfigContent
|
||||
|
||||
@Immutable
|
||||
data class AccountHeaderData(
|
||||
val accountName: TextReference,
|
||||
val cryptoPortfolioIcon: CryptoPortfolioIcon,
|
||||
)
|
||||
|
||||
@Immutable
|
||||
sealed interface TokenSelectorSectionUM {
|
||||
|
||||
data class WalletHeader(val walletName: String) : TokenSelectorSectionUM
|
||||
|
||||
data class TokenGroup(
|
||||
val accountHeader: AccountHeaderData?,
|
||||
val items: ImmutableList<UserAssetItemUM.Single>,
|
||||
) : TokenSelectorSectionUM
|
||||
}
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
package com.tangem.common.ui.markets.tokenselector
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
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.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import com.tangem.common.ui.account.getResId
|
||||
import com.tangem.common.ui.account.getUiColor
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
fun LazyListScope.tokenSelectorSectionItems(sections: ImmutableList<TokenSelectorSectionUM>) {
|
||||
sections.forEachIndexed { index, section ->
|
||||
when (section) {
|
||||
is TokenSelectorSectionUM.WalletHeader -> {
|
||||
item(key = "wallet_${section.walletName}_$index") {
|
||||
WalletHeaderSection(section)
|
||||
}
|
||||
}
|
||||
is TokenSelectorSectionUM.TokenGroup -> {
|
||||
if (section.items.isEmpty()) return@forEachIndexed
|
||||
|
||||
if (index > 0 && sections[index - 1] is TokenSelectorSectionUM.TokenGroup) {
|
||||
item(key = "spacer_before_group_$index") {
|
||||
SpacerH(TangemTheme.dimens2.x2)
|
||||
}
|
||||
}
|
||||
|
||||
val lastIndex = if (section.accountHeader != null) {
|
||||
section.items.size
|
||||
} else {
|
||||
section.items.lastIndex.coerceAtLeast(0)
|
||||
}
|
||||
|
||||
section.accountHeader?.let { header ->
|
||||
item(key = "account_header_$index") {
|
||||
TokenGroupAccountHeaderRow(
|
||||
data = header,
|
||||
modifier = Modifier.tokenGroupRowDecoration(
|
||||
currentIndex = 0,
|
||||
lastIndex = lastIndex,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val indexOffset = if (section.accountHeader != null) 1 else 0
|
||||
section.items.forEachIndexed { itemIndex, single ->
|
||||
item(key = "token_${single.id}_$index") {
|
||||
SingleUserAssetItem(
|
||||
item = single,
|
||||
modifier = Modifier.tokenGroupRowDecoration(
|
||||
currentIndex = indexOffset + itemIndex,
|
||||
lastIndex = lastIndex,
|
||||
),
|
||||
shouldUsePriceBlock = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Modifier.tokenGroupRowDecoration(currentIndex: Int, lastIndex: Int): Modifier =
|
||||
this.roundedShapeItemDecoration(
|
||||
currentIndex = currentIndex,
|
||||
lastIndex = lastIndex,
|
||||
addDefaultPadding = false,
|
||||
radius = TangemTheme.dimens2.x6,
|
||||
backgroundColor = TangemTheme.colors2.surface.level3,
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun WalletHeaderSection(section: TokenSelectorSectionUM.WalletHeader) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens2.x4, bottom = TangemTheme.dimens2.x2)
|
||||
.padding(horizontal = TangemTheme.dimens2.x3),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1),
|
||||
) {
|
||||
Text(
|
||||
text = section.walletName,
|
||||
style = TangemTheme.typography2.subheadlineMedium14,
|
||||
color = TangemTheme.colors2.text.neutral.primary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_key_card_20),
|
||||
modifier = Modifier.size(TangemTheme.dimens2.x5),
|
||||
tint = TangemTheme.colors2.graphic.neutral.tertiary,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TokenGroupAccountHeaderRow(data: AccountHeaderData, modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = TangemTheme.dimens2.x4, bottom = TangemTheme.dimens2.x2)
|
||||
.padding(horizontal = TangemTheme.dimens2.x4),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2),
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(TangemTheme.dimens2.x4),
|
||||
imageVector = ImageVector.vectorResource(data.cryptoPortfolioIcon.value.getResId()),
|
||||
tint = data.cryptoPortfolioIcon.color.getUiColor(),
|
||||
contentDescription = null,
|
||||
)
|
||||
Text(
|
||||
text = data.accountName.resolveReference(),
|
||||
style = TangemTheme.typography2.captionMedium12,
|
||||
color = TangemTheme.colors2.text.neutral.primary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package com.tangem.common.ui.markets.tokenselector
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeState
|
||||
import com.tangem.core.ui.ds.image.TangemIconUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
@Immutable
|
||||
sealed interface BalanceDisplayState {
|
||||
|
||||
data class Loaded(
|
||||
val cryptoBalance: TextReference,
|
||||
val fiatBalance: TextReference,
|
||||
) : BalanceDisplayState
|
||||
|
||||
data class Flickering(
|
||||
val cryptoBalance: TextReference,
|
||||
val fiatBalance: TextReference,
|
||||
) : BalanceDisplayState
|
||||
|
||||
data class Stale(
|
||||
val cryptoBalance: TextReference,
|
||||
val fiatBalance: TextReference,
|
||||
) : BalanceDisplayState
|
||||
|
||||
data object Loading : BalanceDisplayState
|
||||
data object Unreachable : BalanceDisplayState
|
||||
}
|
||||
|
||||
@Immutable
|
||||
sealed interface UserAssetItemUM {
|
||||
val id: String
|
||||
val icon: TangemIconUM
|
||||
val tokenName: String
|
||||
val tokenSymbol: String
|
||||
val onClick: () -> Unit
|
||||
|
||||
data class Single(
|
||||
override val id: String,
|
||||
override val icon: TangemIconUM,
|
||||
override val tokenName: String,
|
||||
override val tokenSymbol: String,
|
||||
val fiatRate: String?,
|
||||
val priceChangeState: PriceChangeState,
|
||||
val balanceState: BalanceDisplayState,
|
||||
val isBalanceHidden: Boolean,
|
||||
val networkName: String,
|
||||
override val onClick: () -> Unit,
|
||||
) : UserAssetItemUM
|
||||
|
||||
data class Grouped(
|
||||
override val id: String,
|
||||
override val icon: TangemIconUM,
|
||||
override val tokenName: String,
|
||||
override val tokenSymbol: String,
|
||||
val tokensCount: Int,
|
||||
val balanceState: BalanceDisplayState,
|
||||
val isBalanceHidden: Boolean,
|
||||
override val onClick: () -> Unit,
|
||||
) : UserAssetItemUM
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue