Updated on 2026-08-14
This commit is contained in:
parent
602e20a1d2
commit
c83a46cc1f
17 changed files with 1116 additions and 143 deletions
|
|
@ -1,6 +1,8 @@
|
|||
package com.tangem.features.feed.model.search
|
||||
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.common.ui.charts.state.MarketChartData
|
||||
import com.tangem.common.ui.charts.state.converter.PriceAndTimePointValuesConverter
|
||||
import com.tangem.common.ui.charts.state.sorted
|
||||
|
|
@ -19,6 +21,7 @@ import com.tangem.domain.markets.toSerializableParam
|
|||
import com.tangem.domain.search.usecase.ClearSearchHistoryUseCase
|
||||
import com.tangem.domain.search.usecase.GetSearchResultsUseCase
|
||||
import com.tangem.domain.search.usecase.SaveRecentSearchTokenUseCase
|
||||
import com.tangem.domain.search.model.UserAssetSearchEntry
|
||||
import com.tangem.domain.search.usecase.SaveSearchQueryUseCase
|
||||
import com.tangem.features.feed.components.search.DefaultSearchComponent
|
||||
import com.tangem.features.feed.model.market.list.state.MarketsListUM
|
||||
|
|
@ -58,6 +61,7 @@ internal class SearchModel @Inject constructor(
|
|||
private val saveRecentSearchTokenUseCase: SaveRecentSearchTokenUseCase,
|
||||
private val clearSearchHistoryUseCase: ClearSearchHistoryUseCase,
|
||||
private val getTokenPriceChartUseCase: GetTokenPriceChartUseCase,
|
||||
private val appRouter: AppRouter,
|
||||
private val stateController: SearchStateController,
|
||||
) : Model() {
|
||||
|
||||
|
|
@ -195,6 +199,15 @@ internal class SearchModel @Inject constructor(
|
|||
stateController.update(UpdateSearchBarQueryTransformer(""))
|
||||
}
|
||||
|
||||
private fun onSingleUserAssetClick(entry: UserAssetSearchEntry) {
|
||||
appRouter.push(
|
||||
AppRoute.CurrencyDetails(
|
||||
userWalletId = entry.userWalletId,
|
||||
currency = entry.currencyStatus.currency,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun subscribeToQueryChanges() {
|
||||
stateController.uiState
|
||||
.map { it.searchBar.query.trim() }
|
||||
|
|
@ -231,6 +244,7 @@ internal class SearchModel @Inject constructor(
|
|||
val converter = UserAssetSearchItemConverter(
|
||||
appCurrency = appCurrency,
|
||||
isBalanceHidden = balanceHidden,
|
||||
onSingleClick = ::onSingleUserAssetClick,
|
||||
)
|
||||
searchResult.userAssets
|
||||
.map(converter::convert)
|
||||
|
|
|
|||
|
|
@ -1,23 +1,34 @@
|
|||
package com.tangem.features.feed.model.search.converter
|
||||
|
||||
import com.tangem.common.ui.account.toUM
|
||||
import com.tangem.common.ui.markets.toMarketsListItemPriceAnnotated
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
|
||||
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.networkIconResId
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.fiat
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.format.bigdecimal.percent
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.search.model.UserAssetSearchEntry
|
||||
import com.tangem.domain.search.model.UserAssetSearchItem
|
||||
import com.tangem.features.feed.ui.search.state.BalanceDisplayState
|
||||
import com.tangem.features.feed.ui.search.state.UserAssetItemUM
|
||||
import com.tangem.utils.StringsSigns
|
||||
import com.tangem.utils.converter.Converter
|
||||
import com.tangem.utils.extensions.orZero
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class UserAssetSearchItemConverter(
|
||||
private val appCurrency: AppCurrency,
|
||||
private val isBalanceHidden: Boolean,
|
||||
private val onSingleClick: (UserAssetSearchEntry) -> Unit,
|
||||
) : Converter<UserAssetSearchItem, UserAssetItemUM> {
|
||||
|
||||
override fun convert(value: UserAssetSearchItem): UserAssetItemUM {
|
||||
|
|
@ -39,17 +50,56 @@ internal class UserAssetSearchItemConverter(
|
|||
tokenName = currency.name,
|
||||
tokenSymbol = currency.symbol,
|
||||
fiatRate = value.fiatRate?.format { fiat(appCurrency.code, appCurrency.symbol) },
|
||||
cryptoBalance = formatCryptoAmount(value.amount, currency.symbol, currency.decimals),
|
||||
fiatBalance = formatFiatAmount(value.fiatAmount),
|
||||
priceChangeState = when (value) {
|
||||
is CryptoCurrencyStatus.Loading,
|
||||
is CryptoCurrencyStatus.Unreachable,
|
||||
is CryptoCurrencyStatus.MissedDerivation,
|
||||
is CryptoCurrencyStatus.NoAmount,
|
||||
-> PriceChangeState.Unknown
|
||||
else -> PriceChangeState.Content(
|
||||
type = PriceChangeType.fromBigDecimal(value.priceChange.orZero()),
|
||||
valueInPercent = value.priceChange.format { percent() },
|
||||
)
|
||||
},
|
||||
balanceState = convertSingleBalanceState(value, currency.symbol, currency.decimals),
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
onClick = {},
|
||||
onClick = { onSingleClick(entry) },
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertGrouped(item: UserAssetSearchItem.Grouped): UserAssetItemUM.Grouped {
|
||||
val totalFiat = item.entries.sumOf { it.currencyStatus.value.fiatAmount ?: BigDecimal.ZERO }
|
||||
val totalCrypto = item.entries.sumOf { it.currencyStatus.value.amount ?: BigDecimal.ZERO }
|
||||
private fun convertSingleBalanceState(
|
||||
value: CryptoCurrencyStatus.Value,
|
||||
symbol: String,
|
||||
decimals: Int,
|
||||
): BalanceDisplayState {
|
||||
return when {
|
||||
value is CryptoCurrencyStatus.Loading && value.amount != null ->
|
||||
BalanceDisplayState.Flickering(
|
||||
cryptoBalance = stringReference(formatCryptoAmount(value.amount, symbol, decimals)),
|
||||
fiatBalance = value.fiatAmount?.toMarketsListItemPriceAnnotated(
|
||||
appCurrencyCode = appCurrency.code, appCurrencySymbol = appCurrency.symbol,
|
||||
) ?: stringReference(StringsSigns.DASH_SIGN),
|
||||
)
|
||||
value is CryptoCurrencyStatus.Loading -> BalanceDisplayState.Loading
|
||||
value is CryptoCurrencyStatus.Unreachable -> BalanceDisplayState.Unreachable
|
||||
value.isError && value.amount != null ->
|
||||
BalanceDisplayState.Stale(
|
||||
cryptoBalance = stringReference(formatCryptoAmount(value.amount, symbol, decimals)),
|
||||
fiatBalance = value.fiatAmount?.toMarketsListItemPriceAnnotated(
|
||||
appCurrencyCode = appCurrency.code, appCurrencySymbol = appCurrency.symbol,
|
||||
) ?: stringReference(StringsSigns.DASH_SIGN),
|
||||
)
|
||||
value.isError -> BalanceDisplayState.Unreachable
|
||||
else -> BalanceDisplayState.Loaded(
|
||||
cryptoBalance = stringReference(formatCryptoAmount(value.amount, symbol, decimals)),
|
||||
fiatBalance = value.fiatAmount?.toMarketsListItemPriceAnnotated(
|
||||
appCurrencyCode = appCurrency.code, appCurrencySymbol = appCurrency.symbol,
|
||||
) ?: stringReference(StringsSigns.DASH_SIGN),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun convertGrouped(item: UserAssetSearchItem.Grouped): UserAssetItemUM.Grouped {
|
||||
val firstCurrency = item.entries.first().currencyStatus.currency
|
||||
val children = item.entries.map { entry ->
|
||||
UserAssetItemUM.GroupedChild(
|
||||
|
|
@ -57,36 +107,78 @@ internal class UserAssetSearchItemConverter(
|
|||
accountName = entry.accountName.toUM(),
|
||||
accountIcon = entry.accountIcon.value,
|
||||
accountColor = entry.accountIcon.color,
|
||||
cryptoBalance = formatCryptoAmount(
|
||||
entry.currencyStatus.value.amount,
|
||||
entry.currencyStatus.currency.symbol,
|
||||
entry.currencyStatus.currency.decimals,
|
||||
),
|
||||
fiatBalance = formatFiatAmount(entry.currencyStatus.value.fiatAmount),
|
||||
currencyStatus = entry.currencyStatus,
|
||||
)
|
||||
}.toImmutableList()
|
||||
|
||||
val entryCurrencyStatus = item.entries.first().currencyStatus
|
||||
|
||||
return UserAssetItemUM.Grouped(
|
||||
id = "grouped_${item.tokenName}_${item.tokenSymbol}",
|
||||
icon = TangemIconUM.Currency(
|
||||
currencyIconState = CryptoCurrencyToIconStateConverter().convert(item.entries.first().currencyStatus),
|
||||
currencyIconState = CurrencyIconState.CoinIcon(
|
||||
url = entryCurrencyStatus.currency.iconUrl,
|
||||
fallbackResId = entryCurrencyStatus.currency.networkIconResId,
|
||||
isGrayscale = entryCurrencyStatus.currency.network.isTestnet || entryCurrencyStatus.value.isError,
|
||||
shouldShowCustomBadge = entryCurrencyStatus.currency.isCustom,
|
||||
),
|
||||
),
|
||||
tokenName = item.tokenName,
|
||||
tokenSymbol = item.tokenSymbol,
|
||||
tokensCount = item.entries.size,
|
||||
totalCryptoBalance = formatCryptoAmount(totalCrypto, firstCurrency.symbol, firstCurrency.decimals),
|
||||
totalFiatBalance = formatFiatAmount(totalFiat),
|
||||
balanceState = convertGroupedBalanceState(item.entries, firstCurrency.symbol, firstCurrency.decimals),
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
children = children,
|
||||
onClick = {},
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertGroupedBalanceState(
|
||||
entries: List<UserAssetSearchEntry>,
|
||||
symbol: String,
|
||||
decimals: Int,
|
||||
): BalanceDisplayState {
|
||||
val hasAnyLoading = entries.any { it.currencyStatus.value is CryptoCurrencyStatus.Loading }
|
||||
val hasAnyError = entries.any { it.currencyStatus.value.isError }
|
||||
val hasAnyAmount = entries.any { it.currencyStatus.value.amount != null }
|
||||
|
||||
val balance = when {
|
||||
hasAnyLoading && !hasAnyAmount -> BalanceDisplayState.Loading
|
||||
hasAnyLoading && hasAnyAmount -> computeGroupBalanceFlickering(entries, symbol, decimals)
|
||||
hasAnyError && entries.size == 1 && !hasAnyAmount -> BalanceDisplayState.Unreachable
|
||||
hasAnyError && entries.size > 1 -> computeGroupBalance(entries, symbol, decimals)
|
||||
else -> computeGroupBalance(entries, symbol, decimals)
|
||||
}
|
||||
return balance
|
||||
}
|
||||
|
||||
private fun computeGroupBalance(
|
||||
entries: List<UserAssetSearchEntry>,
|
||||
symbol: String,
|
||||
decimals: Int,
|
||||
): BalanceDisplayState.Loaded {
|
||||
val totalFiat = entries.sumOf { it.currencyStatus.value.fiatAmount ?: BigDecimal.ZERO }
|
||||
val totalCrypto = entries.sumOf { it.currencyStatus.value.amount ?: BigDecimal.ZERO }
|
||||
return BalanceDisplayState.Loaded(
|
||||
cryptoBalance = stringReference(formatCryptoAmount(totalCrypto, symbol, decimals)),
|
||||
fiatBalance = totalFiat.toMarketsListItemPriceAnnotated(appCurrency.code, appCurrency.symbol),
|
||||
)
|
||||
}
|
||||
|
||||
private fun computeGroupBalanceFlickering(
|
||||
entries: List<UserAssetSearchEntry>,
|
||||
symbol: String,
|
||||
decimals: Int,
|
||||
): BalanceDisplayState.Flickering {
|
||||
val totalFiat = entries.sumOf { it.currencyStatus.value.fiatAmount ?: BigDecimal.ZERO }
|
||||
val totalCrypto = entries.sumOf { it.currencyStatus.value.amount ?: BigDecimal.ZERO }
|
||||
return BalanceDisplayState.Flickering(
|
||||
cryptoBalance = stringReference(formatCryptoAmount(totalCrypto, symbol, decimals)),
|
||||
fiatBalance = totalFiat.toMarketsListItemPriceAnnotated(appCurrency.code, appCurrency.symbol),
|
||||
)
|
||||
}
|
||||
|
||||
private fun formatCryptoAmount(amount: BigDecimal?, symbol: String, decimals: Int): String {
|
||||
return amount?.format { crypto(symbol, decimals) } ?: StringsSigns.DASH_SIGN
|
||||
}
|
||||
|
||||
private fun formatFiatAmount(fiatAmount: BigDecimal?): String {
|
||||
return fiatAmount?.format { fiat(appCurrency.code, appCurrency.symbol) } ?: StringsSigns.DASH_SIGN
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
package com.tangem.features.feed.ui.components
|
||||
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.features.feed.ui.search
|
||||
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
|
|
@ -8,9 +9,8 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
|
|
@ -27,16 +27,20 @@ import com.tangem.core.ui.components.SpacerH
|
|||
import com.tangem.core.ui.components.SpacerW
|
||||
import com.tangem.core.ui.components.list.InfiniteListHandler
|
||||
import com.tangem.core.ui.ds.button.*
|
||||
import com.tangem.core.ui.ds.image.TangemIcon
|
||||
import com.tangem.core.ui.ds.image.TangemIconUM
|
||||
import com.tangem.core.ui.extensions.clickableSingle
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.LocalMainBottomSheetColor
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.feed.ui.search.components.GroupedUserAssetItem
|
||||
import com.tangem.features.feed.ui.search.components.SingleUserAssetItem
|
||||
import com.tangem.features.feed.ui.search.state.*
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
private const val PLACEHOLDER_COUNT = 10
|
||||
private const val LOAD_MORE_THRESHOLD = 5
|
||||
private const val USER_ASSETS_LIMIT = 3
|
||||
|
||||
@Composable
|
||||
internal fun SearchContent(
|
||||
|
|
@ -58,6 +62,14 @@ internal fun SearchContent(
|
|||
lazyListState.scrollToItem(0)
|
||||
}
|
||||
|
||||
var isUserAssetsExpanded by rememberSaveable { mutableStateOf(false) }
|
||||
val shouldShowUserAssetsPortfolio = content is SearchContentUM.Results && content.userAssets.isNotEmpty()
|
||||
LaunchedEffect(shouldShowUserAssetsPortfolio) {
|
||||
if (!shouldShowUserAssetsPortfolio) {
|
||||
isUserAssetsExpanded = false
|
||||
}
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
state = lazyListState,
|
||||
modifier = modifier
|
||||
|
|
@ -80,6 +92,8 @@ internal fun SearchContent(
|
|||
)
|
||||
is SearchContentUM.Results -> searchResultsItems(
|
||||
results = content,
|
||||
isUserAssetsExpanded = isUserAssetsExpanded,
|
||||
onUserAssetsExpandedChange = { isUserAssetsExpanded = it },
|
||||
onResultMarketTokenClick = searchCallbacks.onResultMarketTokenClick,
|
||||
)
|
||||
}
|
||||
|
|
@ -147,18 +161,19 @@ private fun LazyListScope.searchHistoryItems(
|
|||
|
||||
private fun LazyListScope.searchResultsItems(
|
||||
results: SearchContentUM.Results,
|
||||
isUserAssetsExpanded: Boolean,
|
||||
onUserAssetsExpandedChange: (Boolean) -> Unit,
|
||||
onResultMarketTokenClick: (MarketsListItemUM) -> Unit,
|
||||
) {
|
||||
if (results.userAssets.isNotEmpty()) {
|
||||
item(key = "header_portfolio") {
|
||||
SectionHeader(title = stringResourceSafe(R.string.markets_search_portfolio_header))
|
||||
}
|
||||
items(
|
||||
items = results.userAssets,
|
||||
key = { it.id },
|
||||
) { asset ->
|
||||
UserAssetItem(asset)
|
||||
}
|
||||
userAssetsPortfolioItems(
|
||||
assets = results.userAssets,
|
||||
expanded = isUserAssetsExpanded,
|
||||
onExpandedChange = onUserAssetsExpandedChange,
|
||||
)
|
||||
}
|
||||
|
||||
when (val market = results.marketTokens) {
|
||||
|
|
@ -175,6 +190,42 @@ private fun LazyListScope.searchResultsItems(
|
|||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.userAssetsPortfolioItems(
|
||||
assets: ImmutableList<UserAssetItemUM>,
|
||||
expanded: Boolean,
|
||||
onExpandedChange: (Boolean) -> Unit,
|
||||
) {
|
||||
val shouldShowToggle = assets.size > USER_ASSETS_LIMIT
|
||||
val visibleCount = if (shouldShowToggle && !expanded) USER_ASSETS_LIMIT else assets.size
|
||||
|
||||
items(
|
||||
count = visibleCount,
|
||||
key = { index -> "user_asset_${assets[index].id}" },
|
||||
) { index ->
|
||||
Column(
|
||||
modifier = Modifier.animateItem(
|
||||
fadeInSpec = tween(durationMillis = 300),
|
||||
fadeOutSpec = tween(durationMillis = 250),
|
||||
),
|
||||
) {
|
||||
UserAssetItem(assets[index])
|
||||
SpacerH(TangemTheme.dimens2.x2)
|
||||
}
|
||||
}
|
||||
if (shouldShowToggle) {
|
||||
item(key = "user_assets_show_toggle") {
|
||||
ShowAllUserAssetsButton(
|
||||
modifier = Modifier.animateItem(
|
||||
fadeInSpec = tween(durationMillis = 300),
|
||||
fadeOutSpec = tween(durationMillis = 250),
|
||||
),
|
||||
isExpanded = expanded,
|
||||
onClick = { onExpandedChange(!expanded) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.marketSearchResultItems(
|
||||
market: MarketSearchResultUM.Content,
|
||||
hasUserAssetsSection: Boolean,
|
||||
|
|
@ -234,7 +285,7 @@ private fun LazyListScope.marketSearchResultNotFoundItem() {
|
|||
) {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.common_no_results),
|
||||
style = TangemTheme.typography2.bodyRegular14,
|
||||
style = TangemTheme.typography2.subheadlineMedium14,
|
||||
color = TangemTheme.colors2.text.neutral.tertiary,
|
||||
)
|
||||
}
|
||||
|
|
@ -278,106 +329,34 @@ private fun TextHintItem(hint: TextHintItemUM, onHintClick: () -> Unit) {
|
|||
@Composable
|
||||
private fun UserAssetItem(asset: UserAssetItemUM) {
|
||||
when (asset) {
|
||||
is UserAssetItemUM.Single -> SingleUserAssetItem(asset)
|
||||
is UserAssetItemUM.Grouped -> GroupedUserAssetItem(asset)
|
||||
is UserAssetItemUM.Single -> SingleUserAssetItem(item = asset)
|
||||
is UserAssetItemUM.Grouped -> GroupedUserAssetItem(item = asset)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SingleUserAssetItem(asset: UserAssetItemUM.Single) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = asset.onClick)
|
||||
.padding(horizontal = 12.dp, vertical = 14.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
private fun ShowAllUserAssetsButton(isExpanded: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
TangemIcon(
|
||||
modifier = Modifier.size(40.dp),
|
||||
tangemIconUM = asset.icon,
|
||||
TangemButton(
|
||||
buttonUM = TangemButtonUM(
|
||||
text = if (isExpanded) {
|
||||
resourceReference(R.string.feed_search_show_less_user_assets)
|
||||
} else {
|
||||
resourceReference(R.string.feed_search_show_all_user_assets)
|
||||
},
|
||||
tangemIconUM = TangemIconUM.Icon(
|
||||
iconRes = if (isExpanded) R.drawable.ic_chewron_up_20 else R.drawable.ic_chewron_down_20,
|
||||
),
|
||||
iconPosition = TangemButtonIconPosition.End,
|
||||
onClick = onClick,
|
||||
type = TangemButtonType.Secondary,
|
||||
size = TangemButtonSize.X7,
|
||||
shape = TangemButtonShape.Rounded,
|
||||
),
|
||||
)
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = asset.tokenName,
|
||||
style = TangemTheme.typography2.bodySemibold16,
|
||||
color = TangemTheme.colors2.text.neutral.primary,
|
||||
maxLines = 1,
|
||||
)
|
||||
Text(
|
||||
text = asset.tokenSymbol,
|
||||
style = TangemTheme.typography2.captionRegular13,
|
||||
color = TangemTheme.colors2.text.neutral.tertiary,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
if (!asset.isBalanceHidden) {
|
||||
Column(horizontalAlignment = Alignment.End) {
|
||||
Text(
|
||||
text = asset.fiatBalance,
|
||||
style = TangemTheme.typography2.bodySemibold16,
|
||||
color = TangemTheme.colors2.text.neutral.primary,
|
||||
maxLines = 1,
|
||||
)
|
||||
Text(
|
||||
text = asset.cryptoBalance,
|
||||
style = TangemTheme.typography2.captionRegular13,
|
||||
color = TangemTheme.colors2.text.neutral.tertiary,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO [REDACTED_JIRA] update ui item to Portfolio block item
|
||||
@Composable
|
||||
private fun GroupedUserAssetItem(asset: UserAssetItemUM.Grouped) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = asset.onClick)
|
||||
.padding(horizontal = 12.dp, vertical = 14.dp),
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
TangemIcon(
|
||||
modifier = Modifier.size(40.dp),
|
||||
tangemIconUM = asset.icon,
|
||||
)
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = asset.tokenName,
|
||||
style = TangemTheme.typography2.bodySemibold16,
|
||||
color = TangemTheme.colors2.text.neutral.primary,
|
||||
maxLines = 1,
|
||||
)
|
||||
Text(
|
||||
text = "${asset.tokenSymbol} · ${asset.tokensCount}",
|
||||
style = TangemTheme.typography2.captionRegular13,
|
||||
color = TangemTheme.colors2.text.neutral.tertiary,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
if (!asset.isBalanceHidden) {
|
||||
Column(horizontalAlignment = Alignment.End) {
|
||||
Text(
|
||||
text = asset.totalFiatBalance,
|
||||
style = TangemTheme.typography2.bodySemibold16,
|
||||
color = TangemTheme.colors2.text.neutral.primary,
|
||||
maxLines = 1,
|
||||
)
|
||||
Text(
|
||||
text = asset.totalCryptoBalance,
|
||||
style = TangemTheme.typography2.captionRegular13,
|
||||
color = TangemTheme.colors2.text.neutral.tertiary,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -397,7 +376,7 @@ private fun ShowTokensUnder100kItem(onShowTokensClick: () -> Unit, modifier: Mod
|
|||
) {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.markets_search_see_tokens_under_100k),
|
||||
style = TangemTheme.typography2.bodyRegular14,
|
||||
style = TangemTheme.typography2.subheadlineMedium14,
|
||||
color = TangemTheme.colors2.text.neutral.secondary,
|
||||
)
|
||||
TangemButton(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,175 @@
|
|||
package com.tangem.features.feed.ui.search.components
|
||||
|
||||
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.features.feed.ui.search.state.BalanceDisplayState
|
||||
import com.tangem.utils.StringsSigns
|
||||
|
||||
@Composable
|
||||
internal 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,84 @@
|
|||
package com.tangem.features.feed.ui.search.components
|
||||
|
||||
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.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
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.features.feed.ui.components.LayeringIcons
|
||||
import com.tangem.features.feed.ui.search.state.UserAssetItemUM
|
||||
|
||||
@Composable
|
||||
internal 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,117 @@
|
|||
package com.tangem.features.feed.ui.search.components
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
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
|
||||
import com.tangem.features.feed.ui.search.state.BalanceDisplayState
|
||||
import com.tangem.features.feed.ui.search.state.UserAssetItemUM
|
||||
|
||||
@Composable
|
||||
fun SingleUserAssetItem(item: UserAssetItemUM.Single, 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 = {
|
||||
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,
|
||||
)
|
||||
|
||||
PriceBlock(
|
||||
modifier = Modifier.layoutId(TangemRowLayoutId.START_BOTTOM),
|
||||
priceChangeState = item.priceChangeState,
|
||||
fiatRate = item.fiatRate,
|
||||
balanceState = item.balanceState,
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ import androidx.compose.ui.unit.dp
|
|||
import com.tangem.common.ui.charts.state.MarketChartRawData
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
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
|
||||
|
|
@ -217,8 +218,14 @@ internal object SearchContentPreviewFixtures {
|
|||
tokenName = name,
|
||||
tokenSymbol = symbol,
|
||||
fiatRate = "$98,765.43",
|
||||
cryptoBalance = "1.234 $symbol",
|
||||
fiatBalance = "$121,876.50",
|
||||
priceChangeState = PriceChangeState.Content(
|
||||
type = PriceChangeType.UP,
|
||||
valueInPercent = "+2.34%",
|
||||
),
|
||||
balanceState = BalanceDisplayState.Loaded(
|
||||
cryptoBalance = stringReference("1.234 $symbol"),
|
||||
fiatBalance = stringReference("$121,876.50"),
|
||||
),
|
||||
isBalanceHidden = false,
|
||||
onClick = {},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,238 @@
|
|||
package com.tangem.features.feed.ui.search.preview
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.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.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||
import com.tangem.features.feed.ui.search.components.GroupedUserAssetItem
|
||||
import com.tangem.features.feed.ui.search.components.SingleUserAssetItem
|
||||
import com.tangem.features.feed.ui.search.state.BalanceDisplayState
|
||||
import com.tangem.features.feed.ui.search.state.UserAssetItemUM
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
/** Labeled UI state for [SingleUserAssetItem] previews (dropdown label in Studio). */
|
||||
internal data class SingleUserAssetItemPreviewScenario(
|
||||
val title: String,
|
||||
val item: UserAssetItemUM.Single,
|
||||
)
|
||||
|
||||
/** Labeled UI state for [GroupedUserAssetItem] previews. */
|
||||
internal data class GroupedUserAssetItemPreviewScenario(
|
||||
val title: String,
|
||||
val item: UserAssetItemUM.Grouped,
|
||||
)
|
||||
|
||||
@Suppress("StringLiteralDuplication")
|
||||
internal object UserAssetItemPreviewFixtures {
|
||||
|
||||
private val sampleIcon = TangemIconUM.Currency(
|
||||
CurrencyIconState.CoinIcon(
|
||||
url = null,
|
||||
fallbackResId = com.tangem.core.ui.R.drawable.ic_ethereumpow_22,
|
||||
isGrayscale = false,
|
||||
shouldShowCustomBadge = false,
|
||||
),
|
||||
)
|
||||
|
||||
private val cryptoRef = stringReference("1.234 ETH")
|
||||
private val fiatRef = stringReference("$121,876.50")
|
||||
|
||||
private val samplePriceChange = PriceChangeState.Content(
|
||||
valueInPercent = "+2.34%",
|
||||
type = PriceChangeType.UP,
|
||||
)
|
||||
|
||||
private fun balanceLoaded() = BalanceDisplayState.Loaded(
|
||||
cryptoBalance = cryptoRef,
|
||||
fiatBalance = fiatRef,
|
||||
)
|
||||
|
||||
private fun balanceFlickering() = BalanceDisplayState.Flickering(
|
||||
cryptoBalance = cryptoRef,
|
||||
fiatBalance = fiatRef,
|
||||
)
|
||||
|
||||
private fun balanceStale() = BalanceDisplayState.Stale(
|
||||
cryptoBalance = cryptoRef,
|
||||
fiatBalance = fiatRef,
|
||||
)
|
||||
|
||||
private fun balanceLoading() = BalanceDisplayState.Loading
|
||||
|
||||
private fun balanceUnreachable() = BalanceDisplayState.Unreachable
|
||||
|
||||
fun allSingleScenarios(): List<SingleUserAssetItemPreviewScenario> = listOf(
|
||||
SingleUserAssetItemPreviewScenario(
|
||||
title = "Hidden (balanceState ignored)",
|
||||
item = single(
|
||||
balanceState = balanceLoaded(),
|
||||
isBalanceHidden = true,
|
||||
),
|
||||
),
|
||||
SingleUserAssetItemPreviewScenario(
|
||||
title = "Balance – Loaded",
|
||||
item = single(balanceState = balanceLoaded(), isBalanceHidden = false),
|
||||
),
|
||||
SingleUserAssetItemPreviewScenario(
|
||||
title = "Balance – Flickering",
|
||||
item = single(balanceState = balanceFlickering(), isBalanceHidden = false),
|
||||
),
|
||||
SingleUserAssetItemPreviewScenario(
|
||||
title = "Balance – Stale",
|
||||
item = single(balanceState = balanceStale(), isBalanceHidden = false),
|
||||
),
|
||||
SingleUserAssetItemPreviewScenario(
|
||||
title = "Balance – Loading",
|
||||
item = single(balanceState = balanceLoading(), isBalanceHidden = false),
|
||||
),
|
||||
SingleUserAssetItemPreviewScenario(
|
||||
title = "Balance – Unreachable",
|
||||
item = single(balanceState = balanceUnreachable(), isBalanceHidden = false),
|
||||
),
|
||||
)
|
||||
|
||||
fun allGroupedScenarios(): List<GroupedUserAssetItemPreviewScenario> = listOf(
|
||||
GroupedUserAssetItemPreviewScenario(
|
||||
title = "Hidden (balanceState ignored)",
|
||||
item = grouped(
|
||||
balanceState = balanceLoaded(),
|
||||
isBalanceHidden = true,
|
||||
),
|
||||
),
|
||||
GroupedUserAssetItemPreviewScenario(
|
||||
title = "Balance – Loaded",
|
||||
item = grouped(balanceState = balanceLoaded(), isBalanceHidden = false),
|
||||
),
|
||||
GroupedUserAssetItemPreviewScenario(
|
||||
title = "Balance – Flickering",
|
||||
item = grouped(balanceState = balanceFlickering(), isBalanceHidden = false),
|
||||
),
|
||||
GroupedUserAssetItemPreviewScenario(
|
||||
title = "Balance – Stale",
|
||||
item = grouped(balanceState = balanceStale(), isBalanceHidden = false),
|
||||
),
|
||||
GroupedUserAssetItemPreviewScenario(
|
||||
title = "Balance – Loading",
|
||||
item = grouped(balanceState = balanceLoading(), isBalanceHidden = false),
|
||||
),
|
||||
GroupedUserAssetItemPreviewScenario(
|
||||
title = "Balance – Unreachable",
|
||||
item = grouped(balanceState = balanceUnreachable(), isBalanceHidden = false),
|
||||
),
|
||||
)
|
||||
|
||||
private fun single(balanceState: BalanceDisplayState, isBalanceHidden: Boolean): UserAssetItemUM.Single =
|
||||
UserAssetItemUM.Single(
|
||||
id = "single_preview",
|
||||
icon = sampleIcon,
|
||||
tokenName = "Ethereum",
|
||||
tokenSymbol = "ETH",
|
||||
fiatRate = "$98,765.43",
|
||||
priceChangeState = samplePriceChange,
|
||||
balanceState = balanceState,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
onClick = {},
|
||||
)
|
||||
|
||||
private fun grouped(balanceState: BalanceDisplayState, isBalanceHidden: Boolean): UserAssetItemUM.Grouped =
|
||||
UserAssetItemUM.Grouped(
|
||||
id = "grouped_preview",
|
||||
icon = sampleIcon,
|
||||
tokenName = "Ethereum",
|
||||
tokenSymbol = "ETH",
|
||||
tokensCount = 3,
|
||||
balanceState = balanceState,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
children = persistentListOf(),
|
||||
onClick = {},
|
||||
)
|
||||
}
|
||||
|
||||
internal class SingleUserAssetItemPreviewParameterProvider :
|
||||
PreviewParameterProvider<SingleUserAssetItemPreviewScenario> {
|
||||
override val values: Sequence<SingleUserAssetItemPreviewScenario>
|
||||
get() = UserAssetItemPreviewFixtures.allSingleScenarios().asSequence()
|
||||
}
|
||||
|
||||
internal class GroupedUserAssetItemPreviewParameterProvider :
|
||||
PreviewParameterProvider<GroupedUserAssetItemPreviewScenario> {
|
||||
override val values: Sequence<GroupedUserAssetItemPreviewScenario>
|
||||
get() = UserAssetItemPreviewFixtures.allGroupedScenarios().asSequence()
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SingleUserAssetItemPreviewHost(
|
||||
scenario: SingleUserAssetItemPreviewScenario,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.background(TangemTheme.colors2.surface.level1)
|
||||
.padding(8.dp),
|
||||
) {
|
||||
SingleUserAssetItem(item = scenario.item)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun GroupedUserAssetItemPreviewHost(
|
||||
scenario: GroupedUserAssetItemPreviewScenario,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.background(TangemTheme.colors2.surface.level1)
|
||||
.padding(8.dp),
|
||||
) {
|
||||
GroupedUserAssetItem(item = scenario.item)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(name = "Single – all balance states (parameter)", showBackground = true, widthDp = 360)
|
||||
@Preview(
|
||||
name = "Single – all balance states (night)",
|
||||
showBackground = true,
|
||||
widthDp = 360,
|
||||
uiMode = Configuration.UI_MODE_NIGHT_YES,
|
||||
)
|
||||
private fun SingleUserAssetItemPreview_AllBalanceStates(
|
||||
@PreviewParameter(SingleUserAssetItemPreviewParameterProvider::class) scenario: SingleUserAssetItemPreviewScenario,
|
||||
) {
|
||||
TangemThemePreviewRedesign {
|
||||
SingleUserAssetItemPreviewHost(scenario = scenario)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(name = "Grouped – all balance states (parameter)", showBackground = true, widthDp = 360)
|
||||
@Preview(
|
||||
name = "Grouped – all balance states (night)",
|
||||
showBackground = true,
|
||||
widthDp = 360,
|
||||
uiMode = Configuration.UI_MODE_NIGHT_YES,
|
||||
)
|
||||
private fun GroupedUserAssetItemPreview_AllBalanceStates(
|
||||
@PreviewParameter(GroupedUserAssetItemPreviewParameterProvider::class)
|
||||
scenario: GroupedUserAssetItemPreviewScenario,
|
||||
) {
|
||||
TangemThemePreviewRedesign {
|
||||
GroupedUserAssetItemPreviewHost(scenario = scenario)
|
||||
}
|
||||
}
|
||||
|
|
@ -4,8 +4,11 @@ import androidx.compose.runtime.Immutable
|
|||
import com.tangem.common.ui.account.AccountNameUM
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeState
|
||||
import com.tangem.core.ui.ds.image.TangemIconUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.models.account.CryptoPortfolioIcon
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class SearchUM(
|
||||
|
|
@ -45,6 +48,28 @@ sealed interface MarketSearchResultUM {
|
|||
|
||||
data class TextHintItemUM(val text: String)
|
||||
|
||||
@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
|
||||
|
|
@ -59,8 +84,8 @@ sealed interface UserAssetItemUM {
|
|||
override val tokenName: String,
|
||||
override val tokenSymbol: String,
|
||||
val fiatRate: String?,
|
||||
val cryptoBalance: String,
|
||||
val fiatBalance: String,
|
||||
val priceChangeState: PriceChangeState,
|
||||
val balanceState: BalanceDisplayState,
|
||||
val isBalanceHidden: Boolean,
|
||||
override val onClick: () -> Unit,
|
||||
) : UserAssetItemUM
|
||||
|
|
@ -71,8 +96,7 @@ sealed interface UserAssetItemUM {
|
|||
override val tokenName: String,
|
||||
override val tokenSymbol: String,
|
||||
val tokensCount: Int,
|
||||
val totalCryptoBalance: String,
|
||||
val totalFiatBalance: String,
|
||||
val balanceState: BalanceDisplayState,
|
||||
val isBalanceHidden: Boolean,
|
||||
val children: ImmutableList<GroupedChild>,
|
||||
override val onClick: () -> Unit,
|
||||
|
|
@ -83,7 +107,6 @@ sealed interface UserAssetItemUM {
|
|||
val accountName: AccountNameUM,
|
||||
val accountIcon: CryptoPortfolioIcon.Icon,
|
||||
val accountColor: CryptoPortfolioIcon.Color,
|
||||
val cryptoBalance: String,
|
||||
val fiatBalance: String,
|
||||
val currencyStatus: CryptoCurrencyStatus,
|
||||
)
|
||||
}
|
||||
|
|
@ -27,11 +27,7 @@ internal fun contentFeedEntryStackAnimation(): StackAnimation<
|
|||
ComposableModularBottomSheetContentComponent,
|
||||
> =
|
||||
stackAnimation { to, from, _ ->
|
||||
val isSearchToTokenList =
|
||||
(to.configuration as? FeedEntryChildFactory.Child.TokenList)?.params?.shouldAlwaysShowSearchBar == true
|
||||
val isFromSearchTokenList =
|
||||
(from.configuration as? FeedEntryChildFactory.Child.TokenList)?.params?.shouldAlwaysShowSearchBar == true
|
||||
if (isSearchToTokenList || isFromSearchTokenList) {
|
||||
if (to.configuration.usesFadeStackTransition() || from.configuration.usesFadeStackTransition()) {
|
||||
fade()
|
||||
} else {
|
||||
slide()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue