Updated on 2026-08-14
This commit is contained in:
parent
8541119c1a
commit
7703758e56
3 changed files with 362 additions and 63 deletions
|
|
@ -7,10 +7,16 @@ import androidx.compose.foundation.clickable
|
|||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
|
|
@ -20,13 +26,16 @@ import androidx.compose.ui.semantics.semantics
|
|||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
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.SpacerH12
|
||||
import com.tangem.core.ui.components.SpacerH32
|
||||
import com.tangem.core.ui.components.SpacerW2
|
||||
import com.tangem.core.ui.components.appbar.ExpandableSearchView
|
||||
import com.tangem.core.ui.components.list.InfiniteListHandler
|
||||
import com.tangem.core.ui.components.atoms.text.EllipsisText
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.components.tokenlist.PortfolioListItem
|
||||
import com.tangem.core.ui.components.tokenlist.PortfolioTokensListItem
|
||||
import com.tangem.core.ui.components.tokenlist.TokenListItem
|
||||
|
|
@ -38,12 +47,18 @@ import com.tangem.core.ui.res.TangemThemePreview
|
|||
import com.tangem.core.ui.test.BuyTokenScreenTestTags
|
||||
import com.tangem.core.ui.utils.lazyListItemPosition
|
||||
import com.tangem.feature.swap.models.SwapSelectTokenStateHolder
|
||||
import com.tangem.feature.swap.models.TokenBalanceData
|
||||
import com.tangem.feature.swap.models.TokenListUMData
|
||||
import com.tangem.feature.swap.models.TokenToSelectState
|
||||
import com.tangem.feature.swap.models.isEmptyState
|
||||
import com.tangem.feature.swap.models.isNotFoundState
|
||||
import com.tangem.feature.swap.models.market.state.SwapMarketState
|
||||
import com.tangem.feature.swap.presentation.R
|
||||
import com.tangem.feature.swap.ui.market.swapMarketsListItems
|
||||
import com.tangem.feature.swap.ui.preview.SwapSelectTokenPreviewProvider
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
private const val LOAD_MORE_BUFFER = 25
|
||||
|
||||
@Composable
|
||||
internal fun SwapSelectTokenScreen(state: SwapSelectTokenStateHolder, onBack: () -> Unit) {
|
||||
|
|
@ -56,17 +71,9 @@ internal fun SwapSelectTokenScreen(state: SwapSelectTokenStateHolder, onBack: ()
|
|||
content = { padding ->
|
||||
val modifier = Modifier.padding(padding)
|
||||
when {
|
||||
state.availableTokens.isEmpty() && state.unavailableTokens.isEmpty() &&
|
||||
state.tokensListData.tokensList.isEmpty() && state.isAfterSearch -> {
|
||||
TokensNotFound(modifier)
|
||||
}
|
||||
state.availableTokens.isEmpty() && state.unavailableTokens.isEmpty() &&
|
||||
state.tokensListData.tokensList.isEmpty() && !state.isAfterSearch -> {
|
||||
EmptyTokensList(modifier)
|
||||
}
|
||||
else -> {
|
||||
ListOfTokens(state = state, modifier = modifier)
|
||||
}
|
||||
state.isNotFoundState -> TokensNotFound(modifier)
|
||||
state.isEmptyState -> EmptyTokensList(modifier)
|
||||
else -> ListOfTokens(state = state, modifier = modifier)
|
||||
}
|
||||
},
|
||||
topBar = {
|
||||
|
|
@ -135,19 +142,24 @@ private fun TokensNotFound(modifier: Modifier = Modifier) {
|
|||
@Composable
|
||||
private fun ListOfTokens(state: SwapSelectTokenStateHolder, modifier: Modifier = Modifier) {
|
||||
val screenBackgroundColor = TangemTheme.colors.background.secondary
|
||||
val lazyListState = rememberLazyListState()
|
||||
|
||||
LazyColumn(
|
||||
modifier = modifier
|
||||
.background(color = screenBackgroundColor)
|
||||
.fillMaxSize()
|
||||
.imePadding(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
state = lazyListState,
|
||||
) {
|
||||
when (val list = state.tokensListData) {
|
||||
is TokenListUMData.AccountList -> list.tokensList.forEach { item ->
|
||||
portfolioTokensList(
|
||||
portfolio = item,
|
||||
isBalanceHidden = state.isBalanceHidden,
|
||||
)
|
||||
is TokenListUMData.AccountList -> {
|
||||
list.tokensList.forEach { item ->
|
||||
portfolioTokensList(
|
||||
portfolio = item,
|
||||
isBalanceHidden = state.isBalanceHidden,
|
||||
)
|
||||
}
|
||||
}
|
||||
is TokenListUMData.TokenList -> {
|
||||
tokensList(
|
||||
|
|
@ -158,13 +170,56 @@ private fun ListOfTokens(state: SwapSelectTokenStateHolder, modifier: Modifier =
|
|||
TokenListUMData.EmptyList -> Unit
|
||||
}
|
||||
|
||||
tokensToSelectItems(state.availableTokens, state.onTokenSelected)
|
||||
if (state.availableTokens.isNotEmpty()) {
|
||||
tokensToSelectItems(state.availableTokens, state.onTokenSelected)
|
||||
}
|
||||
if (state.unavailableTokens.isNotEmpty()) {
|
||||
item { SpacerH12() }
|
||||
tokensToSelectItems(state.unavailableTokens, state.onTokenSelected)
|
||||
}
|
||||
|
||||
item { SpacerH12() }
|
||||
if (state.availableTokens.isNotEmpty() || state.unavailableTokens.isNotEmpty()) {
|
||||
item { SpacerH32() }
|
||||
} else {
|
||||
item { SpacerH12() }
|
||||
}
|
||||
|
||||
tokensToSelectItems(state.unavailableTokens, state.onTokenSelected)
|
||||
state.marketsState?.let(::swapMarketsListItems)
|
||||
}
|
||||
|
||||
item { SpacerH12() }
|
||||
(state.marketsState as? SwapMarketState.Content)?.let { marketState ->
|
||||
VisibleItemsTracker(
|
||||
lazyListState = lazyListState,
|
||||
marketState = marketState,
|
||||
)
|
||||
|
||||
InfiniteListHandler(
|
||||
listState = lazyListState,
|
||||
buffer = LOAD_MORE_BUFFER,
|
||||
triggerLoadMoreCheckOnItemsCountChange = true,
|
||||
onLoadMore = remember(state) {
|
||||
{
|
||||
state.marketsState.loadMore()
|
||||
true
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun VisibleItemsTracker(lazyListState: LazyListState, marketState: SwapMarketState.Content) {
|
||||
val visibleItems by remember {
|
||||
derivedStateOf {
|
||||
lazyListState.layoutInfo.visibleItemsInfo
|
||||
.mapNotNull { itemInfo ->
|
||||
marketState.items.find { it.getComposeKey() == itemInfo.key }?.id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(visibleItems) {
|
||||
marketState.visibleIdsChanged(visibleItems)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -387,50 +442,45 @@ private fun TokenItem(
|
|||
}
|
||||
}
|
||||
|
||||
private val token = TokenToSelectState.TokenToSelect(
|
||||
tokenIcon = CurrencyIconState.CoinIcon(
|
||||
url = "",
|
||||
fallbackResId = 0,
|
||||
isGrayscale = false,
|
||||
shouldShowCustomBadge = false,
|
||||
),
|
||||
id = "",
|
||||
name = "Optimistic Ethereum (ETH)",
|
||||
symbol = "USDC",
|
||||
addedTokenBalanceData = TokenBalanceData(
|
||||
amount = "15 000 $",
|
||||
amountEquivalent = "15 000 USDT",
|
||||
isBalanceHidden = false,
|
||||
),
|
||||
)
|
||||
|
||||
private val title = TokenToSelectState.Title(
|
||||
title = stringReference("MY TOKENS"),
|
||||
)
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun TokenScreenPreview() {
|
||||
TangemThemePreview {
|
||||
SwapSelectTokenScreen(
|
||||
state = SwapSelectTokenStateHolder(
|
||||
availableTokens = listOf(title, token, token, token).toImmutableList(),
|
||||
unavailableTokens = listOf(title, token, token, token).toImmutableList(),
|
||||
tokensListData = TokenListUMData.EmptyList,
|
||||
isAfterSearch = false,
|
||||
isBalanceHidden = false,
|
||||
onSearchEntered = {},
|
||||
onTokenSelected = {},
|
||||
),
|
||||
onBack = {},
|
||||
)
|
||||
}
|
||||
private class SwapSelectTokenScreenPreviewProvider : PreviewParameterProvider<SwapSelectTokenStateHolder> {
|
||||
override val values: Sequence<SwapSelectTokenStateHolder> = sequenceOf(
|
||||
// Content state with tokens and markets
|
||||
SwapSelectTokenPreviewProvider().provideSwapSelectTokenState(),
|
||||
// Empty state
|
||||
SwapSelectTokenStateHolder(
|
||||
availableTokens = persistentListOf(),
|
||||
unavailableTokens = persistentListOf(),
|
||||
tokensListData = TokenListUMData.EmptyList,
|
||||
marketsState = null,
|
||||
isAfterSearch = false,
|
||||
isBalanceHidden = false,
|
||||
onSearchEntered = {},
|
||||
onTokenSelected = {},
|
||||
),
|
||||
// Not found state
|
||||
SwapSelectTokenStateHolder(
|
||||
availableTokens = persistentListOf(),
|
||||
unavailableTokens = persistentListOf(),
|
||||
tokensListData = TokenListUMData.EmptyList,
|
||||
marketsState = null,
|
||||
isAfterSearch = true,
|
||||
isBalanceHidden = false,
|
||||
onSearchEntered = {},
|
||||
onTokenSelected = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun EmptyTokensListPreview() {
|
||||
private fun TokenScreenPreview(
|
||||
@PreviewParameter(SwapSelectTokenScreenPreviewProvider::class)
|
||||
state: SwapSelectTokenStateHolder,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
EmptyTokensList()
|
||||
SwapSelectTokenScreen(
|
||||
state = state,
|
||||
onBack = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
package com.tangem.feature.swap.ui.market
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.common.ui.markets.MarketsListItem
|
||||
import com.tangem.common.ui.markets.MarketsListItemPlaceholder
|
||||
import com.tangem.core.ui.components.UnableToLoadData
|
||||
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.feature.swap.models.market.state.SwapMarketState
|
||||
import com.tangem.feature.swap.presentation.R
|
||||
|
||||
internal fun LazyListScope.swapMarketsListItems(state: SwapMarketState) {
|
||||
item {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.markets_common_title),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
)
|
||||
}
|
||||
when (state) {
|
||||
is SwapMarketState.Loading -> {
|
||||
items(count = 100, key = { "market_placeholder_$it" }) {
|
||||
MarketsListItemPlaceholder()
|
||||
}
|
||||
}
|
||||
is SwapMarketState.LoadingError -> {
|
||||
item(key = "market_loading_error") {
|
||||
LoadingErrorItem(
|
||||
modifier = Modifier.fillParentMaxWidth(),
|
||||
onTryAgain = state.onRetryClicked,
|
||||
)
|
||||
}
|
||||
}
|
||||
SwapMarketState.SearchNothingFound -> {
|
||||
item(key = "market_not_found") {
|
||||
SearchNothingFoundText(
|
||||
modifier = Modifier.fillParentMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
is SwapMarketState.Content -> {
|
||||
itemsIndexed(
|
||||
items = state.items,
|
||||
key = { _, item -> item.getComposeKey() },
|
||||
) { index, item ->
|
||||
MarketsListItem(
|
||||
model = item,
|
||||
onClick = { state.onItemClick(item) },
|
||||
modifier = Modifier.roundedShapeItemDecoration(
|
||||
currentIndex = index,
|
||||
lastIndex = state.items.lastIndex,
|
||||
backgroundColor = TangemTheme.colors.background.action,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LoadingErrorItem(onTryAgain: () -> Unit, modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier
|
||||
.padding(
|
||||
horizontal = TangemTheme.dimens.spacing16,
|
||||
vertical = TangemTheme.dimens.spacing12,
|
||||
),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
UnableToLoadData(onRetryClick = onTryAgain)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SearchNothingFoundText(modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier = modifier.padding(TangemTheme.dimens.spacing16),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.markets_search_token_no_result_title),
|
||||
style = TangemTheme.typography.caption1,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
package com.tangem.feature.swap.ui.preview
|
||||
|
||||
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.PriceChangeType
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.feature.swap.models.SwapSelectTokenStateHolder
|
||||
import com.tangem.feature.swap.models.TokenBalanceData
|
||||
import com.tangem.feature.swap.models.TokenListUMData
|
||||
import com.tangem.feature.swap.models.TokenToSelectState
|
||||
import com.tangem.feature.swap.models.market.state.SwapMarketState
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
internal class SwapSelectTokenPreviewProvider {
|
||||
|
||||
fun provideSwapSelectTokenState(): SwapSelectTokenStateHolder {
|
||||
return SwapSelectTokenStateHolder(
|
||||
availableTokens = listOf(previewTitle, previewToken, previewToken, previewToken).toImmutableList(),
|
||||
unavailableTokens = listOf(previewTitle, previewToken, previewToken, previewToken).toImmutableList(),
|
||||
tokensListData = TokenListUMData.EmptyList,
|
||||
isAfterSearch = false,
|
||||
isBalanceHidden = false,
|
||||
onSearchEntered = {},
|
||||
onTokenSelected = {},
|
||||
marketsState = createPreviewMarketsState(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createPreviewMarketsState() = SwapMarketState.Content(
|
||||
items = createPreviewMarketItems(),
|
||||
loadMore = { },
|
||||
onItemClick = { },
|
||||
visibleIdsChanged = { },
|
||||
)
|
||||
|
||||
private fun createPreviewMarketItems() = listOf(
|
||||
createMarketItem(
|
||||
id = "1",
|
||||
iconUrl = "",
|
||||
ratingPosition = "10",
|
||||
marketCap = "$6.233 B",
|
||||
trendType = PriceChangeType.UP,
|
||||
chartData = PREVIEW_CHART_DATA,
|
||||
),
|
||||
createMarketItem(
|
||||
id = "2",
|
||||
ratingPosition = "10",
|
||||
marketCap = "$6.233 B",
|
||||
trendType = PriceChangeType.NEUTRAL,
|
||||
chartData = null,
|
||||
),
|
||||
createMarketItem(
|
||||
id = "3",
|
||||
name = "Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin",
|
||||
ratingPosition = "10",
|
||||
marketCap = "$6.23348172384781234 B",
|
||||
trendType = PriceChangeType.DOWN,
|
||||
chartData = PREVIEW_CHART_DATA,
|
||||
),
|
||||
createMarketItem(
|
||||
id = "4",
|
||||
ratingPosition = "10",
|
||||
marketCap = null,
|
||||
trendType = PriceChangeType.UP,
|
||||
chartData = PREVIEW_CHART_DATA,
|
||||
),
|
||||
createMarketItem(
|
||||
id = "5",
|
||||
ratingPosition = null,
|
||||
marketCap = "$6.233 B",
|
||||
trendType = PriceChangeType.UP,
|
||||
chartData = PREVIEW_CHART_DATA,
|
||||
),
|
||||
createMarketItem(
|
||||
id = "6",
|
||||
ratingPosition = null,
|
||||
marketCap = null,
|
||||
trendType = PriceChangeType.UP,
|
||||
chartData = PREVIEW_CHART_DATA,
|
||||
),
|
||||
).toImmutableList()
|
||||
|
||||
private fun createMarketItem(
|
||||
id: String,
|
||||
name: String = "Bitcoin",
|
||||
iconUrl: String? = null,
|
||||
ratingPosition: String?,
|
||||
marketCap: String?,
|
||||
trendType: PriceChangeType,
|
||||
chartData: MarketChartRawData?,
|
||||
) = MarketsListItemUM(
|
||||
id = CryptoCurrency.RawID(id),
|
||||
name = name,
|
||||
currencySymbol = "BTC",
|
||||
iconUrl = iconUrl,
|
||||
ratingPosition = ratingPosition,
|
||||
marketCap = marketCap,
|
||||
price = MarketsListItemUM.Price(text = "31 285.72$"),
|
||||
trendPercentText = "12.43%",
|
||||
trendType = trendType,
|
||||
chartData = chartData,
|
||||
isUnder100kMarketCap = false,
|
||||
stakingRate = stringReference("APY 12.34%"),
|
||||
updateTimestamp = 0,
|
||||
)
|
||||
|
||||
companion object {
|
||||
private const val CHART_VALUE_1 = 0.4
|
||||
private const val CHART_VALUE_2 = 0.2
|
||||
private const val CHART_VALUE_3 = 0.1
|
||||
private const val CHART_VALUE_4 = 2.0
|
||||
private const val CHART_VALUE_5 = 5.0
|
||||
private const val CHART_VALUE_6 = 3.0
|
||||
|
||||
private val PREVIEW_CHART_DATA = MarketChartRawData(
|
||||
y = persistentListOf(
|
||||
CHART_VALUE_1,
|
||||
CHART_VALUE_2,
|
||||
CHART_VALUE_1,
|
||||
CHART_VALUE_3,
|
||||
CHART_VALUE_1,
|
||||
CHART_VALUE_4,
|
||||
CHART_VALUE_5,
|
||||
CHART_VALUE_3,
|
||||
CHART_VALUE_4,
|
||||
CHART_VALUE_4,
|
||||
CHART_VALUE_6,
|
||||
),
|
||||
)
|
||||
|
||||
private val previewToken = TokenToSelectState.TokenToSelect(
|
||||
tokenIcon = CurrencyIconState.CoinIcon(
|
||||
url = "",
|
||||
fallbackResId = 0,
|
||||
isGrayscale = false,
|
||||
shouldShowCustomBadge = false,
|
||||
),
|
||||
id = "",
|
||||
name = "Optimistic Ethereum (ETH)",
|
||||
symbol = "USDC",
|
||||
addedTokenBalanceData = TokenBalanceData(
|
||||
amount = "15 000 $",
|
||||
amountEquivalent = "15 000 USDT",
|
||||
isBalanceHidden = false,
|
||||
),
|
||||
)
|
||||
|
||||
private val previewTitle = TokenToSelectState.Title(
|
||||
title = stringReference("MY TOKENS"),
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue