Updated on 2026-08-14

This commit is contained in:
Tangem 2026-05-05 08:36:44 +02:00
parent c0fa56e200
commit 369f3bbd44
19 changed files with 283 additions and 86 deletions

View file

@ -87,7 +87,7 @@ internal class MarketsTokenDetailsModel @Inject constructor(
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
getUserCountryUseCase: GetUserCountryUseCase,
paramsContainer: ParamsContainer,
private val addToPortfolioManagerFactory: AddToPortfolioManager.Factory,
addToPortfolioManagerFactory: AddToPortfolioManager.Factory,
private val designFeatureToggles: DesignFeatureToggles,
private val getTokenPriceChartUseCase: GetTokenPriceChartUseCase,
private val getTokenMarketInfoUseCase: GetTokenMarketInfoUseCase,
@ -247,6 +247,7 @@ internal class MarketsTokenDetailsModel @Inject constructor(
val state = MutableStateFlow(
MarketsTokenDetailsUM(
tokenName = params.token.name,
symbol = params.token.symbol,
priceText = params.token.tokenQuotes.currentPrice.format {
fiat(
fiatCurrencyCode = currentAppCurrency.value.code,

View file

@ -21,6 +21,7 @@ import com.tangem.core.ui.extensions.conditionalCompose
import com.tangem.core.ui.res.LocalHazeState
import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.WindowInsetsZero
import com.tangem.features.feed.components.FeedEntryChildFactory
import com.tangem.features.feed.ui.utils.contentFeedEntryStackAnimation
@ -138,7 +139,9 @@ private fun EntryContentV2(
},
)
.hazeSourceTangem(zIndex = 0f, state = hazeState),
contentPadding = PaddingValues(top = topBarHeight),
contentPadding = PaddingValues(
top = if (isOpenedInBottomSheet) topBarHeight else TangemTheme.dimens2.x2_5,
),
bottomSheetState = bottomSheetState,
)
}

View file

@ -266,13 +266,13 @@ private fun LazyListScope.bestOpportunitiesItemsV2(state: EarnBestOpportunitiesU
is EarnBestOpportunitiesUM.Empty -> {
item(key = "best_opportunities_empty") {
SpacerH(12.dp)
BestOpportunitiesEmpty() // TODO in [REDACTED_TASK_KEY]
BestOpportunitiesEmpty()
}
}
is EarnBestOpportunitiesUM.EmptyFiltered -> {
item(key = "best_opportunities_empty_filtered") {
SpacerH(12.dp)
BestOpportunitiesEmptyFiltered(onClearFilterClick = state.onClearFilterClick) // TODO in [REDACTED_TASK_KEY]
BestOpportunitiesEmptyFiltered(onClearFilterClick = state.onClearFilterClick)
}
}
is EarnBestOpportunitiesUM.Content -> {
@ -305,7 +305,7 @@ private fun LazyListScope.bestOpportunitiesItemsV2(state: EarnBestOpportunitiesU
color = TangemTheme.colors2.surface.level3,
shape = RoundedCornerShape(TangemTheme.dimens2.x5),
)
.padding(vertical = 142.dp, horizontal = 114.dp),
.padding(vertical = 142.dp, horizontal = 16.dp),
contentAlignment = Alignment.Center,
) {
UnableToLoadData(onRetryClick = state.onRetryClicked)

View file

@ -85,7 +85,7 @@ private fun BestOpportunitiesEmptyV2(modifier: Modifier = Modifier) {
Text(
modifier = Modifier.padding(horizontal = TangemTheme.dimens2.x8),
text = stringResourceSafe(R.string.earn_empty),
style = TangemTheme.typography2.bodyRegular14,
style = TangemTheme.typography2.subheadlineMedium14,
color = TangemTheme.colors2.text.neutral.secondary,
textAlign = TextAlign.Center,
)

View file

@ -44,7 +44,7 @@ private fun BestOpportunitiesEmptyFilteredV2(onClearFilterClick: () -> Unit, mod
) {
Text(
text = stringResourceSafe(R.string.earn_no_results),
style = TangemTheme.typography2.bodyRegular14,
style = TangemTheme.typography2.subheadlineMedium14,
color = TangemTheme.colors2.text.neutral.secondary,
)
SpacerH(TangemTheme.dimens2.x2)

View file

@ -175,13 +175,15 @@ private fun NetworksTypesBlock(
overflow = TextOverflow.Ellipsis,
)
TangemCheckbox(
modifier = Modifier
.padding(start = 8.dp)
.layoutId(layoutId = TangemRowLayoutId.TAIL),
isChecked = item.isSelected,
onCheckedChange = { onOptionClick(item) },
)
if (item.isSelected) {
TangemCheckbox(
modifier = Modifier
.padding(start = 8.dp)
.layoutId(layoutId = TangemRowLayoutId.TAIL),
isChecked = true,
onCheckedChange = { onOptionClick(item) },
)
}
}
}
}
@ -233,13 +235,15 @@ private fun SpecificNetworksBlock(
overflow = TextOverflow.Ellipsis,
)
TangemCheckbox(
modifier = Modifier
.padding(start = 8.dp)
.layoutId(layoutId = TangemRowLayoutId.TAIL),
isChecked = item.isSelected,
onCheckedChange = { onOptionClick(item) },
)
if (item.isSelected) {
TangemCheckbox(
modifier = Modifier
.padding(start = 8.dp)
.layoutId(layoutId = TangemRowLayoutId.TAIL),
isChecked = true,
onCheckedChange = { onOptionClick(item) },
)
}
}
}
}

View file

@ -115,7 +115,8 @@ private fun EarnListItemV2(item: EarnListItemUM, modifier: Modifier = Modifier)
tangemIconUM = TangemIconUM.Currency(item.currencyIconState),
modifier = Modifier
.layoutId(layoutId = TangemRowLayoutId.HEAD)
.padding(end = TangemTheme.dimens2.x2),
.padding(end = TangemTheme.dimens2.x2)
.size(TangemTheme.dimens2.x10),
)
TokenTitle(

View file

@ -15,10 +15,10 @@ import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.SpacerW
import com.tangem.core.ui.ds.opportunities.OpportunitiesBG
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.ds.opportunities.OpportunitiesBG
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.*
@ -53,10 +53,10 @@ private fun MostlyUsedCardV2(item: EarnListItemUM, onClick: () -> Unit, modifier
) {
Column(modifier = Modifier.padding(12.dp)) {
CurrencyIcon(
modifier = Modifier.size(32.dp),
state = item.currencyIconState,
shouldDisplayNetwork = true,
networkBadgeSize = 12.dp,
networkBadgeSize = TangemTheme.dimens2.x4,
iconSize = TangemTheme.dimens2.x10,
networkBadgeBackground = TangemTheme.colors.background.action,
)

View file

@ -118,12 +118,7 @@ private fun Content(
contentPadding = PaddingValues(bottom = bottomBarHeight, top = contentPadding.calculateTopPadding()),
) {
item("header") {
Header(
state = state,
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
)
Header(state = state)
}
item {
if (isRedesignEnabled) SpacerH(TangemTheme.dimens2.x3) else SpacerH16()
@ -202,15 +197,33 @@ internal fun MarketsTokenDetailsTopBar(
}
@Composable
private fun Header(state: MarketsTokenDetailsUM, modifier: Modifier = Modifier) {
private fun Header(state: MarketsTokenDetailsUM) {
if (LocalRedesignEnabled.current) {
HeaderV2(
modifier = Modifier
.padding(TangemTheme.dimens2.x4)
.fillMaxWidth(),
state = state,
)
} else {
HeaderV1(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
state = state,
)
}
}
@Composable
private fun HeaderV1(state: MarketsTokenDetailsUM, modifier: Modifier = Modifier) {
Row(
modifier = modifier,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Column(modifier = Modifier.weight(1f)) {
TokenPriceText(
TokenPriceTextV1(
price = state.priceText,
priceAnnotated = state.priceAnnotated,
triggerPriceChange = state.triggerPriceChange,
)
Row(horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4)) {
@ -240,23 +253,55 @@ private fun Header(state: MarketsTokenDetailsUM, modifier: Modifier = Modifier)
}
@Composable
private fun TokenPriceText(
price: String,
triggerPriceChange: StateEvent<PriceChangeType>,
priceAnnotated: TextReference,
modifier: Modifier = Modifier,
) {
if (LocalRedesignEnabled.current) {
TokenPriceTextV2(
priceAnnotated = priceAnnotated,
triggerPriceChange = triggerPriceChange,
modifier = modifier,
)
} else {
TokenPriceTextV1(
price = price,
triggerPriceChange = triggerPriceChange,
modifier = modifier,
private fun HeaderV2(state: MarketsTokenDetailsUM, modifier: Modifier = Modifier) {
Row(
modifier = modifier,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Column(modifier = Modifier.weight(1f)) {
Row(
verticalAlignment = Alignment.Bottom,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1),
) {
Text(
text = state.tokenName,
style = TangemTheme.typography2.bodySemibold16,
color = TangemTheme.colors2.text.neutral.primary,
)
Text(
text = state.symbol,
style = TangemTheme.typography2.captionMedium12,
color = TangemTheme.colors2.text.neutral.tertiary,
)
}
SpacerH(TangemTheme.dimens2.x1)
TokenPriceTextV2(
priceAnnotated = state.priceAnnotated,
triggerPriceChange = state.triggerPriceChange,
)
SpacerH(TangemTheme.dimens2.x4)
Row(horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1)) {
Text(
text = state.dateTimeText.resolveReference(),
style = TangemTheme.typography2.captionMedium12,
color = TangemTheme.colors2.text.neutral.primary,
)
if (state.priceChangePercentText != null) {
PriceChangeInPercent(
valueInPercent = state.priceChangePercentText,
type = state.priceChangeType,
textStyle = TangemTheme.typography2.captionMedium12,
)
}
}
}
SpacerW4()
CoinIcon(
modifier = Modifier.requiredSize(70.dp),
url = state.iconUrl,
alpha = 1f,
colorFilter = null,
fallbackResId = R.drawable.ic_custom_token_44,
)
}
}
@ -323,9 +368,9 @@ private fun TokenPriceTextV2(
text = priceAnnotated.resolveAnnotatedReference(),
modifier = modifier,
color = color.value,
autoSize = TextAutoSize.StepBased(maxFontSize = TangemTheme.typography2.headingBold34.fontSize),
autoSize = TextAutoSize.StepBased(maxFontSize = TangemTheme.typography2.titleRegular44.fontSize),
maxLines = 1,
style = TangemTheme.typography2.headingBold34,
style = TangemTheme.typography2.titleRegular44,
)
}

View file

@ -11,6 +11,7 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@ -49,6 +50,7 @@ import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.features.feed.impl.R
import com.tangem.features.feed.ui.market.detailed.state.ExchangeItemUM
import com.tangem.features.feed.ui.market.detailed.state.ExchangesBottomSheetContent
@ -273,7 +275,7 @@ private fun ErrorV2(content: ExchangesBottomSheetContent.Error, modifier: Modifi
text = stringResourceSafe(id = content.message),
color = TangemTheme.colors2.text.neutral.tertiary,
textAlign = TextAlign.Center,
style = TangemTheme.typography2.bodyRegular14,
style = TangemTheme.typography2.subheadlineMedium14,
)
SpacerH(8.dp)
@ -369,6 +371,7 @@ private fun ExchangeItemRowPlaceholder(modifier: Modifier = Modifier) {
.layoutId(TangemRowLayoutId.HEAD),
)
RectangleShimmer(
radius = TangemTheme.dimens2.x25,
modifier = Modifier
.width(108.dp)
.height(20.dp)
@ -376,6 +379,7 @@ private fun ExchangeItemRowPlaceholder(modifier: Modifier = Modifier) {
.layoutId(TangemRowLayoutId.START_TOP),
)
RectangleShimmer(
radius = TangemTheme.dimens2.x25,
modifier = Modifier
.width(52.dp)
.height(16.dp)
@ -383,6 +387,7 @@ private fun ExchangeItemRowPlaceholder(modifier: Modifier = Modifier) {
.layoutId(TangemRowLayoutId.START_BOTTOM),
)
RectangleShimmer(
radius = TangemTheme.dimens2.x25,
modifier = Modifier
.width(106.dp)
.height(20.dp)
@ -390,6 +395,7 @@ private fun ExchangeItemRowPlaceholder(modifier: Modifier = Modifier) {
.layoutId(TangemRowLayoutId.END_TOP),
)
RectangleShimmer(
radius = TangemTheme.dimens2.x25,
modifier = Modifier
.width(52.dp)
.height(16.dp)
@ -428,6 +434,25 @@ private fun Preview_ExchangesBottomSheet(
}
}
@Preview
@Preview(name = "Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_ExchangesBottomSheetV2(
@PreviewParameter(ExchangesBottomSheetContentProvider::class) content: ExchangesBottomSheetContent,
) {
TangemThemePreviewRedesign {
CompositionLocalProvider(LocalRedesignEnabled provides true) {
ExchangesBottomSheet(
config = TangemBottomSheetConfig(
onDismissRequest = {},
content = content,
isShown = true,
),
)
}
}
}
private class ExchangesBottomSheetContentProvider : CollectionPreviewParameterProvider<ExchangesBottomSheetContent>(
listOf(
ExchangesBottomSheetContent.Loading(exchangesCount = 13),

View file

@ -20,6 +20,7 @@ internal object MarketsTokenDetailsPreview {
val loadingState = MarketsTokenDetailsUM(
tokenName = "Token Name",
symbol = "USDT",
priceText = "$0.00000000324",
dateTimeText = stringReference("Today"),
priceChangePercentText = "52.00%",
@ -55,6 +56,7 @@ internal object MarketsTokenDetailsPreview {
val contentState = MarketsTokenDetailsUM(
tokenName = "Token Name",
symbol = "USDT",
priceText = "$0.00000000324",
dateTimeText = stringReference("Today"),
priceChangePercentText = "52.00%",

View file

@ -14,6 +14,7 @@ import java.math.BigDecimal
internal data class MarketsTokenDetailsUM(
val tokenName: String,
val symbol: String,
val priceText: String,
val priceAnnotated: TextReference,
val iconUrl: String?,

View file

@ -1,11 +1,25 @@
package com.tangem.features.feed.ui.market.list.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.HorizontalDivider
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.graphics.vector.rememberVectorPainter
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEachIndexed
import com.tangem.core.ui.R
import com.tangem.core.ui.ds.contextmenu.TangemContextMenu
import com.tangem.core.ui.ds.contextmenu.TangemContextMenuCheckboxItem
import com.tangem.core.ui.extensions.clickableSingle
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.feed.model.market.list.state.SortByMenuUM
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
@ -22,15 +36,61 @@ internal fun SortByMenu(
offset = DpOffset.Zero,
modifier = modifier,
) {
SortByTypeUM.entries.fastForEach { sortType ->
TangemContextMenuCheckboxItem(
title = sortType.text,
isChecked = sortMenuUM.selectedOption == sortType,
onClick = {
sortMenuUM.onOptionClicked(sortType)
onDropdownDismiss()
},
)
SortByTypeUM.entries.fastForEachIndexed { index, sortType ->
Column {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
.fillMaxWidth()
.width(238.dp)
.clickableSingle(
onClick = {
sortMenuUM.onOptionClicked(sortType)
onDropdownDismiss()
},
)
.padding(
vertical = TangemTheme.dimens2.x5,
horizontal = TangemTheme.dimens2.x4,
),
) {
Text(
text = sortType.text.resolveReference(),
style = TangemTheme.typography2.headingSemibold17,
color = TangemTheme.colors2.text.neutral.primary,
maxLines = 1,
)
if (sortMenuUM.selectedOption == sortType) {
Box(
modifier = Modifier
.padding(TangemTheme.dimens2.x0_5)
.size(TangemTheme.dimens2.x5)
.background(
color = TangemTheme.colors2.graphic.neutral.primary,
shape = CircleShape,
),
) {
Icon(
painter = rememberVectorPainter(
ImageVector.vectorResource(R.drawable.ic_check_default_24),
),
contentDescription = null,
tint = TangemTheme.colors2.graphic.neutral.primaryInverted,
modifier = Modifier
.align(Alignment.Center)
.padding(TangemTheme.dimens2.x0_5)
.size(TangemTheme.dimens2.x4),
)
}
}
}
if (index < SortByTypeUM.entries.size - 1) {
HorizontalDivider(
thickness = 0.5.dp,
color = TangemTheme.colors2.border.neutral.quaternary,
)
}
}
}
}
}