Updated on 2026-08-14

This commit is contained in:
Tangem 2026-03-20 15:31:57 +03:00
commit 47dc1c38df
762 changed files with 23960 additions and 10924 deletions

View file

@ -7,6 +7,8 @@ import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.navigation.Route
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
import com.tangem.features.feed.components.earn.DefaultEarnComponent
import com.tangem.features.promobanners.api.NewPromoBannersFeatureToggles
import com.tangem.features.promobanners.api.PromoBannersBlockComponent
import com.tangem.features.feed.components.feed.DefaultFeedComponent
import com.tangem.features.feed.components.market.details.DefaultMarketsTokenDetailsComponent
import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioPreselectedDataComponent
@ -21,6 +23,8 @@ internal class FeedEntryChildFactory @Inject constructor(
private val analyticsEventHandler: AnalyticsEventHandler,
private val portfolioComponentFactory: MarketsPortfolioComponent.Factory,
private val addToPortfolioPreselectedDataComponent: AddToPortfolioPreselectedDataComponent.Factory,
private val promoBannersBlockComponentFactory: PromoBannersBlockComponent.Factory,
private val newPromoBannersFeatureToggles: NewPromoBannersFeatureToggles,
) {
@Serializable
@ -100,6 +104,8 @@ internal class FeedEntryChildFactory @Inject constructor(
appComponentContext = appComponentContext,
params = DefaultFeedComponent.FeedParams(feedClickIntents = feedEntryClickIntents),
addToPortfolioComponentFactory = addToPortfolioPreselectedDataComponent,
promoBannersBlockComponentFactory = promoBannersBlockComponentFactory,
newPromoBannersFeatureToggles = newPromoBannersFeatureToggles,
)
}
is Child.Earn -> {

View file

@ -11,12 +11,15 @@ import com.arkivanov.decompose.extensions.compose.subscribeAsState
import com.arkivanov.decompose.router.slot.childSlot
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.child
import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
import com.tangem.core.ui.decompose.EmptyComposableBottomSheetComponent
import com.tangem.features.promobanners.api.NewPromoBannersFeatureToggles
import com.tangem.features.promobanners.api.PromoBannersBlockComponent
import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioPreselectedDataComponent
import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioPreselectedDataComponent.Params
import com.tangem.features.feed.model.feed.FeedComponentModel
@ -28,10 +31,22 @@ internal class DefaultFeedComponent(
appComponentContext: AppComponentContext,
private val params: FeedParams,
private val addToPortfolioComponentFactory: AddToPortfolioPreselectedDataComponent.Factory,
private val promoBannersBlockComponentFactory: PromoBannersBlockComponent.Factory,
private val newPromoBannersFeatureToggles: NewPromoBannersFeatureToggles,
) : ComposableModularBottomSheetContentComponent, AppComponentContext by appComponentContext {
private val feedComponentModel = getOrCreateModel<FeedComponentModel, FeedParams>(params = params)
private val promoBannersBlockComponent: PromoBannersBlockComponent? by lazy {
if (!newPromoBannersFeatureToggles.isNewPromoBannersEnabled) return@lazy null
promoBannersBlockComponentFactory.create(
context = child("promoBannersBlockComponent"),
params = PromoBannersBlockComponent.Params(
placeholder = PromoBannersBlockComponent.Placeholder.FEED,
),
)
}
private val bottomSheetSlot = childSlot(
source = feedComponentModel.bottomSheetNavigation,
serializer = null,
@ -62,6 +77,7 @@ internal class DefaultFeedComponent(
FeedList(
modifier = modifier,
state = state,
promoBannersBlockComponent = promoBannersBlockComponent,
)
bottomSheet.child?.instance?.BottomSheet()
}

View file

@ -111,7 +111,7 @@ internal class PortfolioTokenUMConverter(
if (action.unavailabilityReason == ScenarioUnavailabilityReason.None) {
when (action) {
is TokenActionsState.ActionState.Buy -> QuickActionUM.Buy
is TokenActionsState.ActionState.Swap -> QuickActionUM.Exchange(action.showBadge)
is TokenActionsState.ActionState.Swap -> QuickActionUM.Exchange(action.shouldShowBadge)
is TokenActionsState.ActionState.Receive -> QuickActionUM.Receive
is TokenActionsState.ActionState.Stake -> QuickActionUM.Stake
is TokenActionsState.ActionState.YieldMode -> QuickActionUM.YieldMode(action.apy)

View file

@ -1,9 +1,16 @@
package com.tangem.features.feed.components.news.details
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
@ -12,7 +19,13 @@ import com.tangem.core.ui.components.appbar.TangemTopAppBar
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
import com.tangem.core.ui.ds.topbar.TangemTopBar
import com.tangem.core.ui.ds.topbar.TangemTopBarType
import com.tangem.core.ui.extensions.clickableSingle
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.TokenMarketParams
import com.tangem.domain.news.model.NewsListConfig
@ -32,21 +45,65 @@ internal class DefaultNewsDetailsComponent(
override fun Title(bottomSheetState: State<BottomSheetState>) {
val background = LocalMainBottomSheetColor.current.value
val state by newsDetailsModel.state.collectAsStateWithLifecycle()
TangemTopAppBar(
containerColor = background,
title = null,
startButton = TopAppBarButtonUM.Icon(
iconRes = R.drawable.ic_back_24,
onClicked = state.onBackClick,
isEnabled = bottomSheetState.value == BottomSheetState.EXPANDED,
),
endButton = TopAppBarButtonUM.Icon(
iconRes = R.drawable.ic_share_24,
onClicked = state.onShareClick,
isEnabled = bottomSheetState.value == BottomSheetState.EXPANDED &&
state.articlesStateUM is ArticlesStateUM.Content,
),
)
if (LocalRedesignEnabled.current) {
TangemTopBar(
title = resourceReference(R.string.common_news),
type = TangemTopBarType.BottomSheet,
startContent = {
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.ic_arrow_back_28),
contentDescription = null,
tint = TangemTheme.colors2.graphic.neutral.primary,
modifier = Modifier
.size(TangemTheme.dimens2.x11)
.background(
color = TangemTheme.colors2.button.backgroundSecondary,
shape = CircleShape,
)
.clickableSingle(
onClick = state.onBackClick,
enabled = bottomSheetState.value == BottomSheetState.EXPANDED,
)
.padding(TangemTheme.dimens2.x2),
)
},
endContent = {
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.ic_share_new_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 = state.onShareClick,
enabled = bottomSheetState.value == BottomSheetState.EXPANDED &&
state.articlesStateUM is ArticlesStateUM.Content,
)
.padding(TangemTheme.dimens2.x2_5),
)
},
)
} else {
TangemTopAppBar(
containerColor = background,
title = null,
startButton = TopAppBarButtonUM.Icon(
iconRes = R.drawable.ic_back_24,
onClicked = state.onBackClick,
isEnabled = bottomSheetState.value == BottomSheetState.EXPANDED,
),
endButton = TopAppBarButtonUM.Icon(
iconRes = R.drawable.ic_share_24,
onClicked = state.onShareClick,
isEnabled = bottomSheetState.value == BottomSheetState.EXPANDED &&
state.articlesStateUM is ArticlesStateUM.Content,
),
)
}
}
@Composable

View file

@ -1,9 +1,16 @@
package com.tangem.features.feed.components.news.list
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
@ -12,8 +19,14 @@ import com.tangem.core.ui.components.appbar.TangemTopAppBar
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
import com.tangem.core.ui.ds.topbar.TangemTopBar
import com.tangem.core.ui.ds.topbar.TangemTopBarType
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.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.news.model.NewsListConfig
import com.tangem.features.feed.model.news.list.NewsListModel
import com.tangem.features.feed.ui.news.list.NewsListContent
@ -30,15 +43,40 @@ internal class DefaultNewsListComponent(
override fun Title(bottomSheetState: State<BottomSheetState>) {
val background = LocalMainBottomSheetColor.current.value
val state by newsListModel.state.collectAsStateWithLifecycle()
TangemTopAppBar(
containerColor = background,
title = stringResourceSafe(R.string.common_news),
startButton = TopAppBarButtonUM.Icon(
iconRes = R.drawable.ic_back_24,
onClicked = state.onBackClick,
isEnabled = bottomSheetState.value == BottomSheetState.EXPANDED,
),
)
if (LocalRedesignEnabled.current) {
TangemTopBar(
title = resourceReference(R.string.common_news),
type = TangemTopBarType.BottomSheet,
startContent = {
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.ic_arrow_back_28),
contentDescription = null,
tint = TangemTheme.colors2.graphic.neutral.primary,
modifier = Modifier
.size(TangemTheme.dimens2.x11)
.background(
color = TangemTheme.colors2.button.backgroundSecondary,
shape = CircleShape,
)
.clickableSingle(
onClick = state.onBackClick,
enabled = bottomSheetState.value == BottomSheetState.EXPANDED,
)
.padding(TangemTheme.dimens2.x2),
)
},
)
} else {
TangemTopAppBar(
containerColor = background,
title = stringResourceSafe(R.string.common_news),
startButton = TopAppBarButtonUM.Icon(
iconRes = R.drawable.ic_back_24,
onClicked = state.onBackClick,
isEnabled = bottomSheetState.value == BottomSheetState.EXPANDED,
),
)
}
}
@Composable

View file

@ -0,0 +1,21 @@
package com.tangem.features.feed.deeplink
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.features.feed.entry.deeplink.MarketsDeepLinkHandler
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class DefaultMarketsDeepLinkHandler @AssistedInject constructor(
appRouter: AppRouter,
) : MarketsDeepLinkHandler {
init {
appRouter.push(AppRoute.Markets)
}
@AssistedFactory
interface Factory : MarketsDeepLinkHandler.Factory {
override fun create(): DefaultMarketsDeepLinkHandler
}
}

View file

@ -0,0 +1,80 @@
package com.tangem.features.feed.deeplink
import arrow.core.getOrElse
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.common.routing.deeplink.DeeplinkConst.TOKEN_ID_KEY
import com.tangem.data.common.currency.getTokenIconUrlFromDefaultHost
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.GetTokenMarketInfoUseCase
import com.tangem.domain.markets.TokenMarketParams
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.features.feed.entry.deeplink.MarketsTokenDetailDeepLinkHandler
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
internal class DefaultMarketsTokenDetailDeepLinkHandler @AssistedInject constructor(
@Assisted private val scope: CoroutineScope,
@Assisted private val queryParams: Map<String, String>,
private val appRouter: AppRouter,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getTokenMarketInfoUseCase: GetTokenMarketInfoUseCase,
) : MarketsTokenDetailDeepLinkHandler {
init {
handleDeepLink()
}
private fun handleDeepLink() {
val tokenId = queryParams[TOKEN_ID_KEY]
val rawTokenId = CryptoCurrency.RawID(tokenId.orEmpty())
scope.launch {
val appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse {
AppCurrency.Default
}
val tokenInfo = getTokenMarketInfoUseCase(
appCurrency = appCurrency,
tokenId = rawTokenId,
tokenSymbol = "", // used for analytics
).getOrElse {
Timber.e("Failed to get market token info")
return@launch
}
appRouter.push(
AppRoute.MarketsTokenDetails(
token = TokenMarketParams(
id = rawTokenId,
name = tokenInfo.name,
symbol = tokenInfo.symbol,
tokenQuotes = TokenMarketParams.Quotes(
currentPrice = tokenInfo.quotes.currentPrice,
h24Percent = tokenInfo.quotes.h24ChangePercent,
weekPercent = tokenInfo.quotes.weekChangePercent,
monthPercent = tokenInfo.quotes.monthChangePercent,
),
imageUrl = getTokenIconUrlFromDefaultHost(rawTokenId),
),
appCurrency = appCurrency,
shouldShowPortfolio = true,
analyticsParams = null,
),
)
}
}
@AssistedFactory
interface Factory : MarketsTokenDetailDeepLinkHandler.Factory {
override fun create(
coroutineScope: CoroutineScope,
queryParams: Map<String, String>,
): DefaultMarketsTokenDetailDeepLinkHandler
}
}

View file

@ -0,0 +1,26 @@
package com.tangem.features.feed.deeplink.di
import com.tangem.features.feed.deeplink.DefaultMarketsDeepLinkHandler
import com.tangem.features.feed.deeplink.DefaultMarketsTokenDetailDeepLinkHandler
import com.tangem.features.feed.entry.deeplink.MarketsDeepLinkHandler
import com.tangem.features.feed.entry.deeplink.MarketsTokenDetailDeepLinkHandler
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface MarketsDeepLinkModule {
@Binds
@Singleton
fun bindMarketsDeepLinkHandlerFactory(impl: DefaultMarketsDeepLinkHandler.Factory): MarketsDeepLinkHandler.Factory
@Binds
@Singleton
fun bindMarketsTokenDetailDeepLinkHandlerFactory(
impl: DefaultMarketsTokenDetailDeepLinkHandler.Factory,
): MarketsTokenDetailDeepLinkHandler.Factory
}

View file

@ -1,5 +1,6 @@
package com.tangem.features.feed.featuretoggle
import com.tangem.core.configtoggle.FeatureToggles
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.features.feed.entry.featuretoggle.FeedFeatureToggle
@ -8,5 +9,5 @@ internal class DefaultFeedFeatureToggle(
) : FeedFeatureToggle {
override val isEarnBlockEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled("EARN_BLOCK_ENABLED")
get() = featureTogglesManager.isFeatureEnabled(FeatureToggles.EARN_BLOCK_ENABLED)
}

View file

@ -8,7 +8,6 @@ import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.news.ShortArticle
import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM
import com.tangem.features.feed.ui.utils.mapFormattedDate
import com.tangem.utils.Provider
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableSet
@ -16,7 +15,7 @@ import kotlinx.collections.immutable.toPersistentList
import kotlinx.collections.immutable.toPersistentSet
internal class ShortArticleToArticleConfigUMConverter(
private val isTrending: Provider<Boolean>?,
private val isTrending: Boolean?,
) : Converter<List<ShortArticle>, ImmutableList<ArticleConfigUM>> {
override fun convert(value: List<ShortArticle>): ImmutableList<ArticleConfigUM> {
@ -25,7 +24,7 @@ internal class ShortArticleToArticleConfigUMConverter(
id = shortArticle.id,
title = shortArticle.title,
score = shortArticle.score,
isTrending = isTrending?.invoke() ?: shortArticle.isTrending,
isTrending = isTrending ?: shortArticle.isTrending,
tags = buildArticleTags(shortArticle),
createdAt = mapFormattedDate(shortArticle.createdAt),
isViewed = shortArticle.viewed,

View file

@ -9,7 +9,6 @@ import com.tangem.features.feed.model.feed.analytics.FeedAnalyticsEvent
import com.tangem.features.feed.ui.feed.state.FeedListUM
import com.tangem.features.feed.ui.feed.state.NewsUM
import com.tangem.features.feed.ui.feed.state.NewsUMState
import com.tangem.utils.Provider
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
import org.joda.time.DateTime
@ -92,6 +91,6 @@ internal class UpdateTrendingNewsStateTransformer(
}
private fun getShortArticleConfigConverter(isTrending: Boolean): ShortArticleToArticleConfigUMConverter {
return ShortArticleToArticleConfigUMConverter(isTrending = Provider { isTrending })
return ShortArticleToArticleConfigUMConverter(isTrending = isTrending)
}
}

View file

@ -16,6 +16,7 @@ import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.navigation.share.ShareManager
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.DesignFeatureToggles
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.marketprice.PriceChangeType
@ -47,6 +48,7 @@ import com.tangem.features.feed.model.converter.ShortArticleToArticleConfigUMCon
import com.tangem.features.feed.model.market.details.analytics.MarketDetailsAnalyticsEvent
import com.tangem.features.feed.model.market.details.converter.DescriptionConverter
import com.tangem.features.feed.model.market.details.converter.ExchangeItemStateConverter
import com.tangem.features.feed.model.market.details.converter.ExchangeItemStateConverterV2
import com.tangem.features.feed.model.market.details.converter.TokenMarketInfoConverter
import com.tangem.features.feed.model.market.details.formatter.*
import com.tangem.features.feed.model.market.details.state.QuotesStateUpdater
@ -77,6 +79,7 @@ internal class MarketsTokenDetailsModel @Inject constructor(
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
getUserCountryUseCase: GetUserCountryUseCase,
paramsContainer: ParamsContainer,
private val designFeatureToggles: DesignFeatureToggles,
private val getTokenPriceChartUseCase: GetTokenPriceChartUseCase,
private val getTokenMarketInfoUseCase: GetTokenMarketInfoUseCase,
private val getTokenFullQuotesUseCase: GetTokenFullQuotesUseCase,
@ -147,11 +150,12 @@ internal class MarketsTokenDetailsModel @Inject constructor(
needApplyFCARestrictions = Provider {
userCountry.needApplyFCARestrictions()
},
isRedesignEnabled = designFeatureToggles.isRedesignEnabled,
// ==================
)
private val shortArticleToArticleConfigUMConverter by lazy {
ShortArticleToArticleConfigUMConverter(isTrending = Provider { false })
ShortArticleToArticleConfigUMConverter(isTrending = false)
}
private val descriptionConverter = DescriptionConverter(
@ -701,9 +705,15 @@ internal class MarketsTokenDetailsModel @Inject constructor(
ExchangesBottomSheetContent.Error(onRetryClick = { onListedOnClick(exchangesCount) })
},
ifRight = { list ->
ExchangesBottomSheetContent.Content(
exchangeItems = ExchangeItemStateConverter.convertList(list).toImmutableList(),
)
if (designFeatureToggles.isRedesignEnabled) {
ExchangesBottomSheetContent.ContentV2(
exchangeItemsV2 = ExchangeItemStateConverterV2.convertList(list).toImmutableList(),
)
} else {
ExchangesBottomSheetContent.ContentV1(
exchangeItems = ExchangeItemStateConverter.convertList(list).toImmutableList(),
)
}
},
)

View file

@ -0,0 +1,60 @@
package com.tangem.features.feed.model.market.details.converter
import com.tangem.core.ui.components.audits.AuditLabelUM
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.format.bigdecimal.compact
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.markets.TokenMarketExchange
import com.tangem.domain.markets.TokenMarketExchange.TrustScore
import com.tangem.features.feed.impl.R
import com.tangem.features.feed.ui.market.detailed.state.ExchangeItemUM
import com.tangem.utils.converter.Converter
internal object ExchangeItemStateConverterV2 : Converter<TokenMarketExchange, ExchangeItemUM> {
override fun convert(value: TokenMarketExchange): ExchangeItemUM.Content {
return ExchangeItemUM.Content(
id = value.id,
title = stringReference(value.name),
subTitle = stringReference(value = if (value.isCentralized) "CEX" else "DEX"),
icon = TangemIconUM.Currency(
CurrencyIconState.CoinIcon(
url = value.imageUrl,
fallbackResId = R.drawable.ic_alert_24,
isGrayscale = false,
shouldShowCustomBadge = false,
),
),
volumeInUsd = stringReference(
value.volumeInUsd.format {
fiat(
fiatCurrencyCode = "USD",
fiatCurrencySymbol = "$",
).compact()
},
),
auditLabel = value.trustScore.toAuditLabelUM(),
)
}
private fun TrustScore.toAuditLabelUM(): AuditLabelUM {
return when (this) {
TrustScore.Risky -> AuditLabelUM(
text = resourceReference(id = R.string.markets_token_details_exchange_trust_score_risky),
type = AuditLabelUM.Type.Prohibition,
)
TrustScore.Caution -> AuditLabelUM(
text = resourceReference(id = R.string.markets_token_details_exchange_trust_score_caution),
type = AuditLabelUM.Type.Warning,
)
TrustScore.Trusted -> AuditLabelUM(
text = resourceReference(id = R.string.markets_token_details_exchange_trust_score_trusted),
type = AuditLabelUM.Type.Permit,
)
}
}
}

View file

@ -2,6 +2,8 @@ package com.tangem.features.feed.model.market.details.converter
import androidx.compose.runtime.Stable
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.format.bigdecimal.compact
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.fiat
@ -9,20 +11,23 @@ import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.features.feed.impl.R
import com.tangem.features.feed.ui.market.detailed.state.InfoBottomSheetContent
import com.tangem.features.feed.ui.market.detailed.state.InfoPointUM
import com.tangem.features.feed.ui.market.detailed.state.MetricsUM
import com.tangem.features.feed.ui.market.detailed.state.*
import com.tangem.utils.Provider
import com.tangem.utils.StringsSigns
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import java.math.BigDecimal
import java.math.RoundingMode
@Suppress("LargeClass")
@Stable
internal class MetricsConverter(
private val appCurrency: Provider<AppCurrency>,
private val tokenSymbol: String,
private val onInfoClick: (InfoBottomSheetContent) -> Unit,
private val isRedesignEnabled: Boolean,
) : Converter<TokenMarketInfo.Metrics, MetricsUM> {
@Suppress("LongMethod")
@ -115,10 +120,120 @@ internal class MetricsConverter(
},
),
),
metricsV2 = if (isRedesignEnabled) {
convertToMetricsV2UM(value)
} else {
null
},
)
}
}
@Suppress("LongMethod", "NestedScopeFunctions")
private fun convertToMetricsV2UM(value: TokenMarketInfo.Metrics): MetricsV2UM {
val infoPoints = with(value) {
val liquidity = getLiquidity(value.volume24h, value.marketCap)
persistentListOf(
InfoPointUMV2.MarketCap(
capitalizationValue = stringReference(marketCap.formatAmount()),
onInfoClick = {
onInfoClick(
InfoBottomSheetContent(
title = resourceReference(
R.string.markets_token_details_market_capitalization_full,
),
body = resourceReference(
R.string.markets_token_details_market_capitalization_description,
),
),
)
},
),
InfoPointUMV2.TradingVolume(
tradingValue = stringReference(volume24h.formatAmount()),
liquidity = liquidity,
trendingVolumeLiquidityType = getTrendingVolumeLiquidityType(liquidity),
onInfoClick = {
onInfoClick(
InfoBottomSheetContent(
title = resourceReference(R.string.markets_token_details_trading_volume_full),
body = resourceReference(
R.string.markets_token_details_trading_volume_24h_description,
),
),
)
},
),
InfoPointUMV2.MarketPosition(
position = marketRating?.toString() ?: StringsSigns.DASH_SIGN,
rangeValue = getMarketRatingRangeValue(marketRating),
marketRatingType = getMarketRatingType(marketRating),
onInfoClick = {
onInfoClick(
InfoBottomSheetContent(
title = resourceReference(R.string.markets_token_details_market_rating_full),
body = resourceReference(R.string.markets_token_details_market_rating_description),
),
)
},
),
InfoPointUMV2.FullyDilutedValuation(
value = resourceReference(
R.string.markets_token_details_valuation_value_in_total,
wrappedList(fullyDilutedValuation.formatAmount()),
),
onInfoClick = {
onInfoClick(
InfoBottomSheetContent(
title = resourceReference(
R.string.markets_token_details_fully_diluted_valuation_full,
),
body = resourceReference(
R.string.markets_token_details_fully_diluted_valuation_description,
),
),
)
},
),
InfoPointUMV2.CirculatingSupply(
currentValue = stringReference(circulatingSupply.formatAmount(crypto = true)),
maxValue = maxSupply?.let { supply ->
if (supply > BigDecimal.ZERO) {
stringReference(supply.formatMaxSupply())
} else {
null
}
},
fillValue = getCirculatingSupplyFillValue(circulatingSupply, maxSupply),
onInfoClick = {
onInfoClick(
InfoBottomSheetContent(
title = resourceReference(R.string.markets_token_details_max_supply_full),
body = resourceReference(R.string.markets_token_details_total_supply_description),
),
)
},
),
)
}
return buildMetricsV2UM(infoPoints)
}
private fun buildMetricsV2UM(infoPoints: ImmutableList<InfoPointUMV2>): MetricsV2UM {
val rows = infoPoints.chunked(size = 2)
.map { chunk ->
MetricsV2UM.Row(
first = chunk.first(),
second = chunk.getOrNull(1),
)
}
return MetricsV2UM(
rows = rows.toImmutableList(),
)
}
private fun BigDecimal?.formatMaxSupply(): String {
when (this) {
null -> return StringsSigns.DASH_SIGN
@ -149,4 +264,92 @@ internal class MetricsConverter(
}
}
}
@Suppress("MagicNumber")
private fun getLiquidity(volume24h: BigDecimal?, marketCap: BigDecimal?): Float {
if (volume24h == null || marketCap == null || marketCap == BigDecimal.ZERO) return 0f
val ratio = volume24h
.divide(marketCap, 6, RoundingMode.HALF_UP)
.toFloat()
return ratio.coerceIn(0f, 1f)
}
@Suppress("MagicNumber")
private fun getTrendingVolumeLiquidityType(liquidity: Float): TrendingVolumeLiquidityType {
return when {
liquidity >= 0.5f -> TrendingVolumeLiquidityType.HIGH
liquidity in 0.2f..<0.5f -> TrendingVolumeLiquidityType.MEDIUM
else -> TrendingVolumeLiquidityType.LOW
}
}
@Suppress("MagicNumber")
private fun getMarketRatingType(marketRating: Int?): MarketRatingType {
return when (marketRating) {
1 -> MarketRatingType.GOLD
2 -> MarketRatingType.SILVER
3 -> MarketRatingType.BRONZE
else -> MarketRatingType.OTHER
}
}
@Suppress("MagicNumber")
private fun getMarketRatingRangeValue(marketRating: Int?): Float {
if (marketRating == null) return 0f
return when {
marketRating <= 20 -> {
val segmentStart = 1f
val segmentEnd = 0.75f
val rangeMin = 1
val rangeMax = 20
val progress = segmentStart +
(marketRating - rangeMin).toFloat() / (rangeMax - rangeMin) * (segmentEnd - segmentStart)
progress.coerceIn(0f, 1f)
}
marketRating <= 100 -> {
val segmentStart = 0.76f
val segmentEnd = 0.5f
val rangeMin = 21
val rangeMax = 100
val progress = segmentStart +
(marketRating - rangeMin).toFloat() / (rangeMax - rangeMin) * (segmentEnd - segmentStart)
progress.coerceIn(0f, 1f)
}
marketRating <= 1000 -> {
val segmentStart = 0.51f
val segmentEnd = 0.25f
val rangeMin = 101
val rangeMax = 1000
val progress = segmentStart +
(marketRating - rangeMin).toFloat() / (rangeMax - rangeMin) * (segmentEnd - segmentStart)
progress.coerceIn(0f, 1f)
}
marketRating <= 10000 -> {
val segmentStart = 0.26f
val segmentEnd = 0.01f
val rangeMin = 1001
val rangeMax = 10000
val progress = segmentStart +
(marketRating - rangeMin).toFloat() / (rangeMax - rangeMin) * (segmentEnd - segmentStart)
progress.coerceIn(0f, 1f)
}
else -> {
0f
}
}
}
@Suppress("MagicNumber")
private fun getCirculatingSupplyFillValue(circulatingSupply: BigDecimal?, maxSupply: BigDecimal?): Float? {
if (circulatingSupply == null || maxSupply == null || maxSupply == BigDecimal.ZERO) return null
val ratio = circulatingSupply
.divide(maxSupply, 6, RoundingMode.HALF_UP)
.toFloat()
return ratio.coerceIn(0f, 1f)
}
}

View file

@ -15,6 +15,7 @@ import com.tangem.utils.converter.Converter
@Stable
@Suppress("LongParameterList")
internal class TokenMarketInfoConverter(
private val isRedesignEnabled: Boolean,
private val appCurrency: Provider<AppCurrency>,
private val needApplyFCARestrictions: Provider<Boolean>,
private val onInfoClick: (TangemBottomSheetConfigContent) -> Unit,
@ -47,6 +48,7 @@ internal class TokenMarketInfoConverter(
tokenSymbol = value.symbol,
appCurrency = appCurrency,
onInfoClick = onInfoClick,
isRedesignEnabled = isRedesignEnabled,
)
val exchangesAmount = value.exchangesAmount

View file

@ -0,0 +1,32 @@
package com.tangem.features.feed.ui.components
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.res.TangemTheme
@Composable
internal fun ContainerWithDivider(
modifier: Modifier = Modifier,
showDivider: Boolean = false,
paddingValues: PaddingValues = PaddingValues(start = TangemTheme.dimens2.x3),
content: @Composable () -> Unit,
) {
Box(modifier = modifier) {
content()
if (showDivider) {
HorizontalDivider(
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(paddingValues),
color = TangemTheme.colors2.graphic.neutral.quaternary,
thickness = 1.dp,
)
}
}
}

View file

@ -0,0 +1,77 @@
package com.tangem.features.feed.ui.components
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.heightIn
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.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.extensions.conditional
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
@Composable
internal fun MetricsCard(
modifier: Modifier = Modifier,
onClick: (() -> Unit)? = null,
cardColor: Color = TangemTheme.colors2.surface.level3,
title: @Composable () -> Unit,
content: @Composable () -> Unit,
) {
Column(
modifier = modifier
.background(
color = cardColor,
shape = RoundedCornerShape(TangemTheme.dimens2.x5),
)
.conditional(
condition = onClick != null,
modifier = {
if (onClick != null) {
clickable(onClick = onClick)
} else {
this
}
},
)
.padding(TangemTheme.dimens2.x4),
verticalArrangement = Arrangement.SpaceBetween,
) {
title()
content()
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun MetricsCardPreview() {
TangemThemePreviewRedesign {
MetricsCard(
modifier = Modifier.heightIn(120.dp),
title = {
Text(
text = "$ 22.4 M",
style = TangemTheme.typography2.headingBold22,
color = TangemTheme.colors2.text.neutral.primary,
)
},
content = {
Text(
text = "Market cap",
style = TangemTheme.typography2.captionSemibold12,
color = TangemTheme.colors2.text.neutral.tertiary,
)
},
onClick = {},
)
}
}

View file

@ -0,0 +1,90 @@
package com.tangem.features.feed.ui.components
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
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.draw.clip
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerWMax
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
@Composable
internal fun TokenMarketInformationBlock(
modifier: Modifier = Modifier,
shape: Shape = RoundedCornerShape(TangemTheme.dimens2.x6),
contentPadding: Dp = TangemTheme.dimens2.x4,
title: (@Composable () -> Unit),
content: (@Composable BoxScope.() -> Unit)? = null,
) {
Column(
modifier = modifier
.clip(shape)
.background(color = TangemTheme.colors2.surface.level3)
.padding(contentPadding),
horizontalAlignment = Alignment.Start,
) {
title()
if (content != null) {
Box(modifier = Modifier.fillMaxWidth()) {
content(this)
}
}
}
}
@Composable
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun TokenMarketInformationBlockPreview() {
TangemThemePreviewRedesign {
TokenMarketInformationBlock(
title = {
Row(
modifier = Modifier.padding(12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = "Listed on",
style = TangemTheme.typography2.headingSemibold20,
color = TangemTheme.colors2.text.neutral.primary,
)
SpacerWMax()
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_chevron_24),
tint = TangemTheme.colors2.markers.iconGray,
contentDescription = null,
)
}
},
content = {
Box(
modifier = Modifier
.fillMaxWidth()
.background(TangemTheme.colors2.surface.level2)
.padding(12.dp),
) {
Text(
text = "Smth",
color = TangemTheme.colors2.text.neutral.primary,
)
}
},
)
}
}

View file

@ -5,7 +5,7 @@ import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.*
import androidx.compose.material3.Icon
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
@ -13,33 +13,21 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onFirstVisible
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SmallButtonShimmer
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.SpacerWMax
import com.tangem.core.ui.components.UnableToLoadData
import com.tangem.core.ui.components.buttons.SecondarySmallButton
import com.tangem.core.ui.components.buttons.SmallButtonConfig
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.components.*
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.list.InfiniteListHandler
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.feed.ui.earn.components.EarnItemPlaceholder
import com.tangem.features.feed.ui.earn.components.EarnListItem
import com.tangem.features.feed.ui.earn.components.MostlyUsedCard
import com.tangem.features.feed.ui.earn.components.MostlyUsedPlaceholder
import com.tangem.core.ui.extensions.conditional
import com.tangem.core.ui.extensions.conditionalCompose
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.*
import com.tangem.features.feed.ui.earn.components.*
import com.tangem.features.feed.ui.earn.state.*
import kotlinx.collections.immutable.persistentListOf
@ -51,6 +39,7 @@ internal fun EarnContent(state: EarnUM, modifier: Modifier = Modifier) {
val density = LocalDensity.current
val bottomBarHeight = with(density) { WindowInsets.systemBars.getBottom(this).toDp() }
val listState = rememberLazyListState()
val isRedesignEnabled = LocalRedesignEnabled.current
if (state.bestOpportunities is EarnBestOpportunitiesUM.Content) {
PaginationHandler(
@ -69,7 +58,9 @@ internal fun EarnContent(state: EarnUM, modifier: Modifier = Modifier) {
item(key = "mostly_used_header") {
SectionHeader(
title = stringResourceSafe(R.string.earn_mostly_used),
modifier = Modifier.padding(top = 16.dp),
modifier = Modifier.padding(
top = if (isRedesignEnabled) 10.dp else 16.dp,
),
)
}
@ -83,7 +74,9 @@ internal fun EarnContent(state: EarnUM, modifier: Modifier = Modifier) {
item(key = "best_opportunities_header") {
SectionHeader(
title = stringResourceSafe(R.string.earn_best_opportunities),
modifier = Modifier.padding(top = 20.dp),
modifier = Modifier.padding(
top = if (isRedesignEnabled) 32.dp else 20.dp,
),
)
}
@ -97,14 +90,17 @@ internal fun EarnContent(state: EarnUM, modifier: Modifier = Modifier) {
)
}
bestOpportunitiesItems(
state = state.bestOpportunities,
)
if (isRedesignEnabled) {
bestOpportunitiesItemsV2(state = state.bestOpportunities)
} else {
bestOpportunitiesItemsV1(state = state.bestOpportunities)
}
}
}
@Composable
private fun MostlyUsedContent(state: EarnListUM, onScroll: () -> Unit) {
val isRedesignEnabled = LocalRedesignEnabled.current
AnimatedContent(
targetState = state,
contentKey = { it::class.java },
@ -115,10 +111,7 @@ private fun MostlyUsedContent(state: EarnListUM, onScroll: () -> Unit) {
}
is EarnListUM.Content -> {
LazyRow(
contentPadding = PaddingValues(
horizontal = 16.dp,
vertical = 12.dp,
),
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 12.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
itemsIndexed(
@ -127,12 +120,7 @@ private fun MostlyUsedContent(state: EarnListUM, onScroll: () -> Unit) {
) { index, item ->
val cardModifier = Modifier.conditional(
condition = index == FOURTH_ITEM_INDEX,
modifier = {
onFirstVisible(
minFractionVisible = 0.5f,
callback = onScroll,
)
},
modifier = { onFirstVisible(minFractionVisible = 0.5f, callback = onScroll) },
)
MostlyUsedCard(
modifier = cardModifier,
@ -147,15 +135,24 @@ private fun MostlyUsedContent(state: EarnListUM, onScroll: () -> Unit) {
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 12.dp)
.background(
color = TangemTheme.colors.background.action,
shape = TangemTheme.shapes.roundedCornersXMedium,
.conditionalCompose(
condition = isRedesignEnabled,
modifier = {
background(
color = TangemTheme.colors2.surface.level3,
shape = RoundedCornerShape(TangemTheme.dimens2.x4),
)
},
otherModifier = {
background(
color = TangemTheme.colors.background.action,
shape = TangemTheme.shapes.roundedCornersXMedium,
)
},
)
.padding(vertical = 32.dp, horizontal = 12.dp),
contentAlignment = Alignment.Center,
) {
UnableToLoadData(onRetryClick = animatedState.onRetryClicked)
}
) { UnableToLoadData(onRetryClick = animatedState.onRetryClicked) }
}
EarnListUM.Empty -> Unit // no need to handle
}
@ -179,7 +176,7 @@ private fun BestOpportunitiesFilters(
}
}
private fun LazyListScope.bestOpportunitiesItems(state: EarnBestOpportunitiesUM) {
private fun LazyListScope.bestOpportunitiesItemsV1(state: EarnBestOpportunitiesUM) {
when (state) {
is EarnBestOpportunitiesUM.Loading -> {
val lastIndex = PLACEHOLDER_ITEMS_COUNT - 1
@ -187,7 +184,7 @@ private fun LazyListScope.bestOpportunitiesItems(state: EarnBestOpportunitiesUM)
count = PLACEHOLDER_ITEMS_COUNT,
key = { "placeholder_$it" },
) { index ->
EarnItemPlaceholder(
EarnItemPlaceholderV1(
modifier = Modifier
.roundedShapeItemDecoration(
currentIndex = index,
@ -249,129 +246,126 @@ private fun LazyListScope.bestOpportunitiesItems(state: EarnBestOpportunitiesUM)
}
}
@Composable
private fun FilterButtons(
earnFilterUM: EarnFilterUM,
onNetworkFilterClick: () -> Unit,
onTypeFilterClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier.padding(horizontal = 16.dp),
) {
SecondarySmallButton(
config = SmallButtonConfig(
text = when (earnFilterUM.selectedNetworkFilter) {
is EarnFilterNetworkUM.AllNetworks -> TextReference.Res(R.string.earn_filter_all_networks)
is EarnFilterNetworkUM.MyNetworks -> TextReference.Res(R.string.earn_filter_my_networks)
is EarnFilterNetworkUM.Network -> TextReference.Str(earnFilterUM.selectedNetworkFilter.text)
},
onClick = onNetworkFilterClick,
icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_chevron_24),
isEnabled = earnFilterUM.isNetworkFilterEnabled,
),
)
SpacerWMax()
SecondarySmallButton(
config = SmallButtonConfig(
text = earnFilterUM.selectedTypeFilter.text,
onClick = onTypeFilterClick,
icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_chevron_24),
isEnabled = earnFilterUM.isTypeFilterEnabled,
),
)
private fun LazyListScope.bestOpportunitiesItemsV2(state: EarnBestOpportunitiesUM) {
when (state) {
is EarnBestOpportunitiesUM.Loading -> {
val lastIndex = PLACEHOLDER_ITEMS_COUNT - 1
items(
count = PLACEHOLDER_ITEMS_COUNT,
key = { "placeholder_$it" },
) { index ->
EarnItemPlaceholderV2(
modifier = Modifier
.roundedShapeItemDecoration(
currentIndex = index,
lastIndex = lastIndex,
backgroundColor = TangemTheme.colors2.surface.level3,
),
)
}
}
is EarnBestOpportunitiesUM.Empty -> {
item(key = "best_opportunities_empty") {
SpacerH(12.dp)
BestOpportunitiesEmpty() // TODO in [REDACTED_TASK_KEY]
}
}
is EarnBestOpportunitiesUM.EmptyFiltered -> {
item(key = "best_opportunities_empty_filtered") {
SpacerH(12.dp)
BestOpportunitiesEmptyFiltered(onClearFilterClick = state.onClearFilterClick) // TODO in [REDACTED_TASK_KEY]
}
}
is EarnBestOpportunitiesUM.Content -> {
if (state.items.isNotEmpty()) {
val lastIndex = state.items.lastIndex
itemsIndexed(
items = state.items,
key = { _, item -> "${item.tokenName}-${item.network}" },
) { index, item ->
EarnListItem(
item = item,
modifier = Modifier
.roundedShapeItemDecoration(
currentIndex = index,
lastIndex = lastIndex,
backgroundColor = TangemTheme.colors2.surface.level3,
),
)
}
}
}
is EarnBestOpportunitiesUM.Error -> {
item(key = "best_opportunities_error") {
SpacerH(12.dp)
Box(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.background(
color = TangemTheme.colors2.surface.level3,
shape = RoundedCornerShape(TangemTheme.dimens2.x5),
)
.padding(vertical = 142.dp, horizontal = 114.dp),
contentAlignment = Alignment.Center,
) {
UnableToLoadData(onRetryClick = state.onRetryClicked)
}
}
}
}
}
@Composable
private fun FilterButtonsShimmer(modifier: Modifier = Modifier) {
Row(
modifier = modifier.padding(horizontal = 16.dp),
) {
SmallButtonShimmer(
modifier = Modifier.width(110.dp),
)
SpacerWMax()
SmallButtonShimmer(
modifier = Modifier.width(90.dp),
)
}
}
@Composable
private fun BestOpportunitiesEmpty(modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.background(
color = TangemTheme.colors.background.action,
shape = TangemTheme.shapes.roundedCornersXMedium,
Row(modifier = modifier.padding(horizontal = 16.dp, vertical = 4.dp)) {
if (LocalRedesignEnabled.current) {
RectangleShimmer(
modifier = Modifier
.width(130.dp)
.height(36.dp),
radius = TangemTheme.dimens2.x25,
)
.padding(vertical = 32.dp, horizontal = 12.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
modifier = Modifier
.fillMaxWidth(),
painter = painterResource(R.drawable.ic_empty_64),
contentDescription = null,
tint = Color.Unspecified,
)
SpacerH(24.dp)
Text(
modifier = Modifier
.padding(horizontal = 32.dp),
text = stringResourceSafe(R.string.earn_empty),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
textAlign = TextAlign.Center,
)
}
}
@Composable
private fun BestOpportunitiesEmptyFiltered(onClearFilterClick: () -> Unit, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.background(
color = TangemTheme.colors.background.action,
shape = TangemTheme.shapes.roundedCornersXMedium,
SpacerWMax()
RectangleShimmer(
modifier = Modifier
.width(106.dp)
.height(36.dp),
radius = TangemTheme.dimens2.x25,
)
.padding(vertical = 32.dp, horizontal = 12.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = stringResourceSafe(R.string.earn_no_results),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
)
SpacerH(12.dp)
SecondarySmallButton(
config = SmallButtonConfig(
text = resourceReference(R.string.earn_clear_filter),
onClick = onClearFilterClick,
),
)
} else {
SmallButtonShimmer(
modifier = Modifier.width(110.dp),
)
SpacerWMax()
SmallButtonShimmer(
modifier = Modifier.width(90.dp),
)
}
}
}
@Composable
private fun SectionHeader(title: String, modifier: Modifier = Modifier) {
Text(
modifier = modifier
.fillMaxWidth()
.padding(start = 20.dp, end = 16.dp),
text = title,
style = TangemTheme.typography.h3,
color = TangemTheme.colors.text.primary1,
)
if (LocalRedesignEnabled.current) {
Text(
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 24.dp),
text = title,
style = TangemTheme.typography2.headingSemibold20,
color = TangemTheme.colors2.text.neutral.primary,
)
} else {
Text(
modifier = modifier
.fillMaxWidth()
.padding(start = 20.dp, end = 16.dp),
text = title,
style = TangemTheme.typography.h3,
color = TangemTheme.colors.text.primary1,
)
}
}
@Composable
@ -392,7 +386,7 @@ private fun PaginationHandler(listState: LazyListState, state: EarnBestOpportuni
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun EarnContentPreview() {
private fun EarnContentPreviewV1() {
TangemThemePreview {
val background = TangemTheme.colors.background.tertiary
CompositionLocalProvider(
@ -434,7 +428,49 @@ private fun EarnContentPreview() {
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun EarnContentLoadingPreview() {
private fun EarnContentPreviewV2() {
TangemThemePreviewRedesign {
val background = TangemTheme.colors2.surface.level3
CompositionLocalProvider(
LocalMainBottomSheetColor provides remember { mutableStateOf(background) },
) {
EarnContent(
state = previewEarnUM(
mostlyUsed = EarnListUM.Content(
items = persistentListOf(
previewEarnListItemUM(),
previewEarnListItemUM(
tokenName = "Cosmos",
symbol = "ATOM",
network = "Cosmos",
),
),
),
bestOpportunities = EarnBestOpportunitiesUM.Content(
items = persistentListOf(
previewEarnListItemUM(
tokenName = "Cosmos Hub",
symbol = "ATOM",
network = "Cosmos network",
),
previewEarnListItemUM(
tokenName = "Tether",
symbol = "USDT",
network = "Ethereum Network",
),
),
onLoadMore = {},
),
),
)
}
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun EarnContentLoadingPreviewV1() {
TangemThemePreview {
val background = TangemTheme.colors.background.tertiary
CompositionLocalProvider(
@ -453,7 +489,26 @@ private fun EarnContentLoadingPreview() {
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun EarnContentErrorPreview() {
private fun EarnContentLoadingPreviewV2() {
TangemThemePreviewRedesign {
val background = TangemTheme.colors2.surface.level3
CompositionLocalProvider(
LocalMainBottomSheetColor provides remember { mutableStateOf(background) },
) {
EarnContent(
state = previewEarnUM(
mostlyUsed = EarnListUM.Error(onRetryClicked = {}),
bestOpportunities = EarnBestOpportunitiesUM.Loading,
),
)
}
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun EarnContentErrorPreviewV1() {
TangemThemePreview {
val background = TangemTheme.colors.background.tertiary
CompositionLocalProvider(
@ -481,7 +536,35 @@ private fun EarnContentErrorPreview() {
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun EarnContentEmptyPreview() {
private fun EarnContentErrorPreviewV2() {
TangemThemePreviewRedesign {
val background = TangemTheme.colors2.surface.level3
CompositionLocalProvider(
LocalMainBottomSheetColor provides remember { mutableStateOf(background) },
) {
EarnContent(
state = previewEarnUM(
mostlyUsed = EarnListUM.Content(
items = persistentListOf(
previewEarnListItemUM(),
previewEarnListItemUM(
tokenName = "Cosmos",
symbol = "ATOM",
network = "Cosmos",
),
),
),
bestOpportunities = EarnBestOpportunitiesUM.Error(onRetryClicked = {}),
),
)
}
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun EarnContentEmptyPreviewV1() {
TangemThemePreview {
val background = TangemTheme.colors.background.tertiary
CompositionLocalProvider(
@ -506,6 +589,34 @@ private fun EarnContentEmptyPreview() {
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun EarnContentEmptyPreviewV2() {
TangemThemePreviewRedesign {
val background = TangemTheme.colors2.surface.level3
CompositionLocalProvider(
LocalMainBottomSheetColor provides remember { mutableStateOf(background) },
) {
EarnContent(
state = previewEarnUM(
mostlyUsed = EarnListUM.Content(
items = persistentListOf(
previewEarnListItemUM(),
previewEarnListItemUM(
tokenName = "Cosmos",
symbol = "ATOM",
network = "Cosmos",
),
),
),
bestOpportunities = EarnBestOpportunitiesUM.Empty,
),
)
}
}
}
@Composable
private fun previewEarnListItemUM(
tokenName: String = "USDC",

View file

@ -0,0 +1,93 @@
package com.tangem.features.feed.ui.earn.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
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.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemTheme
@Composable
internal fun BestOpportunitiesEmpty(modifier: Modifier = Modifier) {
if (LocalRedesignEnabled.current) {
BestOpportunitiesEmptyV2(modifier)
} else {
BestOpportunitiesEmptyV1(modifier)
}
}
@Composable
private fun BestOpportunitiesEmptyV1(modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.background(
color = TangemTheme.colors.background.action,
shape = TangemTheme.shapes.roundedCornersXMedium,
)
.padding(vertical = 32.dp, horizontal = 12.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
modifier = Modifier
.fillMaxWidth(),
painter = painterResource(R.drawable.ic_empty_64),
contentDescription = null,
tint = Color.Unspecified,
)
SpacerH(TangemTheme.dimens2.x6)
Text(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens2.x8),
text = stringResourceSafe(R.string.earn_empty),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
textAlign = TextAlign.Center,
)
}
}
@Composable
private fun BestOpportunitiesEmptyV2(modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxWidth()
.padding(horizontal = TangemTheme.dimens2.x4)
.background(
color = TangemTheme.colors2.surface.level3,
shape = RoundedCornerShape(TangemTheme.dimens2.x5),
)
.padding(vertical = TangemTheme.dimens2.x8, horizontal = TangemTheme.dimens2.x3),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
modifier = Modifier
.fillMaxWidth(),
painter = painterResource(R.drawable.ic_empty_64),
contentDescription = null,
tint = TangemTheme.colors2.graphic.neutral.quaternary,
)
SpacerH(TangemTheme.dimens2.x5)
Text(
modifier = Modifier.padding(horizontal = TangemTheme.dimens2.x8),
text = stringResourceSafe(R.string.earn_empty),
style = TangemTheme.typography2.bodyRegular14,
color = TangemTheme.colors2.text.neutral.secondary,
textAlign = TextAlign.Center,
)
}
}

View file

@ -0,0 +1,89 @@
package com.tangem.features.feed.ui.earn.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
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.Alignment
import androidx.compose.ui.Modifier
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.buttons.SecondarySmallButton
import com.tangem.core.ui.components.buttons.SmallButtonConfig
import com.tangem.core.ui.ds.button.*
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemTheme
@Composable
internal fun BestOpportunitiesEmptyFiltered(onClearFilterClick: () -> Unit, modifier: Modifier = Modifier) {
if (LocalRedesignEnabled.current) {
BestOpportunitiesEmptyFilteredV2(onClearFilterClick, modifier)
} else {
BestOpportunitiesEmptyFilteredV1(onClearFilterClick, modifier)
}
}
@Composable
private fun BestOpportunitiesEmptyFilteredV2(onClearFilterClick: () -> Unit, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxWidth()
.padding(horizontal = TangemTheme.dimens2.x4)
.background(
color = TangemTheme.colors2.surface.level3,
shape = RoundedCornerShape(TangemTheme.dimens2.x5),
)
.padding(vertical = TangemTheme.dimens2.x8, horizontal = TangemTheme.dimens2.x3),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = stringResourceSafe(R.string.earn_no_results),
style = TangemTheme.typography2.bodyRegular14,
color = TangemTheme.colors2.text.neutral.secondary,
)
SpacerH(TangemTheme.dimens2.x2)
TangemButton(
buttonUM = TangemButtonUM(
text = resourceReference(R.string.earn_clear_filter),
onClick = onClearFilterClick,
type = TangemButtonType.Secondary,
size = TangemButtonSize.X8,
shape = TangemButtonShape.Rounded,
),
)
}
}
@Composable
private fun BestOpportunitiesEmptyFilteredV1(onClearFilterClick: () -> Unit, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.background(
color = TangemTheme.colors.background.action,
shape = TangemTheme.shapes.roundedCornersXMedium,
)
.padding(vertical = 32.dp, horizontal = 12.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = stringResourceSafe(R.string.earn_no_results),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
)
SpacerH(12.dp)
SecondarySmallButton(
config = SmallButtonConfig(
text = resourceReference(R.string.earn_clear_filter),
onClick = onClearFilterClick,
),
)
}
}

View file

@ -0,0 +1,30 @@
package com.tangem.features.feed.ui.earn.components
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.res.TangemTheme
@Composable
internal fun CardFilterBlock(modifier: Modifier = Modifier, content: @Composable ColumnScope.() -> Unit) {
Column(
modifier = modifier
.fillMaxWidth()
.background(
color = TangemTheme.colors2.surface.level2,
shape = RoundedCornerShape(TangemTheme.dimens2.x5),
)
.border(
width = 1.dp,
color = TangemTheme.colors2.border.neutral.primary,
shape = RoundedCornerShape(TangemTheme.dimens2.x5),
),
content = content,
)
}

View file

@ -0,0 +1,55 @@
package com.tangem.features.feed.ui.earn.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetType
import com.tangem.core.ui.ds.topbar.TangemTopBar
import com.tangem.core.ui.ds.topbar.TangemTopBarType
import com.tangem.core.ui.extensions.clickableSingle
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.feed.impl.R
@Composable
internal inline fun <reified T : TangemBottomSheetConfigContent> EarnFilterBottomSheet(
config: TangemBottomSheetConfig,
crossinline content: @Composable (T) -> Unit,
) {
TangemBottomSheet<T>(
config = config,
type = TangemBottomSheetType.Modal,
containerColor = TangemTheme.colors2.surface.level3,
title = {
TangemTopBar(
title = resourceReference(R.string.earn_filter_by),
type = TangemTopBarType.BottomSheet,
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 = config.onDismissRequest)
.padding(TangemTheme.dimens2.x2_5),
)
},
)
},
content = content,
)
}

View file

@ -1,9 +1,7 @@
package com.tangem.features.feed.ui.earn.components
import android.content.res.Configuration
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
@ -15,11 +13,13 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEachIndexed
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
@ -28,28 +28,53 @@ import com.tangem.core.ui.components.inputrow.inner.DividerContainer
import com.tangem.core.ui.components.rows.RowContentContainer
import com.tangem.core.ui.components.rows.RowText
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
import com.tangem.core.ui.ds.checkbox.TangemCheckbox
import com.tangem.core.ui.ds.row.TangemRowContainer
import com.tangem.core.ui.ds.row.TangemRowLayoutId
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
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.earn.state.EarnFilterByNetworkBottomSheetContentUM
import com.tangem.features.feed.ui.earn.state.EarnFilterNetworkUM
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
@Composable
internal fun EarnFilterByNetworkBottomSheet(config: TangemBottomSheetConfig) {
if (LocalRedesignEnabled.current) {
EarnFilterByNetworkBottomSheetV2(config = config)
} else {
EarnFilterByNetworkBottomSheetV1(config = config)
}
}
@Composable
private fun EarnFilterByNetworkBottomSheetV1(config: TangemBottomSheetConfig) {
TangemBottomSheet<EarnFilterByNetworkBottomSheetContentUM>(
config = config,
titleText = resourceReference(R.string.earn_filter_by),
containerColor = TangemTheme.colors.background.tertiary,
content = { Content(it) },
content = { ContentV1(it) },
)
}
@Composable
private fun Content(content: EarnFilterByNetworkBottomSheetContentUM) {
private fun EarnFilterByNetworkBottomSheetV2(config: TangemBottomSheetConfig) {
EarnFilterBottomSheet<EarnFilterByNetworkBottomSheetContentUM>(
config = config,
content = { ContentV2(it) },
)
}
@Composable
private fun ContentV1(content: EarnFilterByNetworkBottomSheetContentUM) {
val allMyNetworks = remember(content) {
content.networks.filterIsInstance<EarnFilterNetworkUM.AllNetworks>() +
content.networks.filterIsInstance<EarnFilterNetworkUM.MyNetworks>()
@ -79,6 +104,139 @@ private fun Content(content: EarnFilterByNetworkBottomSheetContentUM) {
}
}
@Composable
private fun ContentV2(content: EarnFilterByNetworkBottomSheetContentUM) {
val allMyNetworks = remember(content) {
(content.networks.filterIsInstance<EarnFilterNetworkUM.AllNetworks>() +
content.networks.filterIsInstance<EarnFilterNetworkUM.MyNetworks>()).toImmutableList()
}
val specificNetworks = remember(content) {
content
.networks
.filterIsInstance<EarnFilterNetworkUM.Network>()
.toImmutableList()
}
Column(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens2.x4)
.padding(bottom = TangemTheme.dimens2.x4)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x3),
) {
NetworksTypesBlock(
allMyNetworks = allMyNetworks,
onOptionClick = content.onOptionClick,
)
SpecificNetworksBlock(
specificNetworks = specificNetworks,
onOptionClick = content.onOptionClick,
)
}
}
@Composable
private fun NetworksTypesBlock(
allMyNetworks: ImmutableList<EarnFilterNetworkUM>,
onOptionClick: (EarnFilterNetworkUM) -> Unit,
) {
CardFilterBlock {
allMyNetworks.fastForEachIndexed { index, item ->
TangemRowContainer(
modifier = Modifier
.roundedShapeItemDecoration(
currentIndex = index,
lastIndex = allMyNetworks.lastIndex,
addDefaultPadding = false,
)
.clickable { onOptionClick(item) },
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 12.dp),
) {
Text(
modifier = Modifier.layoutId(layoutId = TangemRowLayoutId.START_TOP),
text = when (item) {
is EarnFilterNetworkUM.AllNetworks -> TextReference.Res(R.string.earn_filter_all_networks)
is EarnFilterNetworkUM.MyNetworks -> TextReference.Res(R.string.earn_filter_my_networks)
is EarnFilterNetworkUM.Network -> TextReference.Str(item.text)
}.resolveReference(),
style = TangemTheme.typography2.bodySemibold16,
color = TangemTheme.colors2.text.neutral.primary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
TangemCheckbox(
modifier = Modifier
.padding(start = 8.dp)
.layoutId(layoutId = TangemRowLayoutId.TAIL),
isChecked = item.isSelected,
onCheckedChange = { onOptionClick(item) },
)
}
}
}
}
@Composable
private fun SpecificNetworksBlock(
specificNetworks: ImmutableList<EarnFilterNetworkUM.Network>,
onOptionClick: (EarnFilterNetworkUM) -> Unit,
) {
if (specificNetworks.isNotEmpty()) {
CardFilterBlock {
Text(
modifier = Modifier
.padding(horizontal = 16.dp)
.padding(top = 16.dp, bottom = 8.dp),
text = stringResourceSafe(id = R.string.earn_filter_networks),
style = TangemTheme.typography2.bodyRegular14,
color = TangemTheme.colors2.text.neutral.tertiary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
specificNetworks.fastForEachIndexed { index, item ->
TangemRowContainer(
modifier = Modifier
.roundedShapeItemDecoration(
currentIndex = index,
lastIndex = specificNetworks.lastIndex,
addDefaultPadding = false,
)
.clickable { onOptionClick(item) },
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 12.dp),
) {
Image(
modifier = Modifier
.size(40.dp)
.layoutId(TangemRowLayoutId.HEAD),
imageVector = ImageVector.vectorResource(item.iconRes),
contentDescription = item.symbol,
)
Text(
modifier = Modifier
.padding(start = 8.dp)
.layoutId(layoutId = TangemRowLayoutId.START_TOP),
text = item.text,
style = TangemTheme.typography2.bodySemibold16,
color = TangemTheme.colors2.text.neutral.primary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
TangemCheckbox(
modifier = Modifier
.padding(start = 8.dp)
.layoutId(layoutId = TangemRowLayoutId.TAIL),
isChecked = item.isSelected,
onCheckedChange = { onOptionClick(item) },
)
}
}
}
}
}
private fun LazyListScope.allMyNetworksList(
allMyNetworks: List<EarnFilterNetworkUM>,
onOptionClicked: (EarnFilterNetworkUM) -> Unit,
@ -201,7 +359,7 @@ private fun LazyListScope.specificNetworksList(
@Preview(widthDp = 360, heightDp = 800)
@Preview(widthDp = 360, heightDp = 800, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview() {
private fun PreviewV1() {
TangemThemePreview(
alwaysShowBottomSheets = true,
) {
@ -235,4 +393,43 @@ private fun Preview() {
)
}
}
}
@Preview(widthDp = 360, heightDp = 800)
@Preview(widthDp = 360, heightDp = 800, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PreviewV2() {
TangemThemePreviewRedesign(
alwaysShowBottomSheets = true,
) {
Box(Modifier.background(TangemTheme.colors.background.secondary)) {
EarnFilterByNetworkBottomSheet(
TangemBottomSheetConfig(
isShown = true,
onDismissRequest = {},
content = EarnFilterByNetworkBottomSheetContentUM(
networks = persistentListOf(
EarnFilterNetworkUM.AllNetworks(isSelected = true),
EarnFilterNetworkUM.MyNetworks(isSelected = false),
EarnFilterNetworkUM.Network(
id = "ethereum",
text = "Ethereum",
symbol = "ETH",
iconRes = R.drawable.img_btc_22,
isSelected = false,
),
EarnFilterNetworkUM.Network(
id = "polygon",
text = "Polygon",
symbol = "MATIC",
iconRes = R.drawable.img_btc_22,
isSelected = false,
),
),
onOptionClick = {},
),
),
)
}
}
}

View file

@ -5,34 +5,62 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
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 androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
import com.tangem.core.ui.components.inputrow.InputRowChecked
import com.tangem.core.ui.components.inputrow.inner.DividerContainer
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
import com.tangem.core.ui.ds.checkbox.TangemCheckbox
import com.tangem.core.ui.ds.row.TangemRowContainer
import com.tangem.core.ui.ds.row.TangemRowLayoutId
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
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.earn.state.EarnFilterByTypeBottomSheetContentUM
import com.tangem.features.feed.ui.earn.state.EarnFilterTypeUM
@Composable
internal fun EarnFilterByTypeBottomSheet(config: TangemBottomSheetConfig) {
if (LocalRedesignEnabled.current) {
EarnFilterByTypeBottomSheetV2(config)
} else {
EarnFilterByTypeBottomSheetV1(config)
}
}
@Composable
private fun EarnFilterByTypeBottomSheetV1(config: TangemBottomSheetConfig) {
TangemBottomSheet<EarnFilterByTypeBottomSheetContentUM>(
config = config,
titleText = resourceReference(R.string.earn_filter_by),
containerColor = TangemTheme.colors.background.tertiary,
content = { Content(it) },
content = { ContentV1(it) },
)
}
@Composable
private fun Content(content: EarnFilterByTypeBottomSheetContentUM) {
private fun EarnFilterByTypeBottomSheetV2(config: TangemBottomSheetConfig) {
EarnFilterBottomSheet<EarnFilterByTypeBottomSheetContentUM>(
config = config,
content = { ContentV2(it) },
)
}
@Composable
private fun ContentV1(content: EarnFilterByTypeBottomSheetContentUM) {
Column(
modifier = Modifier
.padding(
@ -62,10 +90,49 @@ private fun Content(content: EarnFilterByTypeBottomSheetContentUM) {
}
}
@Composable
private fun ContentV2(content: EarnFilterByTypeBottomSheetContentUM) {
CardFilterBlock(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens2.x4)
.padding(bottom = TangemTheme.dimens2.x4),
) {
EarnFilterTypeUM.entries.forEachIndexed { index, type ->
TangemRowContainer(
modifier = Modifier
.roundedShapeItemDecoration(
currentIndex = index,
lastIndex = EarnFilterTypeUM.entries.lastIndex,
addDefaultPadding = false,
)
.clickable { content.onOptionClick(type) },
contentPadding = PaddingValues(horizontal = TangemTheme.dimens2.x4, vertical = TangemTheme.dimens2.x3),
) {
Text(
modifier = Modifier.layoutId(layoutId = TangemRowLayoutId.START_TOP),
text = type.text.resolveReference(),
style = TangemTheme.typography2.bodySemibold16,
color = TangemTheme.colors2.text.neutral.primary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
TangemCheckbox(
modifier = Modifier
.padding(start = 8.dp)
.layoutId(layoutId = TangemRowLayoutId.TAIL),
isChecked = type == content.selectedOption,
onCheckedChange = { content.onOptionClick(type) },
)
}
}
}
}
@Preview(widthDp = 360, heightDp = 640)
@Preview(widthDp = 360, heightDp = 640, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview() {
private fun PreviewV1() {
TangemThemePreview(
alwaysShowBottomSheets = true,
) {
@ -82,4 +149,26 @@ private fun Preview() {
)
}
}
}
@Preview(widthDp = 360, heightDp = 640)
@Preview(widthDp = 360, heightDp = 640, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PreviewV2() {
TangemThemePreviewRedesign(
alwaysShowBottomSheets = true,
) {
Box(Modifier.background(TangemTheme.colors.background.secondary)) {
EarnFilterByTypeBottomSheet(
TangemBottomSheetConfig(
isShown = true,
onDismissRequest = {},
content = EarnFilterByTypeBottomSheetContentUM(
selectedOption = EarnFilterTypeUM.All,
onOptionClick = {},
),
),
)
}
}
}

View file

@ -4,12 +4,16 @@ import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
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.draw.clip
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@ -18,15 +22,33 @@ import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.SpacerW
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.TangemIcon
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.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.*
import com.tangem.features.feed.ui.earn.state.EarnListItemUM
@Composable
internal fun EarnListItem(item: EarnListItemUM, modifier: Modifier = Modifier) {
if (LocalRedesignEnabled.current) {
EarnListItemV2(
item = item,
modifier = modifier,
)
} else {
EarnListItemV1(
item = item,
modifier = modifier,
)
}
}
@Composable
private fun EarnListItemV1(item: EarnListItemUM, modifier: Modifier = Modifier) {
Row(
modifier = modifier
.fillMaxWidth()
@ -82,27 +104,117 @@ internal fun EarnListItem(item: EarnListItemUM, modifier: Modifier = Modifier) {
}
}
@Composable
private fun EarnListItemV2(item: EarnListItemUM, modifier: Modifier = Modifier) {
TangemRowContainer(
modifier = modifier.clickable { item.onItemClick() },
contentPadding = PaddingValues(12.dp),
content = {
TangemIcon(
tangemIconUM = TangemIconUM.Currency(item.currencyIconState),
modifier = Modifier
.layoutId(layoutId = TangemRowLayoutId.HEAD)
.padding(end = TangemTheme.dimens2.x2),
)
TokenTitle(
modifier = Modifier
.layoutId(layoutId = TangemRowLayoutId.START_TOP)
.padding(end = TangemTheme.dimens2.x2),
name = item.tokenName.resolveReference(),
symbol = item.symbol.resolveReference(),
)
Text(
modifier = Modifier
.layoutId(layoutId = TangemRowLayoutId.START_BOTTOM)
.padding(end = TangemTheme.dimens2.x2),
text = item.network.resolveReference(),
color = TangemTheme.colors2.text.neutral.secondary,
style = TangemTheme.typography2.captionSemibold12,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Text(
modifier = Modifier.layoutId(layoutId = TangemRowLayoutId.END_TOP),
text = item.earnValue.resolveReference(),
color = TangemTheme.colors2.text.neutral.primary,
style = TangemTheme.typography2.bodySemibold16,
maxLines = 1,
)
ModeBlock(
modifier = Modifier.layoutId(layoutId = TangemRowLayoutId.END_BOTTOM),
earnType = item.earnType,
)
},
)
}
@Composable
private fun TokenTitle(name: String, symbol: String, modifier: Modifier = Modifier) {
Row(modifier = modifier) {
Text(
modifier = Modifier
.weight(1f, fill = false)
.alignByBaseline(),
text = name,
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.subtitle2,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
if (LocalRedesignEnabled.current) {
Text(
modifier = Modifier
.weight(1f, fill = false)
.alignByBaseline(),
text = name,
color = TangemTheme.colors2.text.neutral.primary,
style = TangemTheme.typography2.bodySemibold16,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
SpacerW(4.dp)
Text(
modifier = Modifier.alignByBaseline(),
text = symbol,
color = TangemTheme.colors2.text.neutral.secondary,
style = TangemTheme.typography2.captionSemibold12,
maxLines = 1,
overflow = TextOverflow.Visible,
)
} else {
Text(
modifier = Modifier
.weight(1f, fill = false)
.alignByBaseline(),
text = name,
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.subtitle2,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
SpacerW(4.dp)
Text(
modifier = Modifier.alignByBaseline(),
text = symbol,
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.body2,
maxLines = 1,
overflow = TextOverflow.Visible,
)
}
}
}
@Composable
private fun ModeBlock(earnType: TextReference, modifier: Modifier = Modifier) {
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(2.dp),
) {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_staking_new_16),
tint = TangemTheme.colors2.markers.iconGray,
contentDescription = null,
)
SpacerW(4.dp)
Text(
modifier = Modifier.alignByBaseline(),
text = symbol,
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.body2,
maxLines = 1,
overflow = TextOverflow.Visible,
text = earnType.resolveReference(),
style = TangemTheme.typography2.captionSemibold12,
color = TangemTheme.colors2.text.neutral.tertiary,
)
}
}
@ -110,7 +222,7 @@ private fun TokenTitle(name: String, symbol: String, modifier: Modifier = Modifi
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun EarnListItemPreview() {
private fun EarnListItemPreviewV1() {
TangemThemePreview {
Column(modifier = Modifier.background(TangemTheme.colors.background.primary)) {
EarnListItem(
@ -151,4 +263,53 @@ private fun EarnListItemPreview() {
)
}
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun EarnListItemPreviewV2() {
TangemThemePreviewRedesign {
Column(modifier = Modifier.background(TangemTheme.colors.background.primary)) {
SpacerH(12.dp)
EarnListItem(
item = EarnListItemUM(
network = stringReference("Ethereum"),
symbol = stringReference("ETH"),
tokenName = stringReference("Ethereum"),
currencyIconState = CurrencyIconState.TokenIcon(
url = null,
topBadgeIconResId = R.drawable.img_eth_22,
fallbackTint = TangemColorPalette.Black,
fallbackBackground = TangemColorPalette.Meadow,
isGrayscale = false,
shouldShowCustomBadge = false,
),
earnValue = stringReference("APY 8.50%"),
earnType = stringReference("Yield"),
onItemClick = {},
),
)
SpacerH(12.dp)
EarnListItem(
item = EarnListItemUM(
network = stringReference("Ethereum"),
symbol = stringReference("USDT"),
tokenName = stringReference("Tether"),
currencyIconState = CurrencyIconState.TokenIcon(
url = null,
topBadgeIconResId = R.drawable.img_eth_22,
fallbackTint = TangemColorPalette.Black,
fallbackBackground = TangemColorPalette.Meadow,
isGrayscale = false,
shouldShowCustomBadge = false,
),
earnValue = stringReference("APY 8.50%"),
earnType = stringReference("Yield"),
onItemClick = {},
),
)
SpacerH(12.dp)
}
}
}

View file

@ -8,27 +8,13 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.CircleShimmer
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.SpacerW
import com.tangem.core.ui.components.SpacerWMax
import com.tangem.core.ui.components.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.TangemThemePreviewRedesign
@Composable
internal fun EarnListPlaceholder(modifier: Modifier = Modifier, placeholderCount: Int = PLACEHOLDER_ITEMS_COUNT) {
Column(
modifier = modifier.fillMaxSize(),
) {
repeat(placeholderCount) {
EarnItemPlaceholder()
}
}
}
@Composable
internal fun EarnItemPlaceholder(modifier: Modifier = Modifier) {
internal fun EarnItemPlaceholderV1(modifier: Modifier = Modifier) {
Row(
modifier = modifier
.fillMaxWidth()
@ -38,9 +24,7 @@ internal fun EarnItemPlaceholder(modifier: Modifier = Modifier) {
),
verticalAlignment = Alignment.CenterVertically,
) {
CircleShimmer(
modifier = Modifier.size(36.dp),
)
CircleShimmer(modifier = Modifier.size(36.dp))
SpacerW(12.dp)
@ -91,18 +75,86 @@ internal fun EarnItemPlaceholder(modifier: Modifier = Modifier) {
}
}
@Composable
internal fun EarnItemPlaceholderV2(modifier: Modifier = Modifier) {
Row(
modifier = modifier
.fillMaxWidth()
.padding(12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
CircleShimmer(modifier = Modifier.size(40.dp))
SpacerW(4.dp)
Column(modifier = Modifier.weight(1f)) {
Row(modifier = Modifier.fillMaxWidth()) {
RectangleShimmer(
modifier = Modifier
.width(96.dp)
.height(20.dp),
radius = TangemTheme.dimens2.x25,
)
SpacerWMax()
RectangleShimmer(
modifier = Modifier
.width(56.dp)
.height(20.dp),
radius = TangemTheme.dimens2.x25,
)
}
SpacerH(4.dp)
Row(modifier = Modifier.fillMaxWidth()) {
RectangleShimmer(
modifier = Modifier
.width(46.dp)
.height(16.dp),
radius = TangemTheme.dimens2.x25,
)
SpacerWMax()
RectangleShimmer(
modifier = Modifier
.width(46.dp)
.height(16.dp),
radius = TangemTheme.dimens2.x25,
)
}
}
}
}
private const val PLACEHOLDER_ITEMS_COUNT = 8
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun EarnListPlaceholderPreview() {
private fun EarnListPlaceholderPreviewV1() {
TangemThemePreview {
Box(
Column(
modifier = Modifier
.background(TangemTheme.colors.background.tertiary),
) {
EarnListPlaceholder()
repeat(PLACEHOLDER_ITEMS_COUNT) {
EarnItemPlaceholderV1()
}
}
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun EarnListPlaceholderPreviewV2() {
TangemThemePreviewRedesign {
Column(
modifier = Modifier
.background(TangemTheme.colors2.surface.level3),
) {
repeat(PLACEHOLDER_ITEMS_COUNT) {
EarnItemPlaceholderV2()
}
}
}
}

View file

@ -0,0 +1,115 @@
package com.tangem.features.feed.ui.earn.components
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerWMax
import com.tangem.core.ui.components.buttons.SecondarySmallButton
import com.tangem.core.ui.components.buttons.SmallButtonConfig
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.ds.button.PrimaryInverseTangemButton
import com.tangem.core.ui.ds.button.TangemButtonShape
import com.tangem.core.ui.ds.button.TangemButtonSize
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.features.feed.ui.earn.state.EarnFilterNetworkUM
import com.tangem.features.feed.ui.earn.state.EarnFilterUM
@Composable
internal fun FilterButtons(
earnFilterUM: EarnFilterUM,
onNetworkFilterClick: () -> Unit,
onTypeFilterClick: () -> Unit,
modifier: Modifier = Modifier,
) {
if (LocalRedesignEnabled.current) {
FilterButtonsV2(
earnFilterUM = earnFilterUM,
onNetworkFilterClick = onNetworkFilterClick,
onTypeFilterClick = onTypeFilterClick,
modifier = modifier,
)
} else {
FilterButtonsV1(
earnFilterUM = earnFilterUM,
onNetworkFilterClick = onNetworkFilterClick,
onTypeFilterClick = onTypeFilterClick,
modifier = modifier,
)
}
}
@Composable
private fun FilterButtonsV1(
earnFilterUM: EarnFilterUM,
onNetworkFilterClick: () -> Unit,
onTypeFilterClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier.padding(horizontal = 16.dp),
) {
SecondarySmallButton(
config = SmallButtonConfig(
text = when (earnFilterUM.selectedNetworkFilter) {
is EarnFilterNetworkUM.AllNetworks -> TextReference.Res(R.string.earn_filter_all_networks)
is EarnFilterNetworkUM.MyNetworks -> TextReference.Res(R.string.earn_filter_my_networks)
is EarnFilterNetworkUM.Network -> TextReference.Str(earnFilterUM.selectedNetworkFilter.text)
},
onClick = onNetworkFilterClick,
icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_chevron_24),
isEnabled = earnFilterUM.isNetworkFilterEnabled,
),
)
SpacerWMax()
SecondarySmallButton(
config = SmallButtonConfig(
text = earnFilterUM.selectedTypeFilter.text,
onClick = onTypeFilterClick,
icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_chevron_24),
isEnabled = earnFilterUM.isTypeFilterEnabled,
),
)
}
}
@Composable
private fun FilterButtonsV2(
earnFilterUM: EarnFilterUM,
onNetworkFilterClick: () -> Unit,
onTypeFilterClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Row(modifier = modifier.padding(horizontal = 16.dp, vertical = 4.dp)) {
PrimaryInverseTangemButton(
text = when (earnFilterUM.selectedNetworkFilter) {
is EarnFilterNetworkUM.AllNetworks -> TextReference.Res(R.string.earn_filter_all_networks)
is EarnFilterNetworkUM.MyNetworks -> TextReference.Res(R.string.earn_filter_my_networks)
is EarnFilterNetworkUM.Network -> TextReference.Str(earnFilterUM.selectedNetworkFilter.text)
},
onClick = onNetworkFilterClick,
iconRes = R.drawable.ic_chewron_down_20,
iconPosition = com.tangem.core.ui.ds.button.TangemButtonIconPosition.End,
size = TangemButtonSize.X9,
shape = TangemButtonShape.Rounded,
isEnabled = earnFilterUM.isNetworkFilterEnabled,
)
SpacerWMax()
PrimaryInverseTangemButton(
text = earnFilterUM.selectedTypeFilter.text,
onClick = onTypeFilterClick,
iconRes = R.drawable.ic_chewron_down_20,
iconPosition = com.tangem.core.ui.ds.button.TangemButtonIconPosition.End,
size = TangemButtonSize.X9,
shape = TangemButtonShape.Rounded,
isEnabled = earnFilterUM.isTypeFilterEnabled,
)
}
}

View file

@ -44,7 +44,7 @@ internal fun MostlyUsedCard(item: EarnListItemUM, onClick: () -> Unit, modifier:
private fun MostlyUsedCardV2(item: EarnListItemUM, onClick: () -> Unit, modifier: Modifier = Modifier) {
OpportunitiesBG(
modifier = modifier
.width(148.dp)
.width(178.dp)
.clip(TangemTheme.shapes.roundedCornersXMedium)
.clickable(onClick = onClick),
icon = TangemIconUM.Currency(item.currencyIconState),

View file

@ -2,14 +2,9 @@ package com.tangem.features.feed.ui.earn.components
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
@ -17,27 +12,36 @@ import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.CircleShimmer
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.SpacerH
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
@Composable
internal fun MostlyUsedPlaceholder(modifier: Modifier = Modifier) {
internal fun MostlyUsedPlaceholder(
modifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues(
horizontal = 16.dp,
vertical = 12.dp,
),
) {
LazyRow(
modifier = modifier,
contentPadding = PaddingValues(
horizontal = 16.dp,
vertical = 12.dp,
),
contentPadding = contentPadding,
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
items(PLACEHOLDER_ITEMS_COUNT) {
MostlyUsedItemPlaceholder()
if (LocalRedesignEnabled.current) {
MostlyUsedItemPlaceholderV2()
} else {
MostlyUsedItemPlaceholderV1()
}
}
}
}
@Composable
fun MostlyUsedItemPlaceholder(modifier: Modifier = Modifier) {
private fun MostlyUsedItemPlaceholderV1(modifier: Modifier = Modifier) {
Column(
modifier = modifier
.width(148.dp)
@ -69,13 +73,52 @@ fun MostlyUsedItemPlaceholder(modifier: Modifier = Modifier) {
}
}
@Composable
private fun MostlyUsedItemPlaceholderV2(modifier: Modifier = Modifier) {
Column(
modifier = modifier
.width(178.dp)
.height(130.dp)
.background(
color = TangemTheme.colors2.surface.level3,
shape = RoundedCornerShape(TangemTheme.dimens2.x4),
)
.padding(12.dp),
) {
CircleShimmer(modifier = Modifier.size(40.dp))
SpacerH(22.dp)
RectangleShimmer(
modifier = Modifier
.width(56.dp)
.height(20.dp),
radius = TangemTheme.dimens2.x25,
)
SpacerH(4.dp)
RectangleShimmer(
modifier = Modifier
.width(46.dp)
.height(16.dp),
radius = TangemTheme.dimens2.x25,
)
}
}
private const val PLACEHOLDER_ITEMS_COUNT = 3
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun MostlyUsedPlaceholderPreview() {
private fun MostlyUsedPlaceholderPreviewV1() {
TangemThemePreview {
MostlyUsedPlaceholder()
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun MostlyUsedPlaceholderPreviewV2() {
TangemThemePreviewRedesign {
MostlyUsedPlaceholder()
}
}

View file

@ -21,6 +21,7 @@ import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.SpacerW
@ -56,7 +57,11 @@ internal fun FeedListHeader(
}
@Composable
internal fun FeedList(state: FeedListUM, modifier: Modifier = Modifier) {
internal fun FeedList(
state: FeedListUM,
modifier: Modifier = Modifier,
promoBannersBlockComponent: ComposableContentComponent? = null,
) {
val background = LocalMainBottomSheetColor.current.value
AnimatedContent(
modifier = modifier,
@ -83,6 +88,7 @@ internal fun FeedList(state: FeedListUM, modifier: Modifier = Modifier) {
FeedListContent(
modifier = Modifier,
state = state,
promoBannersBlockComponent = promoBannersBlockComponent,
)
}
}
@ -123,7 +129,11 @@ private fun FeedSearchBar(
}
@Composable
private fun FeedListContent(state: FeedListUM, modifier: Modifier = Modifier) {
private fun FeedListContent(
state: FeedListUM,
modifier: Modifier = Modifier,
promoBannersBlockComponent: ComposableContentComponent? = null,
) {
val background = LocalMainBottomSheetColor.current.value
Column(
modifier = modifier
@ -140,6 +150,8 @@ private fun FeedListContent(state: FeedListUM, modifier: Modifier = Modifier) {
feedListCallbacks = state.feedListCallbacks,
)
promoBannersBlockComponent?.Content(modifier = Modifier.padding(horizontal = 16.dp))
NewsBlock(
news = state.news,
feedListCallbacks = state.feedListCallbacks,

View file

@ -2,10 +2,15 @@ package com.tangem.features.feed.ui.feed.components
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.clickable
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.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.RectangleShimmer
@ -13,6 +18,9 @@ import com.tangem.core.ui.components.SpacerW
import com.tangem.core.ui.components.buttons.SecondarySmallButton
import com.tangem.core.ui.components.buttons.SmallButtonConfig
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemTheme
@Composable
internal fun Header(
@ -21,6 +29,7 @@ internal fun Header(
shouldShowSeeAll: Boolean,
title: @Composable () -> Unit,
) {
val isRedesignEnabled = LocalRedesignEnabled.current
AnimatedContent(isLoading) { animatedState ->
Row(
modifier = Modifier
@ -30,19 +39,47 @@ internal fun Header(
verticalAlignment = Alignment.CenterVertically,
) {
if (animatedState) {
RectangleShimmer(modifier = Modifier.size(width = 104.dp, height = 18.dp))
if (isRedesignEnabled) {
RectangleShimmer(
modifier = Modifier.size(width = 130.dp, height = 24.dp),
radius = TangemTheme.dimens2.x25,
)
} else {
RectangleShimmer(modifier = Modifier.size(width = 104.dp, height = 18.dp))
}
} else {
Box(modifier = Modifier.weight(1f)) {
title()
}
SpacerW(8.dp)
AnimatedVisibility(shouldShowSeeAll) {
SecondarySmallButton(
config = SmallButtonConfig(
text = TextReference.Res(R.string.common_see_all),
onClick = onSeeAllClick,
),
)
if (isRedesignEnabled) {
Row(
modifier = Modifier
.padding(start = 8.dp)
.clickable(onClick = onSeeAllClick),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = stringResourceSafe(R.string.common_see_all),
color = TangemTheme.colors2.text.neutral.primary,
style = TangemTheme.typography2.bodySemibold16,
)
Icon(
modifier = Modifier.size(24.dp),
imageVector = ImageVector.vectorResource(R.drawable.ic_chevron_small_right_24),
tint = TangemTheme.colors2.markers.iconGray,
contentDescription = null,
)
}
} else {
SecondarySmallButton(
config = SmallButtonConfig(
text = TextReference.Res(R.string.common_see_all),
onClick = onSeeAllClick,
),
)
}
}
}
}

View file

@ -1,9 +1,9 @@
package com.tangem.features.feed.ui.feed.components
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@ -16,9 +16,12 @@ import com.tangem.core.ui.components.UnableToLoadData
import com.tangem.core.ui.components.block.BlockCard
import com.tangem.core.ui.components.block.TangemBlockCardColors
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.feed.ui.earn.components.EarnItemPlaceholderV1
import com.tangem.features.feed.ui.earn.components.EarnListItem
import com.tangem.features.feed.ui.earn.components.EarnListPlaceholder
import com.tangem.features.feed.ui.earn.components.MostlyUsedCard
import com.tangem.features.feed.ui.earn.components.MostlyUsedPlaceholder
import com.tangem.features.feed.ui.earn.state.EarnListItemUM
import com.tangem.features.feed.ui.earn.state.EarnListUM
import kotlinx.collections.immutable.ImmutableList
@ -26,7 +29,15 @@ import kotlinx.collections.immutable.ImmutableList
@Composable
internal fun EarnBlock(onSeeAllClick: () -> Unit, earnListUM: EarnListUM, modifier: Modifier = Modifier) {
if (earnListUM is EarnListUM.Empty) return
if (LocalRedesignEnabled.current) {
EarnBlockV2(onSeeAllClick = onSeeAllClick, earnListUM = earnListUM, modifier = modifier)
} else {
EarnBlockV1(onSeeAllClick = onSeeAllClick, earnListUM = earnListUM, modifier = modifier)
}
}
@Composable
private fun EarnBlockV1(onSeeAllClick: () -> Unit, earnListUM: EarnListUM, modifier: Modifier = Modifier) {
Column(modifier = modifier) {
Header(
title = {
@ -56,7 +67,11 @@ internal fun EarnBlock(onSeeAllClick: () -> Unit, earnListUM: EarnListUM, modifi
when (earnListUM) {
is EarnListUM.Content -> EarnContentBlock(items = earnListUM.items)
is EarnListUM.Error -> EarnErrorBlock(onRetryClick = earnListUM.onRetryClicked)
EarnListUM.Loading -> EarnListPlaceholder(placeholderCount = PLACEHOLDER_ITEM_COUNT)
EarnListUM.Loading -> {
repeat(PLACEHOLDER_ITEM_COUNT) {
EarnItemPlaceholderV1()
}
}
EarnListUM.Empty -> Unit
}
}
@ -65,6 +80,55 @@ internal fun EarnBlock(onSeeAllClick: () -> Unit, earnListUM: EarnListUM, modifi
}
}
@Composable
private fun EarnBlockV2(onSeeAllClick: () -> Unit, earnListUM: EarnListUM, modifier: Modifier = Modifier) {
Column(modifier = modifier) {
Header(
title = {
Text(
text = stringResourceSafe(R.string.markets_earn_common_title),
style = TangemTheme.typography2.headingSemibold20,
color = TangemTheme.colors2.text.neutral.primary,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
},
onSeeAllClick = onSeeAllClick,
isLoading = earnListUM is EarnListUM.Loading,
shouldShowSeeAll = earnListUM is EarnListUM.Content,
)
SpacerH(12.dp)
AnimatedContent(
targetState = earnListUM,
contentKey = { it::class.java },
) { earnListUM ->
when (earnListUM) {
is EarnListUM.Content -> EarnContentBlock(items = earnListUM.items)
is EarnListUM.Error -> {
BlockCard(
modifier = Modifier.padding(horizontal = 16.dp),
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors2.surface.level3),
) {
EarnErrorBlock(onRetryClick = earnListUM.onRetryClicked)
}
}
EarnListUM.Loading -> {
MostlyUsedPlaceholder(
contentPadding = PaddingValues(
start = 16.dp,
end = 16.dp,
bottom = 8.dp,
),
)
}
EarnListUM.Empty -> Unit
}
}
SpacerH(32.dp)
}
}
@Composable
private fun EarnErrorBlock(onRetryClick: () -> Unit) {
UnableToLoadData(
@ -77,9 +141,30 @@ private fun EarnErrorBlock(onRetryClick: () -> Unit) {
@Composable
private fun EarnContentBlock(items: ImmutableList<EarnListItemUM>) {
Column(modifier = Modifier.fillMaxWidth()) {
items.fastForEach { item ->
EarnListItem(item = item)
if (LocalRedesignEnabled.current) {
LazyRow(
contentPadding = PaddingValues(
start = 16.dp,
end = 16.dp,
bottom = 8.dp,
),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
items(
items = items,
key = { item -> "${item.tokenName}-${item.network}" },
) { item ->
MostlyUsedCard(
item = item,
onClick = item.onItemClick,
)
}
}
} else {
Column(modifier = Modifier.fillMaxWidth()) {
items.fastForEach { item ->
EarnListItem(item = item)
}
}
}
}

View file

@ -9,14 +9,15 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.markets.MarketsListItemPlaceholder
import com.tangem.features.feed.ui.feed.components.articles.DefaultLoadingArticle
import com.tangem.features.feed.ui.feed.components.articles.TrendingLoadingArticle
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.block.BlockCard
import com.tangem.core.ui.components.block.TangemBlockCardColors
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.feed.ui.feed.components.articles.DefaultLoadingArticle
import com.tangem.features.feed.ui.feed.components.articles.TrendingLoadingArticle
@Composable
internal fun FeedListLoading(modifier: Modifier = Modifier) {
@ -35,70 +36,142 @@ internal fun FeedListLoading(modifier: Modifier = Modifier) {
@Composable
internal fun MarketLoadingBlock() {
RectangleShimmer(
modifier = Modifier
.padding(start = 16.dp)
.size(width = 104.dp, height = 18.dp),
)
SpacerH(15.dp)
ChartsLoading(modifier = Modifier.padding(horizontal = 16.dp))
SpacerH(35.dp)
}
@Composable
internal fun MarketPulseLoadingBlock() {
RectangleShimmer(
modifier = Modifier
.padding(start = 16.dp)
.size(width = 104.dp, height = 18.dp),
)
SpacerH(15.dp)
LazyRow(
modifier = Modifier.padding(vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically,
contentPadding = PaddingValues(16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
state = rememberLazyListState(),
) {
items(DEFAULT_CHART_SIZE_IN_MARKET) {
RectangleShimmer(modifier = Modifier.size(width = 124.dp, height = 36.dp))
}
}
SpacerH(16.dp)
ChartsLoading(modifier = Modifier.padding(horizontal = 16.dp))
SpacerH(32.dp)
}
@Composable
internal fun NewsLoadingBlock() {
Column {
if (LocalRedesignEnabled.current) {
RectangleShimmer(
modifier = Modifier
.padding(start = 16.dp, top = 16.dp)
.size(width = 132.dp, height = 24.dp),
radius = TangemTheme.dimens2.x25,
)
SpacerH(20.dp)
ChartsLoading(modifier = Modifier.padding(horizontal = 16.dp))
SpacerH(40.dp)
} else {
RectangleShimmer(
modifier = Modifier
.padding(start = 16.dp)
.size(width = 104.dp, height = 18.dp),
)
SpacerH(15.dp)
TrendingLoadingArticle(modifier = Modifier.padding(horizontal = 16.dp))
SpacerH(12.dp)
LazyRow(
verticalAlignment = Alignment.CenterVertically,
contentPadding = PaddingValues(16.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
state = rememberLazyListState(),
) {
items(DEFAULT_CHART_SIZE_IN_MARKET) {
DefaultLoadingArticle()
}
}
ChartsLoading(modifier = Modifier.padding(horizontal = 16.dp))
SpacerH(35.dp)
}
}
@Composable
internal fun MarketPulseLoadingBlock() {
if (LocalRedesignEnabled.current) {
RectangleShimmer(
modifier = Modifier
.padding(start = 16.dp, top = 16.dp)
.size(width = 132.dp, height = 24.dp),
radius = TangemTheme.dimens2.x25,
)
SpacerH(15.dp)
LazyRow(
modifier = Modifier.padding(vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically,
contentPadding = PaddingValues(16.dp),
horizontalArrangement = Arrangement.spacedBy(6.dp),
state = rememberLazyListState(),
) {
items(DEFAULT_CHART_SIZE_IN_MARKET) {
RectangleShimmer(
modifier = Modifier.size(width = 112.dp, height = 36.dp),
radius = TangemTheme.dimens2.x25,
)
}
}
SpacerH(16.dp)
ChartsLoading(modifier = Modifier.padding(horizontal = 16.dp))
SpacerH(32.dp)
} else {
RectangleShimmer(
modifier = Modifier
.padding(start = 16.dp)
.size(width = 104.dp, height = 18.dp),
)
SpacerH(15.dp)
LazyRow(
modifier = Modifier.padding(vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically,
contentPadding = PaddingValues(16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
state = rememberLazyListState(),
) {
items(DEFAULT_CHART_SIZE_IN_MARKET) {
RectangleShimmer(modifier = Modifier.size(width = 124.dp, height = 36.dp))
}
}
SpacerH(16.dp)
ChartsLoading(modifier = Modifier.padding(horizontal = 16.dp))
SpacerH(32.dp)
}
}
@Composable
internal fun NewsLoadingBlock() {
if (LocalRedesignEnabled.current) {
Column {
RectangleShimmer(
modifier = Modifier
.padding(start = 16.dp, top = 16.dp)
.size(width = 132.dp, height = 24.dp),
radius = TangemTheme.dimens2.x25,
)
SpacerH(20.dp)
TrendingLoadingArticle(modifier = Modifier.padding(horizontal = 16.dp))
SpacerH(12.dp)
LazyRow(
verticalAlignment = Alignment.CenterVertically,
contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
state = rememberLazyListState(),
) {
items(DEFAULT_CHART_SIZE_IN_MARKET) {
DefaultLoadingArticle()
}
}
SpacerH(40.dp)
}
} else {
Column {
RectangleShimmer(
modifier = Modifier
.padding(start = 16.dp)
.size(width = 104.dp, height = 18.dp),
)
SpacerH(15.dp)
TrendingLoadingArticle(modifier = Modifier.padding(horizontal = 16.dp))
SpacerH(12.dp)
LazyRow(
verticalAlignment = Alignment.CenterVertically,
contentPadding = PaddingValues(16.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
state = rememberLazyListState(),
) {
items(DEFAULT_CHART_SIZE_IN_MARKET) {
DefaultLoadingArticle()
}
}
SpacerH(35.dp)
}
}
}
@Composable
private fun ChartsLoading(modifier: Modifier = Modifier) {
val isRedesignEnabled = LocalRedesignEnabled.current
BlockCard(
modifier = modifier,
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
colors = TangemBlockCardColors.copy(
containerColor = if (isRedesignEnabled) {
TangemTheme.colors2.surface.level3
} else {
TangemTheme.colors.background.action
},
),
) {
Column(
modifier = Modifier

View file

@ -25,7 +25,9 @@ import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.UnableToLoadData
import com.tangem.core.ui.components.block.BlockCard
import com.tangem.core.ui.components.block.TangemBlockCardColors
import com.tangem.core.ui.ds.tabs.TangemTab
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
import com.tangem.features.feed.ui.feed.state.FeedListCallbacks
@ -34,6 +36,7 @@ import com.tangem.features.feed.ui.feed.state.MarketChartUM
@Composable
internal fun MarketBlock(marketChart: MarketChartUM?, feedListCallbacks: FeedListCallbacks) {
val isRedesignEnabled = LocalRedesignEnabled.current
AnimatedContent(
targetState = marketChart,
label = "MarketBlockChartAnimation",
@ -49,8 +52,16 @@ internal fun MarketBlock(marketChart: MarketChartUM?, feedListCallbacks: FeedLis
title = {
Text(
text = stringResourceSafe(R.string.markets_common_title),
style = TangemTheme.typography.h3,
color = TangemTheme.colors.text.primary1,
style = if (isRedesignEnabled) {
TangemTheme.typography2.headingSemibold20
} else {
TangemTheme.typography.h3
},
color = if (isRedesignEnabled) {
TangemTheme.colors2.text.neutral.primary
} else {
TangemTheme.colors.text.primary1
},
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
@ -76,8 +87,10 @@ internal fun MarketBlock(marketChart: MarketChartUM?, feedListCallbacks: FeedLis
}
}
@Suppress("LongMethod")
@Composable
internal fun MarketPulseBlock(marketChartConfig: MarketChartConfig, feedListCallbacks: FeedListCallbacks) {
val isRedesignEnabled = LocalRedesignEnabled.current
val onSeeAllClick by rememberUpdatedState {
feedListCallbacks.onMarketOpenClick(marketChartConfig.currentSortByType)
}
@ -86,8 +99,16 @@ internal fun MarketPulseBlock(marketChartConfig: MarketChartConfig, feedListCall
title = {
Text(
text = stringResourceSafe(R.string.markets_pulse_common_title),
style = TangemTheme.typography.h3,
color = TangemTheme.colors.text.primary1,
style = if (isRedesignEnabled) {
TangemTheme.typography2.headingSemibold20
} else {
TangemTheme.typography.h3
},
color = if (isRedesignEnabled) {
TangemTheme.colors2.text.neutral.primary
} else {
TangemTheme.colors.text.primary1
},
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
@ -98,9 +119,13 @@ internal fun MarketPulseBlock(marketChartConfig: MarketChartConfig, feedListCall
)
LazyRow(
modifier = Modifier.padding(vertical = 4.dp),
modifier = Modifier.padding(vertical = if (isRedesignEnabled) 12.dp else 4.dp),
verticalAlignment = Alignment.CenterVertically,
contentPadding = PaddingValues(16.dp),
contentPadding = if (isRedesignEnabled) {
PaddingValues(horizontal = 16.dp, vertical = 6.dp)
} else {
PaddingValues(16.dp)
},
horizontalArrangement = Arrangement.spacedBy(8.dp),
state = rememberLazyListState(),
) {
@ -108,16 +133,22 @@ internal fun MarketPulseBlock(marketChartConfig: MarketChartConfig, feedListCall
items = marketChartConfig.getFilterPreset(),
key = SortByTypeUM::name,
) { sortByTypeUM ->
ChartsFilterChip(
sortByTypeUM = sortByTypeUM,
isSelected = sortByTypeUM == marketChartConfig.currentSortByType,
onClick = { feedListCallbacks.onSortTypeClick(sortByTypeUM) },
)
if (isRedesignEnabled) {
TangemTab(
text = sortByTypeUM.text,
isChecked = sortByTypeUM == marketChartConfig.currentSortByType,
onCheckedChange = { feedListCallbacks.onSortTypeClick(sortByTypeUM) },
)
} else {
ChartsFilterChip(
sortByTypeUM = sortByTypeUM,
isSelected = sortByTypeUM == marketChartConfig.currentSortByType,
onClick = { feedListCallbacks.onSortTypeClick(sortByTypeUM) },
)
}
}
}
SpacerH(12.dp)
AnimatedContent(
targetState = marketChartConfig.currentSortByType,
label = "MarketPulseChartAnimation",
@ -141,9 +172,16 @@ private fun Charts(
onItemClick: (MarketsListItemUM) -> Unit,
modifier: Modifier = Modifier,
) {
val isRedesignEnabled = LocalRedesignEnabled.current
BlockCard(
modifier = modifier,
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
colors = TangemBlockCardColors.copy(
containerColor = if (isRedesignEnabled) {
TangemTheme.colors2.surface.level3
} else {
TangemTheme.colors.background.action
},
),
) {
Column(modifier = Modifier.fillMaxWidth()) {
when (marketChart) {

View file

@ -234,7 +234,7 @@ private fun TrendingArticleBackground(
Color(0xFF7C16F1).copy(alpha = .8f),
Color.Transparent,
),
center = Offset(w / 2f, 2.4f * h),
center = Offset(w / 2f, 2.6f * h),
radius = radiusScale * 1.57f,
),
)

View file

@ -154,7 +154,10 @@ private fun ArticleHeaderV2(
text = tag.text,
tangemIconUM = when (val content = tag.leadingContent) {
LabelLeadingContentUM.None -> null
is LabelLeadingContentUM.Token -> TangemIconUM.Url(content.iconUrl)
is LabelLeadingContentUM.Token -> TangemIconUM.Url(
url = content.iconUrl,
fallbackRes = R.drawable.ic_alert_24,
)
},
shape = TangemBadgeShape.Rounded,
size = TangemBadgeSize.X9,

View file

@ -7,6 +7,7 @@ import androidx.compose.ui.layout.SubcomposeLayout
import androidx.compose.ui.layout.SubcomposeMeasureScope
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.label.Label
import com.tangem.core.ui.components.label.entity.LabelLeadingContentUM
import com.tangem.core.ui.components.label.entity.LabelUM
@ -40,7 +41,10 @@ internal fun Tags(tags: ImmutableList<LabelUM>, modifier: Modifier = Modifier) {
text = tag.text,
tangemIconUM = when (val content = tag.leadingContent) {
LabelLeadingContentUM.None -> null
is LabelLeadingContentUM.Token -> TangemIconUM.Url(content.iconUrl)
is LabelLeadingContentUM.Token -> TangemIconUM.Url(
url = content.iconUrl,
fallbackRes = R.drawable.ic_alert_24,
)
},
shape = TangemBadgeShape.Rounded,
size = TangemBadgeSize.X6,

View file

@ -37,6 +37,7 @@ import com.tangem.core.ui.event.StateEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.domain.markets.PriceChangeInterval
@ -81,6 +82,7 @@ private fun Content(
modifier: Modifier = Modifier,
portfolioBlock: @Composable ((Modifier) -> Unit)?,
) {
val isRedesignEnabled = LocalRedesignEnabled.current
val density = LocalDensity.current
val bottomBarHeight = with(density) { WindowInsets.systemBars.getBottom(this).toDp() }
val lazyListState = rememberLazyListState()
@ -133,6 +135,7 @@ private fun Content(
state = state.body,
portfolioBlock = portfolioBlock,
relatedNews = state.relatedNews,
isRedesignEnabled = isRedesignEnabled,
)
}
}

View file

@ -2,28 +2,31 @@ package com.tangem.features.feed.ui.market.detailed.components
import android.content.res.Configuration
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.CircleShape
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.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.vectorResource
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.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.CircleShimmer
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.SpacerH12
import com.tangem.core.ui.components.appbar.TangemTopAppBar
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
@ -35,16 +38,22 @@ import com.tangem.core.ui.components.buttons.SmallButtonConfig
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.token.TokenItem
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
import com.tangem.core.ui.ds.button.*
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.ds.topbar.TangemTopBar
import com.tangem.core.ui.ds.topbar.TangemTopBarType
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.features.feed.impl.R
import com.tangem.features.feed.ui.market.detailed.state.ExchangeItemUM
import com.tangem.features.feed.ui.market.detailed.state.ExchangesBottomSheetContent
import kotlinx.collections.immutable.toImmutableList
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet as TangemBottomSheetV2
/**
* Exchanges bottom sheet
@ -55,6 +64,15 @@ import kotlinx.collections.immutable.toImmutableList
*/
@Composable
internal fun ExchangesBottomSheet(config: TangemBottomSheetConfig) {
if (LocalRedesignEnabled.current) {
ExchangesBottomSheetV2(config)
} else {
ExchangesBottomSheetV1(config)
}
}
@Composable
private fun ExchangesBottomSheetV1(config: TangemBottomSheetConfig) {
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
TangemBottomSheet<ExchangesBottomSheetContent>(
@ -98,6 +116,77 @@ internal fun ExchangesBottomSheet(config: TangemBottomSheetConfig) {
)
}
@Composable
private fun ExchangesBottomSheetV2(config: TangemBottomSheetConfig) {
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
TangemBottomSheetV2<ExchangesBottomSheetContent>(
config = config,
title = { content ->
TangemTopBar(
title = resourceReference(content.titleResId),
type = TangemTopBarType.BottomSheet,
startContent = {
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.ic_arrow_back_28),
contentDescription = null,
tint = TangemTheme.colors2.graphic.neutral.primary,
modifier = Modifier
.size(TangemTheme.dimens2.x11)
.background(
color = TangemTheme.colors2.button.backgroundSecondary,
shape = CircleShape,
)
.clickableSingle(onClick = config.onDismissRequest)
.padding(TangemTheme.dimens2.x2),
)
},
)
},
content = { content ->
Box {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(bottom = bottomBarHeight),
) {
item(key = "subtitle") {
Subtitle(
subtitleRes = content.subtitleResId,
volumeReference = content.volumeReference,
modifier = Modifier.padding(horizontal = TangemTheme.dimens2.x7),
)
}
if (content.exchangeItemsV2.isNotEmpty()) {
val lastIndex = content.exchangeItemsV2.lastIndex
itemsIndexed(
items = content.exchangeItemsV2,
key = { _, item -> item.id },
) { index, item ->
ExchangeItemRow(
exchangeItemUM = item,
modifier = Modifier
.roundedShapeItemDecoration(
currentIndex = index,
lastIndex = lastIndex,
backgroundColor = TangemTheme.colors2.surface.level3,
),
)
}
}
}
if (content is ExchangesBottomSheetContent.Error) {
Error(
content = content,
modifier = Modifier.align(Alignment.Center),
)
}
}
},
)
}
@Composable
private fun Title(@StringRes textResId: Int, onBackClick: () -> Unit) {
TangemTopAppBar(
@ -120,17 +209,36 @@ private fun Subtitle(@StringRes subtitleRes: Int, volumeReference: TextReference
@Composable
private fun SubtitleText(textReference: TextReference) {
Text(
text = textReference.resolveReference(),
color = TangemTheme.colors.text.tertiary,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
style = TangemTheme.typography.body2,
)
if (LocalRedesignEnabled.current) {
Text(
text = textReference.resolveReference(),
color = TangemTheme.colors2.text.neutral.secondary,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
style = TangemTheme.typography2.captionSemibold12,
)
} else {
Text(
text = textReference.resolveReference(),
color = TangemTheme.colors.text.tertiary,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
style = TangemTheme.typography.body2,
)
}
}
@Composable
private fun Error(content: ExchangesBottomSheetContent.Error, modifier: Modifier = Modifier) {
if (LocalRedesignEnabled.current) {
ErrorV2(content, modifier)
} else {
ErrorV1(content, modifier)
}
}
@Composable
private fun ErrorV1(content: ExchangesBottomSheetContent.Error, modifier: Modifier = Modifier) {
Column(
modifier = modifier,
verticalArrangement = Arrangement.Center,
@ -154,6 +262,155 @@ private fun Error(content: ExchangesBottomSheetContent.Error, modifier: Modifier
}
}
@Composable
private fun ErrorV2(content: ExchangesBottomSheetContent.Error, modifier: Modifier = Modifier) {
Column(
modifier = modifier,
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = stringResourceSafe(id = content.message),
color = TangemTheme.colors2.text.neutral.tertiary,
textAlign = TextAlign.Center,
style = TangemTheme.typography2.bodyRegular14,
)
SpacerH(8.dp)
TangemButton(
buttonUM = TangemButtonUM(
text = resourceReference(com.tangem.core.ui.R.string.try_to_load_data_again_button_title),
onClick = content.onRetryClick,
type = TangemButtonType.Secondary,
size = TangemButtonSize.X8,
shape = TangemButtonShape.Rounded,
),
)
}
}
@Composable
private fun ExchangeItemRow(exchangeItemUM: ExchangeItemUM, modifier: Modifier = Modifier) {
when (exchangeItemUM) {
is ExchangeItemUM.Content -> {
ExchangeItemRowContent(exchangeItemUM, modifier)
}
is ExchangeItemUM.Loading -> {
ExchangeItemRowPlaceholder(modifier)
}
}
}
@Composable
private fun ExchangeItemRowContent(exchangeItemUM: ExchangeItemUM.Content, modifier: Modifier = Modifier) {
TangemRowContainer(
modifier = modifier,
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 14.dp),
) {
TangemIcon(
tangemIconUM = exchangeItemUM.icon,
modifier = Modifier
.layoutId(layoutId = TangemRowLayoutId.HEAD)
.size(TangemTheme.dimens2.x10),
)
Text(
modifier = Modifier
.padding(start = TangemTheme.dimens2.x2)
.layoutId(TangemRowLayoutId.START_TOP),
text = exchangeItemUM.title.resolveReference(),
style = TangemTheme.typography2.bodySemibold16,
color = TangemTheme.colors2.text.neutral.primary,
)
Text(
modifier = Modifier
.padding(start = TangemTheme.dimens2.x2)
.layoutId(TangemRowLayoutId.START_BOTTOM),
text = exchangeItemUM.subTitle.resolveReference(),
style = TangemTheme.typography2.captionSemibold12,
color = TangemTheme.colors2.text.neutral.secondary,
)
Text(
modifier = Modifier
.padding(start = TangemTheme.dimens2.x2)
.layoutId(TangemRowLayoutId.END_TOP),
text = exchangeItemUM.volumeInUsd.resolveReference(),
style = TangemTheme.typography2.bodySemibold16,
color = TangemTheme.colors2.text.neutral.primary,
)
Text(
modifier = Modifier
.background(
color = getColorByTrustValue(exchangeItemUM.auditLabel.type).copy(alpha = .1f),
shape = CircleShape,
)
.padding(vertical = 2.dp, horizontal = 6.dp)
.layoutId(TangemRowLayoutId.END_BOTTOM),
text = exchangeItemUM.auditLabel.text.resolveReference(),
style = TangemTheme.typography2.captionSemibold11,
color = getColorByTrustValue(exchangeItemUM.auditLabel.type),
)
}
}
@Composable
private fun ExchangeItemRowPlaceholder(modifier: Modifier = Modifier) {
TangemRowContainer(
modifier = modifier,
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 14.dp),
) {
CircleShimmer(
modifier = Modifier
.size(40.dp)
.layoutId(TangemRowLayoutId.HEAD),
)
RectangleShimmer(
modifier = Modifier
.width(108.dp)
.height(20.dp)
.padding(start = 4.dp)
.layoutId(TangemRowLayoutId.START_TOP),
)
RectangleShimmer(
modifier = Modifier
.width(52.dp)
.height(16.dp)
.padding(start = 4.dp)
.layoutId(TangemRowLayoutId.START_BOTTOM),
)
RectangleShimmer(
modifier = Modifier
.width(106.dp)
.height(20.dp)
.padding(start = 4.dp)
.layoutId(TangemRowLayoutId.END_TOP),
)
RectangleShimmer(
modifier = Modifier
.width(52.dp)
.height(16.dp)
.padding(start = 4.dp)
.layoutId(TangemRowLayoutId.END_BOTTOM),
)
}
}
@Composable
private fun getColorByTrustValue(type: AuditLabelUM.Type): Color {
return when (type) {
AuditLabelUM.Type.Prohibition -> TangemTheme.colors2.fill.status.attention
AuditLabelUM.Type.Warning -> TangemTheme.colors2.fill.status.warning
AuditLabelUM.Type.Permit,
AuditLabelUM.Type.Info,
AuditLabelUM.Type.General,
-> TangemTheme.colors2.fill.status.accent
}
}
@Preview
@Preview(name = "Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
@ -175,7 +432,7 @@ private class ExchangesBottomSheetContentProvider : CollectionPreviewParameterPr
listOf(
ExchangesBottomSheetContent.Loading(exchangesCount = 13),
ExchangesBottomSheetContent.Error(onRetryClick = {}),
ExchangesBottomSheetContent.Content(
ExchangesBottomSheetContent.ContentV1(
exchangeItems = List(size = 13) { index ->
TokenItemState.Content(
id = index.toString(),

View file

@ -1,30 +1,52 @@
package com.tangem.features.feed.ui.market.detailed.components
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.vectorResource
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetType
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheetTitle
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.ds.row.TangemRowContainer
import com.tangem.core.ui.ds.row.TangemRowLayoutId
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.resolveReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.feed.impl.R
import com.tangem.features.feed.ui.market.detailed.state.InfoBottomSheetContent
import dev.jeziellago.compose.markdowntext.MarkdownText
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet as TangemBottomSheetV2
@Composable
internal fun InfoBottomSheet(config: TangemBottomSheetConfig) {
if (LocalRedesignEnabled.current) {
InfoBottomSheetV2(config)
} else {
InfoBottomSheetV1(config)
}
}
@Composable
private fun InfoBottomSheetV1(config: TangemBottomSheetConfig) {
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
TangemBottomSheet<InfoBottomSheetContent>(
@ -62,6 +84,89 @@ internal fun InfoBottomSheet(config: TangemBottomSheetConfig) {
)
}
@Suppress("LongMethod")
@Composable
private fun InfoBottomSheetV2(config: TangemBottomSheetConfig) {
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
TangemBottomSheetV2<InfoBottomSheetContent>(
config = config,
type = TangemBottomSheetType.Modal,
containerColor = TangemTheme.colors2.surface.level3,
title = { content ->
TangemTopBar(
title = content.title,
type = TangemTopBarType.BottomSheet,
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 = config.onDismissRequest)
.padding(TangemTheme.dimens2.x2_5),
)
},
)
},
content = { content ->
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.padding(horizontal = TangemTheme.dimens2.x4),
) {
MarkdownText(
modifier = Modifier.padding(bottom = TangemTheme.dimens2.x3),
markdown = content.body.resolveReference(),
disableLinkMovementMethod = true,
linkifyMask = 0,
syntaxHighlightColor = TangemTheme.colors2.text.neutral.secondary,
style = TangemTheme.typography2.bodyRegular16.copy(
TangemTheme.colors2.text.neutral.secondary,
),
)
if (content.generatedAINotificationUM != null) {
TangemRowContainer(
modifier = Modifier
.background(
color = TangemTheme.colors2.surface.level4,
shape = RoundedCornerShape(TangemTheme.dimens2.x5),
)
.clickableSingle(onClick = content.generatedAINotificationUM.onClick),
contentPadding = PaddingValues(TangemTheme.dimens2.x3),
) {
Icon(
modifier = Modifier
.size(TangemTheme.dimens2.x10)
.padding(TangemTheme.dimens2.x1)
.layoutId(TangemRowLayoutId.HEAD),
tint = TangemTheme.colors2.markers.iconBlue,
contentDescription = null,
imageVector = ImageVector.vectorResource(R.drawable.ic_magic_28),
)
Text(
modifier = Modifier
.padding(start = TangemTheme.dimens2.x1)
.layoutId(TangemRowLayoutId.START_TOP),
text = stringResourceSafe(R.string.information_generated_with_ai),
style = TangemTheme.typography2.captionSemibold12,
color = TangemTheme.colors2.text.neutral.primary,
)
}
}
SpacerH(bottomBarHeight)
}
},
)
}
@Composable
private fun AdditionalInfoNotification(onClick: () -> Unit, modifier: Modifier = Modifier) {
Notification(

View file

@ -2,17 +2,11 @@ package com.tangem.features.feed.ui.market.detailed.components
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.*
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.vector.ImageVector
@ -21,18 +15,30 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
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.SpacerW4
import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.components.text.TooltipText
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
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.core.ui.utils.PreviewShimmerContainer
import com.tangem.features.feed.ui.market.detailed.state.InfoPointUM
@Composable
internal fun InfoPoint(infoPointUM: InfoPointUM, modifier: Modifier = Modifier) {
if (LocalRedesignEnabled.current) {
InfoPointV2(infoPointUM, modifier)
} else {
InfoPointV1(infoPointUM, modifier)
}
}
@Composable
private fun InfoPointV1(infoPointUM: InfoPointUM, modifier: Modifier = Modifier) {
Column(
modifier = modifier.padding(vertical = TangemTheme.dimens.spacing8),
horizontalAlignment = Alignment.Start,
@ -81,8 +87,68 @@ internal fun InfoPoint(infoPointUM: InfoPointUM, modifier: Modifier = Modifier)
}
}
@Composable
private fun InfoPointV2(infoPointUM: InfoPointUM, modifier: Modifier = Modifier) {
Column(
modifier = modifier.padding(vertical = TangemTheme.dimens.spacing8),
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
Row {
Text(
text = infoPointUM.value,
style = TangemTheme.typography2.headingSemibold20,
color = TangemTheme.colors2.text.neutral.primary,
)
if (infoPointUM.change != null) {
SpacerW4()
Icon(
modifier = Modifier
.size(TangemTheme.dimens2.x2)
.align(Alignment.CenterVertically),
imageVector = ImageVector.vectorResource(
id = when (infoPointUM.change) {
InfoPointUM.ChangeType.UP -> R.drawable.ic_arrow_up_8
InfoPointUM.ChangeType.DOWN -> R.drawable.ic_arrow_down_8
},
),
tint = when (infoPointUM.change) {
InfoPointUM.ChangeType.UP -> TangemTheme.colors2.markers.iconGreen
InfoPointUM.ChangeType.DOWN -> TangemTheme.colors2.markers.iconRed
},
contentDescription = null,
)
}
}
if (infoPointUM.onInfoClick != null) {
InformationTextBlock(
text = infoPointUM.title,
onInfoClick = infoPointUM.onInfoClick,
informationTextBlockIconPosition = InformationTextBlockIconPosition.START,
)
} else {
Text(
text = infoPointUM.title.resolveReference(),
style = TangemTheme.typography2.captionSemibold12,
color = TangemTheme.colors2.text.neutral.tertiary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
}
@Composable
internal fun InfoPointShimmer(modifier: Modifier = Modifier, withTooltip: Boolean = false) {
if (LocalRedesignEnabled.current) {
InfoPointShimmerV2(modifier)
} else {
InfoPointShimmerV1(modifier, withTooltip)
}
}
@Composable
private fun InfoPointShimmerV1(modifier: Modifier = Modifier, withTooltip: Boolean = false) {
Column(
modifier = modifier.padding(vertical = TangemTheme.dimens.spacing8),
horizontalAlignment = Alignment.Start,
@ -115,10 +181,34 @@ internal fun InfoPointShimmer(modifier: Modifier = Modifier, withTooltip: Boolea
}
}
@Composable
private fun InfoPointShimmerV2(modifier: Modifier = Modifier) {
Column(
modifier = modifier.padding(vertical = TangemTheme.dimens2.x6),
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1),
) {
RectangleShimmer(
modifier = Modifier
.fillMaxWidth()
.height(24.dp)
.padding(end = 10.dp),
radius = TangemTheme.dimens2.x25,
)
RectangleShimmer(
modifier = Modifier
.width(68.dp)
.height(16.dp),
radius = TangemTheme.dimens2.x25,
)
}
}
@Preview
@Preview("Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun ContentPreview() {
private fun ContentPreviewV1() {
TangemThemePreview {
Column(
modifier = Modifier
@ -158,6 +248,53 @@ private fun ContentPreview() {
}
}
@Preview
@Preview("Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun ContentPreviewV2() {
CompositionLocalProvider(
LocalRedesignEnabled provides true,
) {
TangemThemePreviewRedesign {
Column(
modifier = Modifier
.width(150.dp)
.background(TangemTheme.colors.background.tertiary),
) {
InfoPoint(
infoPointUM = InfoPointUM(
title = stringReference("Market Cap"),
value = "$1,000,000,000",
),
)
InfoPoint(
infoPointUM = InfoPointUM(
title = stringReference("Market Cap"),
value = "$1,000,000,000",
onInfoClick = { },
),
)
InfoPoint(
infoPointUM = InfoPointUM(
title = stringReference("Market Cap"),
value = "$1,000,000",
change = InfoPointUM.ChangeType.UP,
onInfoClick = { },
),
)
InfoPoint(
infoPointUM = InfoPointUM(
title = stringReference("Market Cap"),
value = "$1,000,000",
change = InfoPointUM.ChangeType.DOWN,
onInfoClick = { },
),
)
}
}
}
}
@Preview
@Preview("Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
@ -186,7 +323,7 @@ private fun PreviewShimmer() {
}
},
actualContent = {
ContentPreview()
ContentPreviewV1()
},
)
}

View file

@ -0,0 +1,86 @@
package com.tangem.features.feed.ui.market.detailed.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
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.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextOverflow
import com.tangem.core.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.conditional
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
@Composable
internal fun InformationTextBlock(
text: TextReference,
modifier: Modifier = Modifier,
onInfoClick: (() -> Unit)? = null,
textColor: Color = TangemTheme.colors2.text.neutral.tertiary,
infoIconColor: Color = TangemTheme.colors2.markers.iconGray,
informationTextBlockIconPosition: InformationTextBlockIconPosition = InformationTextBlockIconPosition.START,
) {
val interactionSource = remember { MutableInteractionSource() }
val infoIcon: @Composable () -> Unit = {
Icon(
modifier = Modifier.size(TangemTheme.dimens2.x4),
imageVector = ImageVector.vectorResource(id = R.drawable.ic_information_24),
tint = infoIconColor,
contentDescription = null,
)
}
val contentText: @Composable () -> Unit = {
Text(
text = text.resolveReference(),
style = TangemTheme.typography2.captionSemibold12,
color = textColor,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
Row(
modifier = modifier
.conditional(
condition = onInfoClick != null,
modifier = {
onInfoClick?.let { infoClick ->
clickable(
interactionSource = interactionSource,
indication = null,
onClick = infoClick,
)
} ?: this
},
),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1),
) {
when (informationTextBlockIconPosition) {
InformationTextBlockIconPosition.START -> {
infoIcon()
contentText()
}
InformationTextBlockIconPosition.END -> {
contentText()
infoIcon()
}
}
}
}
internal enum class InformationTextBlockIconPosition {
START, END
}

View file

@ -1,37 +1,33 @@
package com.tangem.features.feed.ui.market.detailed.components
import android.content.res.Configuration
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.SpacerW
import com.tangem.core.ui.components.block.information.GridItems
import com.tangem.core.ui.components.block.information.InformationBlock
import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons
import com.tangem.core.ui.components.text.TooltipText
import com.tangem.core.ui.ds.tabs.TangemSegmentUM
import com.tangem.core.ui.ds.tabs.TangemSegmentedPicker
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
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.core.ui.utils.PreviewShimmerContainer
import com.tangem.domain.markets.PriceChangeInterval
import com.tangem.features.feed.impl.R
import com.tangem.features.feed.ui.components.TokenMarketInformationBlock
import com.tangem.features.feed.ui.market.detailed.getText
import com.tangem.features.feed.ui.market.detailed.state.InfoPointUM
import com.tangem.features.feed.ui.market.detailed.state.InsightsUM
@ -40,6 +36,15 @@ import kotlinx.collections.immutable.toImmutableList
@Composable
internal fun InsightsBlock(state: InsightsUM, modifier: Modifier = Modifier) {
if (LocalRedesignEnabled.current) {
InsightsBlockV2(state, modifier)
} else {
InsightsBlockV1(state, modifier)
}
}
@Composable
private fun InsightsBlockV1(state: InsightsUM, modifier: Modifier = Modifier) {
var currentInterval by remember { mutableStateOf(PriceChangeInterval.H24) }
InformationBlock(
@ -104,8 +109,84 @@ internal fun InsightsBlock(state: InsightsUM, modifier: Modifier = Modifier) {
)
}
@Composable
private fun InsightsBlockV2(state: InsightsUM, modifier: Modifier = Modifier) {
val segmentItems = persistentListOf(
TangemSegmentUM(
id = PriceChangeInterval.H24.name,
title = resourceReference(R.string.markets_token_details_insight_day_timeline),
),
TangemSegmentUM(
id = PriceChangeInterval.WEEK.name,
title = resourceReference(R.string.markets_token_details_insight_week_timeline),
),
TangemSegmentUM(
id = PriceChangeInterval.MONTH.name,
title = resourceReference(R.string.markets_token_details_insight_month_timeline),
),
)
var currentInterval by remember { mutableStateOf(segmentItems.first()) }
TokenMarketInformationBlock(
modifier = modifier,
title = {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(
text = stringResourceSafe(R.string.markets_token_details_insights),
style = TangemTheme.typography2.headingSemibold20,
color = TangemTheme.colors2.text.neutral.primary,
)
TangemSegmentedPicker(
items = segmentItems,
initialSelectedItem = segmentItems.first(),
hasSeparator = true,
isFixed = false,
isAltSurface = true,
minSegmentWidth = 48.dp,
onClick = { segment ->
currentInterval = segment
state.onIntervalChanged(PriceChangeInterval.valueOf(segment.id))
},
)
}
},
content = {
val infoPoints = when (currentInterval.id) {
PriceChangeInterval.H24.name -> state.h24Info
PriceChangeInterval.WEEK.name -> state.weekInfo
PriceChangeInterval.MONTH.name -> state.monthInfo
else -> state.h24Info
}
GridItems(
modifier = Modifier.padding(top = 24.dp),
items = infoPoints,
itemContent = { infoPointUM ->
InfoPoint(
modifier = Modifier.align(Alignment.CenterStart),
infoPointUM = infoPointUM,
)
},
)
},
)
}
@Composable
internal fun InsightsBlockPlaceholder(modifier: Modifier = Modifier) {
if (LocalRedesignEnabled.current) {
InsightsBlockPlaceholderV2(modifier)
} else {
InsightsBlockPlaceholderV1(modifier)
}
}
@Composable
internal fun InsightsBlockPlaceholderV1(modifier: Modifier = Modifier) {
val subtitle2dp = with(LocalDensity.current) { TangemTheme.typography.subtitle2.lineHeight.toDp() }
val caption1dp = with(LocalDensity.current) { TangemTheme.typography.caption1.lineHeight.toDp() }
val headerHeight = maxOf(subtitle2dp, caption1dp) + TangemTheme.dimens.spacing4
@ -134,10 +215,47 @@ internal fun InsightsBlockPlaceholder(modifier: Modifier = Modifier) {
)
}
@Composable
internal fun InsightsBlockPlaceholderV2(modifier: Modifier = Modifier) {
TokenMarketInformationBlock(
modifier = modifier,
title = {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
RectangleShimmer(
modifier = Modifier
.height(24.dp)
.width(120.dp),
radius = TangemTheme.dimens2.x25,
)
SpacerW(62.dp)
RectangleShimmer(
modifier = Modifier
.height(36.dp)
.weight(1f),
radius = TangemTheme.dimens2.x25,
)
}
},
content = {
GridItems(
items = List(size = 4) { it }.toImmutableList(),
horizontalArragement = Arrangement.spacedBy(10.dp),
itemContent = {
InfoPointShimmer(modifier = Modifier.fillMaxWidth())
},
)
},
)
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun ContentPreview() {
private fun ContentPreviewV1() {
TangemThemePreview {
InsightsBlock(
state = InsightsUM(
@ -205,11 +323,95 @@ private fun ContentPreview() {
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PreviewPlaceholder() {
private fun ContentPreviewV2() {
CompositionLocalProvider(LocalRedesignEnabled provides true) {
TangemThemePreviewRedesign {
InsightsBlock(
state = InsightsUM(
h24Info = persistentListOf(
InfoPointUM(
title = resourceReference(R.string.markets_token_details_experienced_buyers),
value = "1 000 000 000",
),
InfoPointUM(
title = resourceReference(R.string.markets_token_details_buy_pressure),
value = "1 000 000 000",
),
InfoPointUM(
title = resourceReference(R.string.markets_token_details_holders),
value = "1 000 000 000",
),
InfoPointUM(
title = resourceReference(R.string.markets_token_details_liquidity),
value = "1 000 000 000",
),
),
weekInfo = persistentListOf(
InfoPointUM(
title = resourceReference(R.string.markets_token_details_experienced_buyers),
value = "1 000 000",
),
InfoPointUM(
title = resourceReference(R.string.markets_token_details_buy_pressure),
value = "1 000 000",
),
InfoPointUM(
title = resourceReference(R.string.markets_token_details_holders),
value = "1 000 000",
),
InfoPointUM(
title = resourceReference(R.string.markets_token_details_liquidity),
value = "1 000 000",
),
),
monthInfo = persistentListOf(
InfoPointUM(
title = resourceReference(R.string.markets_token_details_experienced_buyers),
value = "1 000",
),
InfoPointUM(
title = resourceReference(R.string.markets_token_details_buy_pressure),
value = "1 000",
),
InfoPointUM(
title = resourceReference(R.string.markets_token_details_holders),
value = "1 000",
),
InfoPointUM(
title = resourceReference(R.string.markets_token_details_liquidity),
value = "1 000",
),
),
onInfoClick = {},
onIntervalChanged = {},
),
)
}
}
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PreviewPlaceholderV1() {
TangemThemePreview {
PreviewShimmerContainer(
actualContent = { ContentPreview() },
actualContent = { ContentPreviewV1() },
shimmerContent = { InsightsBlockPlaceholder() },
)
}
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PreviewPlaceholderV2() {
TangemThemePreviewRedesign {
CompositionLocalProvider(LocalRedesignEnabled provides true) {
PreviewShimmerContainer(
actualContent = { ContentPreviewV2() },
shimmerContent = { InsightsBlockPlaceholder() },
)
}
}
}

View file

@ -1,14 +1,7 @@
package com.tangem.features.feed.ui.market.detailed.components
import android.content.res.Configuration
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@ -21,18 +14,35 @@ import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.components.block.information.InformationBlock
import com.tangem.core.ui.components.buttons.chip.Chip
import com.tangem.core.ui.components.inputrow.inner.DividerContainer
import com.tangem.core.ui.ds.badge.TangemBadge
import com.tangem.core.ui.ds.badge.TangemBadgeIconPosition
import com.tangem.core.ui.ds.badge.TangemBadgeShape
import com.tangem.core.ui.ds.badge.TangemBadgeSize
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.stringResourceSafe
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.core.ui.utils.PreviewShimmerContainer
import com.tangem.features.feed.impl.R
import com.tangem.features.feed.ui.components.ContainerWithDivider
import com.tangem.features.feed.ui.market.detailed.state.LinksUM
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@Composable
internal fun LinksBlock(state: LinksUM, modifier: Modifier = Modifier) {
if (LocalRedesignEnabled.current) {
LinksBlockV2(state, modifier)
} else {
LinksBlockV1(state, modifier)
}
}
@Composable
private fun LinksBlockV1(state: LinksUM, modifier: Modifier = Modifier) {
InformationBlock(
modifier = modifier,
contentHorizontalPadding = 0.dp,
@ -47,22 +57,22 @@ internal fun LinksBlock(state: LinksUM, modifier: Modifier = Modifier) {
},
content = {
Column {
SubBlock(
SubBlockV1(
title = stringResourceSafe(id = R.string.markets_token_details_official_links),
links = state.officialLinks,
onLinkClick = state.onLinkClick,
)
SubBlock(
SubBlockV1(
title = stringResourceSafe(id = R.string.markets_token_details_social),
links = state.social,
onLinkClick = state.onLinkClick,
)
SubBlock(
SubBlockV1(
title = stringResourceSafe(id = R.string.markets_token_details_repository),
links = state.repository,
onLinkClick = state.onLinkClick,
)
SubBlock(
SubBlockV1(
title = stringResourceSafe(id = R.string.markets_token_details_blockchain_site),
links = state.blockchainSite,
onLinkClick = state.onLinkClick,
@ -73,9 +83,36 @@ internal fun LinksBlock(state: LinksUM, modifier: Modifier = Modifier) {
)
}
@Composable
private fun LinksBlockV2(state: LinksUM, modifier: Modifier = Modifier) {
Column(modifier = modifier) {
SubBlockV2(
title = stringResourceSafe(id = R.string.markets_token_details_official_links),
links = state.officialLinks,
onLinkClick = state.onLinkClick,
)
SubBlockV2(
title = stringResourceSafe(id = R.string.markets_token_details_social),
links = state.social,
onLinkClick = state.onLinkClick,
)
SubBlockV2(
title = stringResourceSafe(id = R.string.markets_token_details_repository),
links = state.repository,
onLinkClick = state.onLinkClick,
)
SubBlockV2(
title = stringResourceSafe(id = R.string.markets_token_details_blockchain_site),
links = state.blockchainSite,
onLinkClick = state.onLinkClick,
lastBlock = true,
)
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun SubBlock(
private fun SubBlockV1(
links: ImmutableList<LinksUM.Link>,
onLinkClick: (LinksUM.Link) -> Unit,
modifier: Modifier = Modifier,
@ -115,8 +152,67 @@ private fun SubBlock(
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun SubBlockV2(
title: String,
links: ImmutableList<LinksUM.Link>,
onLinkClick: (LinksUM.Link) -> Unit,
modifier: Modifier = Modifier,
lastBlock: Boolean = false,
) {
if (links.isEmpty()) return
ContainerWithDivider(
modifier = modifier,
showDivider = !lastBlock,
) {
Column(
modifier = Modifier.padding(vertical = TangemTheme.dimens2.x2),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2),
) {
Text(
modifier = Modifier.padding(start = 10.dp, top = TangemTheme.dimens2.x4),
text = title,
style = TangemTheme.typography2.bodySemibold16,
color = TangemTheme.colors2.text.neutral.primary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
FlowRow(
modifier = Modifier.padding(vertical = TangemTheme.dimens2.x2, horizontal = 3.dp),
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2),
) {
links.fastForEach { link ->
TangemBadge(
text = stringReference(link.title),
onClick = { onLinkClick(link) },
iconPosition = TangemBadgeIconPosition.Start,
tangemIconUM = TangemIconUM.Icon(
iconRes = link.iconRes,
tintReference = { TangemTheme.colors2.markers.iconGray },
),
size = TangemBadgeSize.X9,
shape = TangemBadgeShape.Rounded,
)
}
}
}
}
}
@Composable
fun LinksBlockPlaceholder(modifier: Modifier = Modifier) {
if (LocalRedesignEnabled.current) {
LinksBlockPlaceholderV2(modifier)
} else {
LinksBlockPlaceholderV1(modifier)
}
}
@Composable
private fun LinksBlockPlaceholderV1(modifier: Modifier = Modifier) {
InformationBlock(
modifier = modifier,
contentHorizontalPadding = 0.dp,
@ -128,22 +224,31 @@ fun LinksBlockPlaceholder(modifier: Modifier = Modifier) {
},
content = {
Column {
SubBlockPlaceholder()
SubBlockPlaceholder()
SubBlockPlaceholder(lastBlock = true)
SubBlockPlaceholderV1()
SubBlockPlaceholderV1()
SubBlockPlaceholderV1(lastBlock = true)
}
},
)
}
@Composable
private fun SubBlockPlaceholder(modifier: Modifier = Modifier, lastBlock: Boolean = false) {
private fun LinksBlockPlaceholderV2(modifier: Modifier = Modifier) {
Column(modifier = modifier) {
SubBlockPlaceholderV2()
SubBlockPlaceholderV2()
SubBlockPlaceholderV2(lastBlock = true)
}
}
@Composable
private fun SubBlockPlaceholderV1(modifier: Modifier = Modifier, lastBlock: Boolean = false) {
DividerContainer(
modifier = modifier,
showDivider = !lastBlock,
) {
Column(
modifier = Modifier.padding(TangemTheme.dimens.spacing12),
modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing12),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
TextShimmer(
@ -163,12 +268,42 @@ private fun SubBlockPlaceholder(modifier: Modifier = Modifier, lastBlock: Boolea
}
}
@Composable
private fun SubBlockPlaceholderV2(modifier: Modifier = Modifier, lastBlock: Boolean = false) {
ContainerWithDivider(
modifier = modifier,
showDivider = !lastBlock,
) {
Column(
modifier = Modifier.padding(TangemTheme.dimens2.x2),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2),
) {
TextShimmer(
modifier = Modifier.width(56.dp),
style = TangemTheme.typography2.bodySemibold16,
radius = TangemTheme.dimens2.x25,
)
Row(
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2),
) {
repeat(times = 3) {
ChipShimmer(
modifier = Modifier
.height(36.dp)
.weight(1f),
)
}
}
}
}
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun ContentPreview() {
private fun ContentPreviewV1() {
TangemThemePreview {
LinksBlock(
LinksBlockV1(
state = LinksUM(
officialLinks = persistentListOf(
LinksUM.Link(
@ -216,11 +351,73 @@ private fun ContentPreview() {
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PlaceholderPreview() {
TangemThemePreview {
PreviewShimmerContainer(
shimmerContent = { LinksBlockPlaceholder() },
actualContent = { ContentPreview() },
private fun ContentPreviewV2() {
TangemThemePreviewRedesign {
LinksBlockV2(
state = LinksUM(
officialLinks = persistentListOf(
LinksUM.Link(
title = "Website",
iconRes = R.drawable.ic_plus_24,
url = "https://tangem.com",
),
LinksUM.Link(
title = "Website",
iconRes = R.drawable.ic_plus_24,
url = "https://tangem.com",
),
LinksUM.Link(
title = "Website",
iconRes = R.drawable.ic_plus_24,
url = "https://tangem.com",
),
),
social = persistentListOf(
LinksUM.Link(
title = "Twitter",
iconRes = R.drawable.ic_plus_24,
url = "https://tangem.com",
),
LinksUM.Link(
title = "Facebook",
iconRes = R.drawable.ic_plus_24,
url = "https://tangem.com",
),
),
repository = persistentListOf(
LinksUM.Link(
title = "Github",
iconRes = R.drawable.ic_plus_24,
url = "https://tangem.com",
),
),
blockchainSite = persistentListOf(),
onLinkClick = {},
),
)
}
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PlaceholderPreviewV1() {
TangemThemePreview {
PreviewShimmerContainer(
shimmerContent = { LinksBlockPlaceholderV1() },
actualContent = { ContentPreviewV1() },
)
}
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PlaceholderPreviewV2() {
TangemThemePreviewRedesign {
Column {
LinksBlockPlaceholderV2()
ContentPreviewV2()
}
}
}

View file

@ -2,31 +2,33 @@ package com.tangem.features.feed.ui.market.detailed.components
import android.content.res.Configuration
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTag
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.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.R
import com.tangem.core.ui.components.SpacerWMax
import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.components.block.information.InformationBlock
import com.tangem.core.ui.extensions.resolveReference
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.test.MarketsTestTags
import com.tangem.features.feed.ui.components.TokenMarketInformationBlock
import com.tangem.features.feed.ui.market.detailed.state.ListedOnUM
import kotlinx.coroutines.delay
@ -39,6 +41,15 @@ import kotlinx.coroutines.delay
*/
@Composable
internal fun ListedOnBlock(state: ListedOnUM, modifier: Modifier = Modifier) {
if (LocalRedesignEnabled.current) {
ListedOnBlockV2(state, modifier)
} else {
ListedOnBlockV1(state, modifier)
}
}
@Composable
private fun ListedOnBlockV1(state: ListedOnUM, modifier: Modifier = Modifier) {
Box(modifier = modifier) {
InformationBlock(
title = {
@ -75,8 +86,55 @@ internal fun ListedOnBlock(state: ListedOnUM, modifier: Modifier = Modifier) {
}
}
@Composable
private fun ListedOnBlockV2(state: ListedOnUM, modifier: Modifier = Modifier) {
TokenMarketInformationBlock(
modifier = modifier.clickable(enabled = state is ListedOnUM.Content) {
(state as? ListedOnUM.Content)?.onClick?.invoke()
},
title = {
Row(verticalAlignment = Alignment.CenterVertically) {
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text(
text = state.title.resolveReference(),
style = TangemTheme.typography2.headingSemibold20,
color = TangemTheme.colors2.text.neutral.primary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Text(
text = state.description.resolveReference(),
style = TangemTheme.typography2.captionSemibold13,
color = TangemTheme.colors2.text.neutral.secondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
SpacerWMax()
Icon(
modifier = Modifier.size(20.dp),
imageVector = ImageVector.vectorResource(R.drawable.ic_chevron_small_right_24),
tint = TangemTheme.colors2.markers.iconGray,
contentDescription = null,
)
}
},
)
}
@Composable
internal fun ListedOnBlockPlaceholder(modifier: Modifier = Modifier) {
if (LocalRedesignEnabled.current) {
ListedOnBlockPlaceholderV2(modifier)
} else {
ListedOnBlockPlaceholderV1(modifier)
}
}
@Composable
internal fun ListedOnBlockPlaceholderV1(modifier: Modifier = Modifier) {
InformationBlock(
title = {
TextShimmer(
@ -95,11 +153,32 @@ internal fun ListedOnBlockPlaceholder(modifier: Modifier = Modifier) {
}
}
@Composable
internal fun ListedOnBlockPlaceholderV2(modifier: Modifier = Modifier) {
TokenMarketInformationBlock(
modifier = modifier.fillMaxWidth(),
title = {
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
TextShimmer(
style = TangemTheme.typography2.headingSemibold20,
modifier = Modifier.width(120.dp),
radius = TangemTheme.dimens2.x25,
)
TextShimmer(
style = TangemTheme.typography2.captionSemibold13,
modifier = Modifier.width(66.dp),
radius = TangemTheme.dimens2.x25,
)
}
},
)
}
@Composable
private fun Description(state: ListedOnUM, modifier: Modifier = Modifier) {
Text(
text = state.description.resolveReference(),
modifier = modifier,
modifier = modifier.semantics { testTag = MarketsTestTags.LISTED_ON_EXCHANGES_COUNT },
color = TangemTheme.colors.text.tertiary,
overflow = TextOverflow.Ellipsis,
maxLines = 1,

View file

@ -1,14 +1,14 @@
package com.tangem.features.feed.ui.market.detailed.components
import android.content.res.Configuration
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.TextButton
import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.components.block.information.GridItems
@ -16,12 +16,16 @@ import com.tangem.core.ui.components.block.information.InformationBlock
import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
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.utils.PreviewShimmerContainer
import com.tangem.features.feed.impl.R
import com.tangem.features.feed.ui.components.MetricsCard
import com.tangem.features.feed.ui.market.detailed.state.InfoPointUM
import com.tangem.features.feed.ui.market.detailed.state.InfoPointUMV2
import com.tangem.features.feed.ui.market.detailed.state.MetricsUM
import com.tangem.features.feed.ui.market.detailed.state.MetricsV2UM
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
@ -29,6 +33,17 @@ const val MAX_METRICS_COUNT = 6
@Composable
internal fun MetricsBlock(state: MetricsUM, modifier: Modifier = Modifier) {
if (LocalRedesignEnabled.current) {
state.metricsV2?.let {
MetricsBlockV2(it, modifier)
}
} else {
MetricsBlockV1(state, modifier)
}
}
@Composable
private fun MetricsBlockV1(state: MetricsUM, modifier: Modifier = Modifier) {
var isExpanded by remember { mutableStateOf(false) }
InformationBlock(
@ -64,6 +79,50 @@ internal fun MetricsBlock(state: MetricsUM, modifier: Modifier = Modifier) {
)
}
@Composable
private fun MetricsBlockV2(state: MetricsV2UM, modifier: Modifier = Modifier) {
Column(
modifier = modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2),
) {
state.rows.forEach { row ->
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2),
) {
Box(Modifier.weight(1f)) {
MetricRowItem(row.first)
}
row.second?.let { second ->
Box(Modifier.weight(1f)) {
MetricRowItem(second)
}
}
}
}
}
}
@Composable
private fun MetricRowItem(item: InfoPointUMV2) {
when (item) {
is InfoPointUMV2.CirculatingSupply -> CirculatingSupplyCard(item)
else -> MetricCard(item)
}
}
@Composable
private fun MetricCard(item: InfoPointUMV2) {
when (item) {
is InfoPointUMV2.MarketCap -> MarketCapCard(item)
is InfoPointUMV2.TradingVolume -> TradingVolumeCard(item)
is InfoPointUMV2.MarketPosition -> MarketPositionCard(item)
is InfoPointUMV2.FullyDilutedValuation -> FDVCard(item)
is InfoPointUMV2.CirculatingSupply -> Unit
}
}
// TODO make TextButton clickable area smaller and remove paddings for an action in InformationBlock
@Composable
private fun ShowLessMoreButton(expanded: Boolean, onClick: () -> Unit) {
@ -84,6 +143,15 @@ private fun ShowLessMoreButton(expanded: Boolean, onClick: () -> Unit) {
@Composable
internal fun MetricsBlockPlaceholder(modifier: Modifier = Modifier) {
if (LocalRedesignEnabled.current) {
MetricsBlockPlaceholderV2(modifier)
} else {
MetricsBlockPlaceholderV1(modifier)
}
}
@Composable
private fun MetricsBlockPlaceholderV1(modifier: Modifier = Modifier) {
InformationBlock(
modifier = modifier,
title = {
@ -111,6 +179,108 @@ internal fun MetricsBlockPlaceholder(modifier: Modifier = Modifier) {
)
}
@Composable
private fun MetricsBlockPlaceholderV2(modifier: Modifier = Modifier) {
Column(
modifier = modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2),
) {
repeat(2) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2),
) {
repeat(2) {
Box(Modifier.weight(1f)) {
MetricsCard(
modifier = Modifier
.heightIn(120.dp)
.fillMaxWidth(),
title = {
RectangleShimmer(
modifier = Modifier
.fillMaxWidth()
.height(28.dp)
.padding(end = 10.dp),
radius = TangemTheme.dimens2.x25,
)
},
content = {
RectangleShimmer(
modifier = Modifier
.fillMaxWidth()
.height(16.dp)
.padding(end = 74.dp),
radius = TangemTheme.dimens2.x25,
)
},
)
}
}
}
}
CirculatingSupplyCardPlaceholder()
}
}
@Composable
private fun CirculatingSupplyCardPlaceholder() {
MetricsCard(
modifier = Modifier
.heightIn(120.dp)
.fillMaxWidth(),
title = {
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
) {
RectangleShimmer(
modifier = Modifier
.width(104.dp)
.height(16.dp),
radius = TangemTheme.dimens2.x25,
)
RectangleShimmer(
modifier = Modifier
.width(64.dp)
.height(16.dp),
radius = TangemTheme.dimens2.x25,
)
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
) {
RectangleShimmer(
modifier = Modifier
.width(160.dp)
.height(28.dp),
radius = TangemTheme.dimens2.x25,
)
RectangleShimmer(
modifier = Modifier
.width(48.dp)
.height(28.dp),
radius = TangemTheme.dimens2.x25,
)
}
}
},
content = {
RectangleShimmer(
modifier = Modifier
.fillMaxWidth()
.height(6.dp),
radius = TangemTheme.dimens2.x25,
)
},
)
}
@Preview
@Preview("Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
@ -150,6 +320,7 @@ private fun BlockPreview() {
onInfoClick = {},
),
),
metricsV2 = null,
),
)
}

View file

@ -0,0 +1,417 @@
package com.tangem.features.feed.ui.market.detailed.components
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Icon
import androidx.compose.material3.LinearProgressIndicator
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
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.res.vectorResource
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.unit.dp
import androidx.compose.ui.unit.sp
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.progressbar.TangemLinearProgressIndicator
import com.tangem.core.ui.ds.progress.TangemLinearProgressIndicatorWithDot
import com.tangem.core.ui.ds.row.TangemRowContainer
import com.tangem.core.ui.ds.row.TangemRowLayoutId
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.LocalIsInDarkTheme
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.features.feed.impl.R
import com.tangem.features.feed.ui.components.MetricsCard
import com.tangem.features.feed.ui.market.detailed.state.InfoPointUMV2
import com.tangem.features.feed.ui.market.detailed.state.MarketRatingType
import com.tangem.features.feed.ui.market.detailed.state.TrendingVolumeLiquidityType
@Composable
internal fun MarketCapCard(item: InfoPointUMV2.MarketCap) {
MetricsCard(
modifier = Modifier
.heightIn(120.dp)
.fillMaxWidth(),
title = {
Text(
text = item.capitalizationValue.resolveReference(),
style = TangemTheme.typography2.headingSemibold20,
color = TangemTheme.colors2.text.neutral.primary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
},
content = {
InformationTextBlock(
text = resourceReference(R.string.markets_token_details_market_capitalization),
onInfoClick = item.onInfoClick,
)
},
)
}
@Composable
internal fun TradingVolumeCard(item: InfoPointUMV2.TradingVolume) {
val tradingColor = when (item.trendingVolumeLiquidityType) {
TrendingVolumeLiquidityType.HIGH -> TangemTheme.colors2.markers.backgroundSolidGreen
TrendingVolumeLiquidityType.MEDIUM -> TangemTheme.colors2.graphic.status.attention
TrendingVolumeLiquidityType.LOW -> TangemTheme.colors2.graphic.status.warning
}
MetricsCard(
modifier = Modifier
.heightIn(120.dp)
.fillMaxWidth(),
title = {
Row {
Text(
text = item.tradingValue.resolveReference(),
style = TangemTheme.typography2.headingSemibold20,
color = TangemTheme.colors2.text.neutral.primary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Text(
modifier = Modifier.padding(TangemTheme.dimens2.x1),
text = stringResourceSafe(R.string.markets_token_details_trading_interval),
style = TangemTheme.typography2.captionSemibold11,
color = TangemTheme.colors2.text.neutral.primary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
},
content = {
Column(modifier = Modifier.fillMaxWidth()) {
TangemLinearProgressIndicator(
modifier = Modifier
.fillMaxWidth()
.height(6.dp),
progress = { item.liquidity },
color = tradingColor,
backgroundColor = TangemTheme.colors2.graphic.neutral.primaryInvertedConstant.copy(alpha = .1f),
)
SpacerH(12.dp)
InformationTextBlock(
text = resourceReference(R.string.markets_token_details_trading_volume),
textColor = tradingColor,
infoIconColor = tradingColor,
onInfoClick = item.onInfoClick,
)
}
},
cardColor = tradingColor.copy(alpha = .2f),
)
}
@Composable
internal fun MarketPositionCard(item: InfoPointUMV2.MarketPosition) {
val ratingCardColor = mapRatingToCardColor(marketRatingType = item.marketRatingType)
val ratingColor = mapRatingToColor(marketRatingType = item.marketRatingType)
MetricsCard(
modifier = Modifier
.heightIn(120.dp)
.fillMaxWidth(),
title = {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_big_laurel_left_20),
tint = ratingColor,
contentDescription = null,
)
Text(
textAlign = TextAlign.Center,
text = item.position,
color = ratingColor,
style = TangemTheme.typography2.headingSemibold20.copy(letterSpacing = 0.sp),
maxLines = 1,
)
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_big_laurel_right_20),
tint = ratingColor,
contentDescription = null,
)
}
},
content = {
Column(modifier = Modifier.fillMaxWidth()) {
TangemLinearProgressIndicatorWithDot(
modifier = Modifier
.fillMaxWidth()
.height(6.dp),
progress = { item.rangeValue },
dotColor = TangemTheme.colors2.fill.neutral.primaryInvertedConstant,
backgroundColor = TangemTheme.colors2.graphic.neutral.primaryInvertedConstant.copy(alpha = .1f),
)
SpacerH(12.dp)
InformationTextBlock(
text = resourceReference(R.string.markets_token_details_market_rating),
textColor = ratingColor,
infoIconColor = ratingColor,
onInfoClick = item.onInfoClick,
)
}
},
cardColor = ratingCardColor,
)
}
@Composable
internal fun FDVCard(item: InfoPointUMV2.FullyDilutedValuation) {
MetricsCard(
modifier = Modifier
.heightIn(120.dp)
.fillMaxWidth(),
title = {
Text(
text = item.value.resolveReference(),
style = TangemTheme.typography2.headingSemibold20,
color = TangemTheme.colors2.text.neutral.primary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
},
content = {
InformationTextBlock(
text = resourceReference(R.string.markets_token_details_fully_diluted_valuation),
onInfoClick = item.onInfoClick,
)
},
)
}
@Composable
internal fun CirculatingSupplyCard(item: InfoPointUMV2.CirculatingSupply) {
MetricsCard(
modifier = Modifier
.heightIn(min = if (item.fillValue == null) 88.dp else 114.dp)
.fillMaxWidth(),
title = {
TangemRowContainer(contentPadding = PaddingValues(0.dp)) {
Text(
modifier = Modifier.layoutId(TangemRowLayoutId.START_TOP),
text = stringResourceSafe(R.string.markets_token_details_circulating_supply),
style = TangemTheme.typography2.captionSemibold13,
color = TangemTheme.colors2.text.neutral.tertiary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Text(
modifier = Modifier
.padding(top = 12.dp)
.layoutId(TangemRowLayoutId.START_BOTTOM),
text = item.currentValue.resolveReference(),
style = TangemTheme.typography2.headingSemibold20,
color = TangemTheme.colors2.text.neutral.primary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Text(
modifier = Modifier.layoutId(TangemRowLayoutId.END_TOP),
text = stringResourceSafe(R.string.markets_token_details_max_supply),
style = TangemTheme.typography2.captionSemibold13,
color = TangemTheme.colors2.text.neutral.tertiary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
if (item.maxValue != null) {
Text(
modifier = Modifier
.padding(top = 12.dp)
.layoutId(TangemRowLayoutId.END_BOTTOM),
text = item.maxValue.resolveReference(),
style = TangemTheme.typography2.headingSemibold20,
color = TangemTheme.colors2.text.neutral.primary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
},
content = {
if (item.fillValue != null) {
LinearProgressIndicator(
modifier = Modifier
.fillMaxWidth()
.height(6.dp),
color = TangemTheme.colors2.graphic.status.accent,
trackColor = TangemTheme.colors2.graphic.neutral.primaryInvertedConstant.copy(alpha = .1f),
progress = { item.fillValue },
strokeCap = StrokeCap.Round,
drawStopIndicator = {},
gapSize = 4.dp,
)
}
},
onClick = item.onInfoClick,
)
}
@Composable
private fun MarketRatingType.baseColor(): Color {
val isDarkTheme = LocalIsInDarkTheme.current
return when (this) {
MarketRatingType.GOLD ->
if (isDarkTheme) Color(GOLD_PLACE_COLOR_NIGHT) else Color(GOLD_PLACE_COLOR_LIGHT)
MarketRatingType.SILVER ->
if (isDarkTheme) Color(SILVER_PLACE_COLOR_NIGHT) else Color(SILVER_PLACE_COLOR_LIGHT)
MarketRatingType.BRONZE ->
if (isDarkTheme) Color(BRONZE_PLACE_COLOR_NIGHT) else Color(BRONZE_PLACE_COLOR_LIGHT)
MarketRatingType.OTHER ->
TangemTheme.colors2.graphic.neutral.primary
}
}
@Composable
private fun mapRatingToColor(marketRatingType: MarketRatingType): Color = marketRatingType.baseColor()
@Composable
private fun mapRatingToCardColor(marketRatingType: MarketRatingType): Color {
return when (marketRatingType) {
MarketRatingType.OTHER -> TangemTheme.colors2.surface.level3
else -> marketRatingType.baseColor().copy(alpha = 0.3f)
}
}
private const val GOLD_PLACE_COLOR_NIGHT = 0xFFFBEE76
private const val GOLD_PLACE_COLOR_LIGHT = 0xFFD9B900
private const val SILVER_PLACE_COLOR_NIGHT = 0xFFAABEF7
private const val SILVER_PLACE_COLOR_LIGHT = 0xFF6680CC
private const val BRONZE_PLACE_COLOR_NIGHT = 0xFFFF9976
private const val BRONZE_PLACE_COLOR_LIGHT = 0xFFCC7F66
@Suppress("LongMethod")
@Preview(widthDp = 360, heightDp = 1500, showBackground = true)
@Preview(widthDp = 360, heightDp = 1500, showBackground = true, locale = "ru")
@Preview(widthDp = 360, heightDp = 1500, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun MetricsCardsPreview() {
CompositionLocalProvider(LocalRedesignEnabled provides true) {
TangemThemePreviewRedesign {
Column(
modifier = Modifier
.fillMaxWidth()
.background(TangemTheme.colors2.surface.level2)
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
MarketCapCard(
item = InfoPointUMV2.MarketCap(
capitalizationValue = stringReference("$ 1.2 T"),
onInfoClick = {},
),
)
TradingVolumeCard(
item = InfoPointUMV2.TradingVolume(
tradingValue = stringReference("$ 45.2 M"),
liquidity = 0.75f,
trendingVolumeLiquidityType = TrendingVolumeLiquidityType.HIGH,
onInfoClick = {},
),
)
TradingVolumeCard(
item = InfoPointUMV2.TradingVolume(
tradingValue = stringReference("$ 12.1 M"),
liquidity = 0.45f,
trendingVolumeLiquidityType = TrendingVolumeLiquidityType.MEDIUM,
onInfoClick = {},
),
)
TradingVolumeCard(
item = InfoPointUMV2.TradingVolume(
tradingValue = stringReference("$ 2.3 M"),
liquidity = 0.15f,
trendingVolumeLiquidityType = TrendingVolumeLiquidityType.LOW,
onInfoClick = {},
),
)
MarketPositionCard(
item = InfoPointUMV2.MarketPosition(
position = "1",
rangeValue = 0.02f,
marketRatingType = MarketRatingType.GOLD,
onInfoClick = {},
),
)
MarketPositionCard(
item = InfoPointUMV2.MarketPosition(
position = "2",
rangeValue = 0.05f,
marketRatingType = MarketRatingType.SILVER,
onInfoClick = {},
),
)
MarketPositionCard(
item = InfoPointUMV2.MarketPosition(
position = "3",
rangeValue = 0.08f,
marketRatingType = MarketRatingType.BRONZE,
onInfoClick = {},
),
)
MarketPositionCard(
item = InfoPointUMV2.MarketPosition(
position = "42",
rangeValue = 0.42f,
marketRatingType = MarketRatingType.OTHER,
onInfoClick = {},
),
)
FDVCard(
item = InfoPointUMV2.FullyDilutedValuation(
value = stringReference("$ 1.5 T"),
onInfoClick = {},
),
)
CirculatingSupplyCard(
item = InfoPointUMV2.CirculatingSupply(
currentValue = stringReference("12.5 B POL"),
maxValue = stringReference("21 B POL"),
fillValue = 0.6f,
onInfoClick = {},
),
)
CirculatingSupplyCard(
item = InfoPointUMV2.CirculatingSupply(
currentValue = stringReference("18.9 M ETH"),
maxValue = null,
fillValue = null,
onInfoClick = {},
),
)
}
}
}
}

View file

@ -1,11 +1,7 @@
package com.tangem.features.feed.ui.market.detailed.components
import androidx.annotation.FloatRange
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@ -22,6 +18,7 @@ import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.feed.impl.R
import kotlin.math.round
@ -34,6 +31,28 @@ internal fun ScoreStarsBlock(
horizontalSpacing: Dp,
scoreTextStyle: TextStyle,
modifier: Modifier = Modifier,
) {
if (LocalRedesignEnabled.current) {
ScoreStarsBlockV2(
score = score,
modifier = modifier,
)
} else {
ScoreStarsBlockV1(
score = score,
horizontalSpacing = horizontalSpacing,
scoreTextStyle = scoreTextStyle,
modifier = modifier,
)
}
}
@Composable
private fun ScoreStarsBlockV1(
score: Float,
horizontalSpacing: Dp,
scoreTextStyle: TextStyle,
modifier: Modifier = Modifier,
) {
val rounded = score.roundTo1decimal()
val percentage = rounded / STARS_COUNT
@ -51,16 +70,38 @@ internal fun ScoreStarsBlock(
}
}
@Composable
private fun ScoreStarsBlockV2(score: Float, modifier: Modifier = Modifier) {
val rounded = score.roundTo1decimal()
val percentage = rounded / STARS_COUNT
Box(
modifier = modifier,
contentAlignment = Alignment.Center,
) {
Stars(fraction = percentage)
}
}
@Suppress("MagicNumber")
@Composable
private fun Stars(@FloatRange(0.0, 1.0) fraction: Float = 0f) {
if (LocalRedesignEnabled.current) {
StarsV2(fraction)
} else {
StarsV1(fraction)
}
}
@Suppress("MagicNumber")
@Composable
private fun StarsV1(@FloatRange(0.0, 1.0) fraction: Float = 0f) {
val grayColor = TangemTheme.colors.icon.inactive
Row(
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4),
verticalAlignment = Alignment.CenterVertically,
) {
repeat(times = 5) { i ->
repeat(times = STARS_COUNT) { i ->
Box(
modifier = Modifier.size(TangemTheme.dimens.size16),
contentAlignment = Alignment.Center,
@ -94,6 +135,49 @@ private fun Stars(@FloatRange(0.0, 1.0) fraction: Float = 0f) {
}
}
@Suppress("MagicNumber")
@Composable
private fun StarsV2(@FloatRange(0.0, 1.0) fraction: Float = 0f) {
val grayColor = TangemTheme.colors2.markers.iconGray
Row(
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1),
verticalAlignment = Alignment.CenterVertically,
) {
repeat(times = STARS_COUNT) { i ->
Box(
modifier = Modifier.size(TangemTheme.dimens2.x5),
contentAlignment = Alignment.Center,
) {
Icon(
modifier = Modifier
.requiredSize(16.dp)
.graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen)
.drawWithCache {
onDrawWithContent {
val starFraction = ((fraction - i * 0.2) / 0.2).coerceIn(0.0, 1.0)
val starFractionFloat = starFraction
.toFloat()
.roundTo1decimal()
drawContent()
drawRect(
color = grayColor,
topLeft = Offset(x = size.width * starFractionFloat, y = 0f),
size = Size(size.width * (1 - starFractionFloat), size.height),
blendMode = BlendMode.SrcIn,
)
}
},
imageVector = ImageVector.vectorResource(R.drawable.ic_star_filled_20),
contentDescription = null,
tint = TangemTheme.colors2.markers.iconBlue,
)
}
}
}
}
@Suppress("MagicNumber")
private fun Float.roundTo1decimal(): Float {
return round(this * 10) / 10

View file

@ -2,33 +2,45 @@ package com.tangem.features.feed.ui.market.detailed.components
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.*
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.draw.clip
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.components.text.TooltipText
import com.tangem.core.ui.ds.row.TangemRowContainer
import com.tangem.core.ui.ds.row.TangemRowLayoutId
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
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.core.ui.utils.PreviewShimmerContainer
import com.tangem.features.feed.impl.R
import com.tangem.features.feed.ui.components.ContainerWithDivider
import com.tangem.features.feed.ui.market.detailed.state.SecurityScoreUM
@Composable
internal fun SecurityScoreBlock(state: SecurityScoreUM, modifier: Modifier = Modifier) {
if (LocalRedesignEnabled.current) {
SecurityScoreBlockV2(state, modifier)
} else {
SecurityScoreBlockV1(state, modifier)
}
}
@Composable
private fun SecurityScoreBlockV1(state: SecurityScoreUM, modifier: Modifier = Modifier) {
Row(
modifier = modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
@ -66,8 +78,99 @@ internal fun SecurityScoreBlock(state: SecurityScoreUM, modifier: Modifier = Mod
}
}
@Composable
private fun SecurityScoreBlockV2(state: SecurityScoreUM, modifier: Modifier = Modifier) {
ContainerWithDivider(
modifier = modifier,
showDivider = true,
) {
TangemRowContainer(modifier = Modifier.padding(top = 20.dp, bottom = 24.dp)) {
Text(
modifier = Modifier.layoutId(layoutId = TangemRowLayoutId.START_TOP),
text = "${state.score}",
color = TangemTheme.colors2.text.neutral.primary,
style = TangemTheme.typography2.headingBold28,
)
InformationTextBlock(
modifier = Modifier.layoutId(layoutId = TangemRowLayoutId.START_BOTTOM),
text = resourceReference(R.string.markets_token_details_security_score),
onInfoClick = state.onInfoClick,
textColor = TangemTheme.colors2.text.neutral.primary,
informationTextBlockIconPosition = InformationTextBlockIconPosition.END,
)
Text(
modifier = Modifier.layoutId(layoutId = TangemRowLayoutId.END_BOTTOM),
text = state.description.resolveReference(),
style = TangemTheme.typography2.captionSemibold12,
color = TangemTheme.colors2.text.neutral.tertiary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
ScoreStarsBlock(
modifier = Modifier
.padding(bottom = 16.dp)
.layoutId(layoutId = TangemRowLayoutId.END_TOP),
score = state.score,
scoreTextStyle = TangemTheme.typography.body1,
horizontalSpacing = TangemTheme.dimens.spacing8,
)
}
}
}
@Composable
internal fun SecurityScoreBlockPlaceholder(modifier: Modifier = Modifier) {
if (LocalRedesignEnabled.current) {
SecurityScoreBlockPlaceholderV2(modifier)
} else {
SecurityScoreBlockPlaceholderV1(modifier)
}
}
@Composable
private fun SecurityScoreBlockPlaceholderV2(modifier: Modifier = Modifier) {
TangemRowContainer(modifier = modifier) {
TextShimmer(
modifier = Modifier
.width(114.dp)
.layoutId(layoutId = TangemRowLayoutId.START_TOP),
style = TangemTheme.typography2.headingBold28,
radius = TangemTheme.dimens2.x25,
)
TextShimmer(
modifier = Modifier
.width(74.dp)
.padding(top = 8.dp)
.layoutId(layoutId = TangemRowLayoutId.START_BOTTOM),
style = TangemTheme.typography2.captionSemibold12,
radius = TangemTheme.dimens2.x25,
)
TextShimmer(
modifier = Modifier
.width(116.dp)
.layoutId(layoutId = TangemRowLayoutId.END_TOP),
style = TangemTheme.typography2.headingBold28,
radius = TangemTheme.dimens2.x25,
)
TextShimmer(
modifier = Modifier
.width(96.dp)
.padding(top = 8.dp)
.layoutId(layoutId = TangemRowLayoutId.END_BOTTOM),
style = TangemTheme.typography2.captionSemibold12,
radius = TangemTheme.dimens2.x25,
)
}
}
@Composable
private fun SecurityScoreBlockPlaceholderV1(modifier: Modifier = Modifier) {
Row(
modifier = modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
@ -109,7 +212,7 @@ internal fun SecurityScoreBlockPlaceholder(modifier: Modifier = Modifier) {
@Preview(widthDp = 328, showBackground = true, locale = "ru")
@Preview(widthDp = 328, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun ContentPreview() {
private fun ContentPreviewV1() {
TangemThemePreview {
SecurityScoreBlock(
state = SecurityScoreUM(
@ -124,7 +227,7 @@ private fun ContentPreview() {
@Preview(widthDp = 328, showBackground = true)
@Preview(widthDp = 328, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PreviewPlaceholder() {
private fun PreviewPlaceholderV1() {
TangemThemePreview {
PreviewShimmerContainer(
shimmerContent = {
@ -133,8 +236,36 @@ private fun PreviewPlaceholder() {
)
},
actualContent = {
ContentPreview()
ContentPreviewV1()
},
)
}
}
@Preview(widthDp = 328, showBackground = true)
@Preview(widthDp = 328, showBackground = true, locale = "ru")
@Preview(widthDp = 328, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun ContentPreviewV2() {
CompositionLocalProvider(
LocalRedesignEnabled provides true,
) {
TangemThemePreviewRedesign {
Column(modifier = Modifier.background(TangemTheme.colors2.surface.level2)) {
SecurityScoreBlock(
state = SecurityScoreUM(
score = 3.5f,
description = stringReference("Based on 3 ratings"),
onInfoClick = {},
),
)
SpacerH(10.dp)
SecurityScoreBlockPlaceholder(
modifier = Modifier.fillMaxWidth(),
)
}
}
}
}

View file

@ -1,42 +1,71 @@
package com.tangem.features.feed.ui.market.detailed.components
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.*
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.CompositingStrategy
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastForEachIndexed
import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import com.tangem.core.ui.components.*
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetType
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheetTitle
import com.tangem.core.ui.components.inputrow.inner.DividerContainer
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
import com.tangem.core.ui.ds.row.TangemRowContainer
import com.tangem.core.ui.ds.row.TangemRowLayoutId
import com.tangem.core.ui.ds.topbar.TangemTopBar
import com.tangem.core.ui.ds.topbar.TangemTopBarType
import com.tangem.core.ui.extensions.clickableSingle
import com.tangem.core.ui.extensions.resolveReference
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.preview.SecurityScorePreviewData
import com.tangem.features.feed.ui.market.detailed.state.SecurityScoreBottomSheetContent
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet as TangemBottomSheetV2
@Composable
internal fun SecurityScoreBottomSheet(config: TangemBottomSheetConfig) {
if (LocalRedesignEnabled.current) {
SecurityScoreBottomSheetV2(config)
} else {
SecurityScoreBottomSheetV1(config)
}
}
@Composable
private fun SecurityScoreBottomSheetV1(config: TangemBottomSheetConfig) {
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
TangemBottomSheet<SecurityScoreBottomSheetContent>(
@ -82,6 +111,62 @@ internal fun SecurityScoreBottomSheet(config: TangemBottomSheetConfig) {
)
}
@Composable
private fun SecurityScoreBottomSheetV2(config: TangemBottomSheetConfig) {
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
TangemBottomSheetV2<SecurityScoreBottomSheetContent>(
config = config,
type = TangemBottomSheetType.Modal,
title = { content ->
TangemTopBar(
title = content.title,
type = TangemTopBarType.BottomSheet,
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 = config.onDismissRequest)
.padding(TangemTheme.dimens2.x2_5),
)
},
)
},
content = { content ->
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.padding(horizontal = TangemTheme.dimens2.x4),
) {
Text(
text = content.description.resolveReference(),
style = TangemTheme.typography2.bodyRegular15.copy(
color = TangemTheme.colors2.text.neutral.tertiary,
),
)
SpacerH12()
content.providers.fastForEach { provider ->
SecurityScoreProviderRowV2(
providerUM = provider,
onLinkClick = { content.onProviderLinkClick(provider) },
)
SpacerH(TangemTheme.dimens2.x2)
}
SpacerH16()
SpacerH(bottomBarHeight)
}
},
)
}
@Composable
private fun SecurityScoreProviderRow(
providerUM: SecurityScoreBottomSheetContent.SecurityScoreProviderUM,
@ -145,17 +230,91 @@ private fun SecurityScoreProviderRow(
horizontalSpacing = TangemTheme.dimens.spacing3,
)
UrlBlock(providerUM)
UrlBlockV1(providerUM)
}
}
}
@Composable
private fun UrlBlock(providerUM: SecurityScoreBottomSheetContent.SecurityScoreProviderUM) {
private fun SecurityScoreProviderRowV2(
providerUM: SecurityScoreBottomSheetContent.SecurityScoreProviderUM,
onLinkClick: () -> Unit,
) {
TangemRowContainer(
modifier = Modifier
.background(
color = TangemTheme.colors2.surface.level4,
shape = RoundedCornerShape(TangemTheme.dimens2.x5),
)
.border(
width = 1.dp,
color = TangemTheme.colors2.border.neutral.primary,
shape = RoundedCornerShape(TangemTheme.dimens2.x5),
),
) {
SubcomposeAsyncImage(
modifier = Modifier
.size(size = TangemTheme.dimens.size40)
.clip(TangemTheme.shapes.roundedCorners8)
.layoutId(TangemRowLayoutId.HEAD),
model = ImageRequest.Builder(context = LocalContext.current)
.data(providerUM.iconUrl)
.crossfade(enable = true)
.allowHardware(false)
.build(),
loading = { RectangleShimmer(radius = TangemTheme.dimens2.x3) },
error = { RectangleShimmer(radius = TangemTheme.dimens2.x3) },
contentDescription = null,
)
Text(
modifier = Modifier
.padding(start = TangemTheme.dimens2.x2)
.layoutId(TangemRowLayoutId.START_TOP),
text = providerUM.name,
style = TangemTheme.typography2.bodySemibold16,
color = TangemTheme.colors2.text.neutral.primary,
)
providerUM.lastAuditDate?.let { auditDate ->
Text(
modifier = Modifier
.padding(start = TangemTheme.dimens2.x2)
.layoutId(TangemRowLayoutId.START_BOTTOM),
text = auditDate,
style = TangemTheme.typography2.captionSemibold12,
color = TangemTheme.colors2.text.neutral.secondary,
)
}
StarBlock(
score = providerUM.score,
modifier = Modifier
.padding(start = TangemTheme.dimens2.x2)
.clickableSingle(onClick = onLinkClick)
.layoutId(TangemRowLayoutId.END_TOP),
)
UrlBlockV2(
modifier = Modifier
.padding(start = TangemTheme.dimens2.x2)
.clickableSingle(onClick = onLinkClick)
.layoutId(TangemRowLayoutId.END_BOTTOM),
providerUM = providerUM,
)
}
}
@Composable
private fun UrlBlockV1(
providerUM: SecurityScoreBottomSheetContent.SecurityScoreProviderUM,
modifier: Modifier = Modifier,
) {
val urlData = providerUM.urlData
val rootHost = urlData?.rootHost
if (urlData != null && rootHost != null) {
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4),
) {
Text(
@ -174,10 +333,75 @@ private fun UrlBlock(providerUM: SecurityScoreBottomSheetContent.SecurityScorePr
}
}
@Composable
private fun UrlBlockV2(
providerUM: SecurityScoreBottomSheetContent.SecurityScoreProviderUM,
modifier: Modifier = Modifier,
) {
val urlData = providerUM.urlData
val rootHost = urlData?.rootHost
if (urlData != null && rootHost != null) {
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x0_5),
) {
Text(
text = urlData.rootHost,
style = TangemTheme.typography2.captionSemibold12,
color = TangemTheme.colors2.text.neutral.secondary,
)
Icon(
modifier = Modifier
.size(TangemTheme.dimens2.x4),
imageVector = ImageVector.vectorResource(id = R.drawable.ic_arrow_top_right_24),
contentDescription = null,
tint = TangemTheme.colors2.markers.iconGray,
)
}
}
}
@Suppress("MagicNumber")
@Composable
fun StarBlock(score: Float, modifier: Modifier = Modifier) {
val grayColor = TangemTheme.colors2.markers.iconGray
val percentage = score / 5
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = score.toString(),
style = TangemTheme.typography2.bodySemibold16,
color = TangemTheme.colors2.text.neutral.primary,
)
Icon(
modifier = Modifier
.requiredSize(TangemTheme.dimens2.x5)
.graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen)
.drawWithCache {
onDrawWithContent {
drawContent()
drawRect(
color = grayColor,
topLeft = Offset(x = size.width * percentage, y = 0f),
size = Size(size.width * (1 - percentage), size.height),
blendMode = BlendMode.SrcIn,
)
}
},
imageVector = ImageVector.vectorResource(R.drawable.ic_star_filled_20),
contentDescription = null,
tint = TangemTheme.colors2.markers.iconBlue,
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun SecurityScoreBottomSheetPreview() {
private fun SecurityScoreBottomSheetPreviewV1() {
TangemThemePreview {
SecurityScoreBottomSheet(
config = TangemBottomSheetConfig(
@ -187,4 +411,21 @@ private fun SecurityScoreBottomSheetPreview() {
),
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun SecurityScoreBottomSheetPreviewV2() {
TangemThemePreviewRedesign {
CompositionLocalProvider(LocalRedesignEnabled provides true) {
SecurityScoreBottomSheet(
config = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = {},
content = SecurityScorePreviewData.bottomSheetContent,
),
)
}
}
}

View file

@ -22,6 +22,27 @@ import com.tangem.features.feed.ui.market.detailed.state.MarketsTokenDetailsUM.R
@Suppress("CanBeNonNullable")
internal fun LazyListScope.tokenMarketDetailsBody(
isRedesignEnabled: Boolean,
state: MarketsTokenDetailsUM.Body,
portfolioBlock: @Composable ((Modifier) -> Unit)?,
relatedNews: RelatedNews,
) {
if (isRedesignEnabled) {
tokenMarketDetailsBodyV2(
state = state,
relatedNews = relatedNews,
)
} else {
tokenMarketDetailsBodyV1(
state = state,
portfolioBlock = portfolioBlock,
relatedNews = relatedNews,
)
}
}
@Suppress("CanBeNonNullable")
private fun LazyListScope.tokenMarketDetailsBodyV1(
state: MarketsTokenDetailsUM.Body,
portfolioBlock: @Composable ((Modifier) -> Unit)?,
relatedNews: RelatedNews,
@ -40,7 +61,7 @@ internal fun LazyListScope.tokenMarketDetailsBody(
aboutCoinHeader()
loadingInfoBlocks()
loadingInfoBlocks(false)
}
is MarketsTokenDetailsUM.Body.Content -> {
if (state.description != null) {
@ -59,7 +80,36 @@ internal fun LazyListScope.tokenMarketDetailsBody(
aboutCoinHeader()
infoBlocksList(state.infoBlocks)
infoBlocksListV1(state = state.infoBlocks)
}
is MarketsTokenDetailsUM.Body.Error -> {
error(state)
}
MarketsTokenDetailsUM.Body.Nothing -> {
// Do nothing
}
}
}
@Suppress("CanBeNonNullable")
private fun LazyListScope.tokenMarketDetailsBodyV2(state: MarketsTokenDetailsUM.Body, relatedNews: RelatedNews) {
when (state) {
MarketsTokenDetailsUM.Body.Loading -> {
item("description-loading") {
DescriptionPlaceholder(modifier = Modifier.blockPaddings())
}
loadingInfoBlocks(true)
}
is MarketsTokenDetailsUM.Body.Content -> {
if (state.description != null) {
description(state.description)
}
infoBlocksListV2(
state = state.infoBlocks,
relatedNews = relatedNews,
)
}
is MarketsTokenDetailsUM.Body.Error -> {
error(state)
@ -112,7 +162,7 @@ private fun LazyListScope.description(description: MarketsTokenDetailsUM.Descrip
}
}
internal fun LazyListScope.infoBlocksList(state: MarketsTokenDetailsUM.InformationBlocks) {
internal fun LazyListScope.infoBlocksListV1(state: MarketsTokenDetailsUM.InformationBlocks) {
if (state.insights != null) {
item("insights") {
InsightsBlock(
@ -166,7 +216,64 @@ internal fun LazyListScope.infoBlocksList(state: MarketsTokenDetailsUM.Informati
}
}
private fun LazyListScope.loadingInfoBlocks() {
internal fun LazyListScope.infoBlocksListV2(state: MarketsTokenDetailsUM.InformationBlocks, relatedNews: RelatedNews) {
if (state.metrics != null) {
item("metrics") {
MetricsBlock(
modifier = Modifier.blockPaddings(),
state = state.metrics,
)
}
}
if (state.insights != null) {
item("insights") {
InsightsBlock(
modifier = Modifier.blockPaddings(),
state = state.insights,
)
}
}
item(key = "listedOn") {
ListedOnBlock(
state = state.listedOn,
modifier = Modifier.blockPaddings(),
)
}
if (relatedNews.articles.isNotEmpty()) {
relatedNews(relatedNews)
}
if (state.securityScore != null) {
item("securityScore") {
SecurityScoreBlock(
modifier = Modifier.blockPaddings(),
state = state.securityScore,
)
}
}
if (state.links != null) {
item("links") {
LinksBlock(
modifier = Modifier.blockPaddings(),
state = state.links,
)
}
}
}
private fun LazyListScope.loadingInfoBlocks(isRedesignEnabled: Boolean) {
if (isRedesignEnabled) {
loadingInfoBlocksV2()
} else {
loadingInfoBlocksV1()
}
}
private fun LazyListScope.loadingInfoBlocksV1() {
item("insights-loading") {
InsightsBlockPlaceholder(
modifier = Modifier.blockPaddings(),
@ -194,6 +301,28 @@ private fun LazyListScope.loadingInfoBlocks() {
}
}
private fun LazyListScope.loadingInfoBlocksV2() {
item("metrics-loading") {
MetricsBlockPlaceholder(modifier = Modifier.blockPaddings())
}
item("insights-loading") {
InsightsBlockPlaceholder(modifier = Modifier.blockPaddings())
}
item(key = "listedOn-loading") {
ListedOnBlockPlaceholder(modifier = Modifier.blockPaddings())
}
item("securityScore-loading") {
SecurityScoreBlockPlaceholder(modifier = Modifier.blockPaddings())
}
item("links-loading") {
LinksBlockPlaceholder(modifier = Modifier.blockPaddings())
}
}
private fun LazyListScope.relatedNews(relatedNews: RelatedNews) {
item("related-news") {
Column(

View file

@ -104,6 +104,7 @@ internal object MarketsTokenDetailsPreview {
infoPoint,
infoPoint,
),
metricsV2 = null,
),
pricePerformance = PricePerformanceUM(
h24 = PricePerformanceUM.Value(

View file

@ -0,0 +1,23 @@
package com.tangem.features.feed.ui.market.detailed.state
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.audits.AuditLabelUM
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.extensions.TextReference
@Immutable
internal sealed interface ExchangeItemUM {
val id: String
data class Content(
override val id: String,
val title: TextReference,
val subTitle: TextReference,
val icon: TangemIconUM,
val volumeInUsd: TextReference,
val auditLabel: AuditLabelUM,
) : ExchangeItemUM
data class Loading(override val id: String) : ExchangeItemUM
}

View file

@ -1,6 +1,7 @@
package com.tangem.features.feed.ui.market.detailed.state
import androidx.annotation.StringRes
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.extensions.TextReference
@ -18,6 +19,7 @@ import kotlinx.collections.immutable.toImmutableList
*
[REDACTED_AUTHOR]
*/
@Immutable
internal sealed interface ExchangesBottomSheetContent : TangemBottomSheetConfigContent {
/** Title of bottom sheet. Like, app bar. */
@ -39,6 +41,8 @@ internal sealed interface ExchangesBottomSheetContent : TangemBottomSheetConfigC
/** Exchange items */
val exchangeItems: ImmutableList<TokenItemState>
val exchangeItemsV2: ImmutableList<ExchangeItemUM>
/**
* Loading state
*
@ -49,6 +53,10 @@ internal sealed interface ExchangesBottomSheetContent : TangemBottomSheetConfigC
override val exchangeItems: ImmutableList<TokenItemState>
get() = List(size = exchangesCount) { index -> TokenItemState.Loading(id = "loading#$index") }
.toImmutableList()
override val exchangeItemsV2: ImmutableList<ExchangeItemUM>
get() = List(size = exchangesCount) { index -> ExchangeItemUM.Loading(id = "loading#$index") }
.toImmutableList()
}
/**
@ -56,8 +64,14 @@ internal sealed interface ExchangesBottomSheetContent : TangemBottomSheetConfigC
*
* @property exchangeItems exchanges
*/
data class Content(
data class ContentV1(
override val exchangeItems: ImmutableList<TokenItemState>,
override val exchangeItemsV2: ImmutableList<ExchangeItemUM> = persistentListOf(),
) : ExchangesBottomSheetContent
data class ContentV2(
override val exchangeItemsV2: ImmutableList<ExchangeItemUM>,
override val exchangeItems: ImmutableList<TokenItemState> = persistentListOf(),
) : ExchangesBottomSheetContent
/** Error state */
@ -65,6 +79,7 @@ internal sealed interface ExchangesBottomSheetContent : TangemBottomSheetConfigC
val onRetryClick: () -> Unit,
) : ExchangesBottomSheetContent {
override val exchangeItems: ImmutableList<TokenItemState> = persistentListOf()
override val exchangeItemsV2: ImmutableList<ExchangeItemUM> = persistentListOf()
@StringRes
val message: Int = R.string.markets_loading_error_title

View file

@ -1,5 +1,6 @@
package com.tangem.features.feed.ui.market.detailed.state
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.pluralReference
import com.tangem.core.ui.extensions.resourceReference
@ -11,6 +12,7 @@ import com.tangem.features.feed.impl.R
*
[REDACTED_AUTHOR]
*/
@Immutable
internal sealed interface ListedOnUM {
/** Title */
@ -20,6 +22,7 @@ internal sealed interface ListedOnUM {
/** Description */
val description: TextReference
@Immutable
/** Empty state. No exchanges found */
data object Empty : ListedOnUM {
override val description = resourceReference(id = R.string.markets_token_details_empty_exchanges)
@ -31,6 +34,7 @@ internal sealed interface ListedOnUM {
* @property onClick lambda be invoked when button is clicked
* @property amount amount of exchanges
*/
@Immutable
data class Content(
val onClick: () -> Unit,
private val amount: Int,

View file

@ -1,7 +1,68 @@
package com.tangem.features.feed.ui.market.detailed.state
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
internal data class MetricsUM(
val metrics: ImmutableList<InfoPointUM>,
)
val metricsV2: MetricsV2UM?,
)
@Immutable
internal sealed interface InfoPointUMV2 {
@Immutable
data class MarketCap(
val capitalizationValue: TextReference,
val onInfoClick: () -> Unit,
) : InfoPointUMV2
@Immutable
data class TradingVolume(
val tradingValue: TextReference,
val liquidity: Float,
val trendingVolumeLiquidityType: TrendingVolumeLiquidityType,
val onInfoClick: () -> Unit,
) : InfoPointUMV2
@Immutable
data class MarketPosition(
val position: String,
val rangeValue: Float,
val marketRatingType: MarketRatingType,
val onInfoClick: () -> Unit,
) : InfoPointUMV2
@Immutable
data class FullyDilutedValuation(
val value: TextReference,
val onInfoClick: () -> Unit,
) : InfoPointUMV2
@Immutable
data class CirculatingSupply(
val currentValue: TextReference,
val maxValue: TextReference?,
val fillValue: Float?,
val onInfoClick: () -> Unit,
) : InfoPointUMV2
}
internal data class MetricsV2UM(
val rows: ImmutableList<Row>,
) {
internal data class Row(
val first: InfoPointUMV2,
val second: InfoPointUMV2?,
)
}
internal enum class TrendingVolumeLiquidityType {
HIGH, MEDIUM, LOW,
}
internal enum class MarketRatingType {
GOLD, SILVER, BRONZE, OTHER
}

View file

@ -15,6 +15,8 @@ import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.UnableToLoadData
import com.tangem.core.ui.components.haze.hazeSourceTangem
import com.tangem.core.ui.components.pager.PagerIndicator
import com.tangem.core.ui.ds.TangemPagerIndicator
import com.tangem.core.ui.ds.TangemPagerIndicatorColors
import com.tangem.core.ui.extensions.conditionalCompose
import com.tangem.core.ui.res.*
import com.tangem.features.feed.ui.news.details.components.ArticleDetail
@ -105,12 +107,24 @@ private fun Content(state: NewsDetailsUM, background: Color) {
)
}
if (state.articles.size > 1) {
PagerIndicator(
pagerState = pagerState,
modifier = Modifier
.align(Alignment.BottomCenter)
.windowInsetsPadding(WindowInsets.navigationBars),
)
if (isRedesignEnabled) {
TangemPagerIndicator(
pagerState = pagerState,
modifier = Modifier
.align(Alignment.BottomCenter)
.windowInsetsPadding(WindowInsets.navigationBars),
colors = TangemPagerIndicatorColors.copy(
overlay = TangemTheme.colors2.tabs.backgroundSecondary.copy(alpha = .1f),
),
)
} else {
PagerIndicator(
pagerState = pagerState,
modifier = Modifier
.align(Alignment.BottomCenter)
.windowInsetsPadding(WindowInsets.navigationBars),
)
}
}
}
}

View file

@ -28,6 +28,7 @@ internal fun RelatedTokensBlock(
onItemClick: ((MarketsListItemUM) -> Unit)?,
modifier: Modifier = Modifier,
) {
val isRedesignEnabled = LocalRedesignEnabled.current
val isVisible = remember(relatedTokensUM) {
when (relatedTokensUM) {
is RelatedTokensUM.Content -> relatedTokensUM.items.isNotEmpty()
@ -40,7 +41,7 @@ internal fun RelatedTokensBlock(
Column(modifier = modifier) {
SpacerH(40.dp)
if (LocalRedesignEnabled.current) {
if (isRedesignEnabled) {
Text(
modifier = Modifier.padding(horizontal = 8.dp),
text = stringResourceSafe(R.string.news_related_tokens),
@ -58,7 +59,11 @@ internal fun RelatedTokensBlock(
BlockCard(
colors = TangemBlockCardColors.copy(
containerColor = TangemTheme.colors.background.action,
containerColor = if (isRedesignEnabled) {
TangemTheme.colors2.surface.level3
} else {
TangemTheme.colors.background.action
},
),
) {
Column(modifier = Modifier.fillMaxWidth()) {