Updated on 2026-08-14
This commit is contained in:
commit
bfd903b43c
595 changed files with 12755 additions and 19837 deletions
|
|
@ -2,9 +2,12 @@ package com.tangem.features.feed.components.market.details.portfolio.impl.model
|
|||
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.ui.bottomsheet.receive.mapToAddressModels
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.event.OfframpAnalyticsEvent
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.core.ui.clipboard.ClipboardManager
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.message.DialogMessage
|
||||
|
|
@ -12,9 +15,8 @@ import com.tangem.core.ui.message.SnackbarMessage
|
|||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.demo.IsDemoCardUseCase
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.offramp.GetOfframpUrlUseCase
|
||||
import com.tangem.domain.onramp.model.OnrampSource
|
||||
import com.tangem.domain.redux.ReduxStateHolder
|
||||
import com.tangem.domain.tokens.legacy.TradeCryptoAction
|
||||
import com.tangem.domain.tokens.model.TokenActionsState
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.loader.PortfolioData
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.TokenActionsBSContentUM
|
||||
|
|
@ -30,7 +32,9 @@ internal class TokenActionsHandler @AssistedInject constructor(
|
|||
private val router: Router,
|
||||
private val clipboardManager: ClipboardManager,
|
||||
private val uiMessageSender: UiMessageSender,
|
||||
private val reduxStateHolder: ReduxStateHolder,
|
||||
private val getOfframpUrlUseCase: GetOfframpUrlUseCase,
|
||||
private val urlOpener: UrlOpener,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
@Assisted private val currentAppCurrency: Provider<AppCurrency>,
|
||||
@Assisted private val onHandleQuickAction: (HandledQuickAction) -> Unit,
|
||||
private val isDemoCardUseCase: IsDemoCardUseCase,
|
||||
|
|
@ -104,12 +108,13 @@ internal class TokenActionsHandler @AssistedInject constructor(
|
|||
}
|
||||
|
||||
private fun onSellClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) {
|
||||
reduxStateHolder.dispatch(
|
||||
TradeCryptoAction.Sell(
|
||||
cryptoCurrencyStatus = cryptoCurrencyData.status,
|
||||
appCurrencyCode = currentAppCurrency().code,
|
||||
),
|
||||
)
|
||||
getOfframpUrlUseCase(
|
||||
cryptoCurrencyStatus = cryptoCurrencyData.status,
|
||||
appCurrencyCode = currentAppCurrency().code,
|
||||
).onRight { url ->
|
||||
urlOpener.openUrl(url)
|
||||
analyticsEventHandler.send(OfframpAnalyticsEvent.ScreenOpened)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onExchangeClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) {
|
||||
|
|
|
|||
|
|
@ -7,9 +7,6 @@ internal class DefaultFeedFeatureToggle(
|
|||
private val featureTogglesManager: FeatureTogglesManager,
|
||||
) : FeedFeatureToggle {
|
||||
|
||||
override val isFeedEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled("FEED_ENABLED")
|
||||
|
||||
override val isEarnBlockEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled("EARN_BLOCK_ENABLED")
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.features.feed.model.converter
|
||||
|
||||
import com.tangem.common.ui.news.ArticleConfigUM
|
||||
import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM
|
||||
import com.tangem.core.ui.components.label.entity.LabelLeadingContentUM
|
||||
import com.tangem.core.ui.components.label.entity.LabelUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
|
|
|||
|
|
@ -32,12 +32,10 @@ import com.tangem.features.feed.model.earn.filters.state.EarnFilterNetworkUMConv
|
|||
import com.tangem.features.feed.model.earn.filters.state.EarnFilterTypeConverter
|
||||
import com.tangem.features.feed.model.earn.filters.state.EarnFilterTypeUMConverter
|
||||
import com.tangem.features.feed.model.earn.state.EarnStateController
|
||||
import com.tangem.features.feed.model.earn.state.transformers.EarnFilterSelectedStateTransformer
|
||||
import com.tangem.features.feed.model.earn.state.transformers.UpdateBestOpportunitiesStateTransformer
|
||||
import com.tangem.features.feed.model.earn.state.transformers.UpdateEarnUMInitialStateTransformer
|
||||
import com.tangem.features.feed.model.earn.state.transformers.UpdateMostlyUsedStateTransformer
|
||||
import com.tangem.features.feed.model.earn.state.transformers.*
|
||||
import com.tangem.features.feed.model.earn.statemanager.EarnListBatchFlowManager
|
||||
import com.tangem.features.feed.model.earn.statemanager.EarnListStateManager
|
||||
import com.tangem.features.feed.ui.earn.state.EarnBestOpportunitiesUM
|
||||
import com.tangem.features.feed.ui.earn.state.EarnFilterNetworkUM
|
||||
import com.tangem.features.feed.ui.earn.state.EarnFilterTypeUM
|
||||
import com.tangem.features.feed.ui.earn.state.EarnUM
|
||||
|
|
@ -69,8 +67,8 @@ internal class EarnModel @Inject constructor(
|
|||
private val earnNetworks = MutableStateFlow<EarnNetworks>(Either.Right(emptyList()))
|
||||
private val earnListConfigProvider = Provider {
|
||||
createEarnTokensListConfig(
|
||||
selectedTypeFilter = stateController.value.selectedTypeFilter,
|
||||
selectedNetworkFilter = stateController.value.selectedNetworkFilter,
|
||||
selectedTypeFilter = stateController.value.earnFilterUM.selectedTypeFilter,
|
||||
selectedNetworkFilter = stateController.value.earnFilterUM.selectedNetworkFilter,
|
||||
earnNetworks = earnNetworks.value,
|
||||
)
|
||||
}
|
||||
|
|
@ -104,7 +102,6 @@ internal class EarnModel @Inject constructor(
|
|||
init {
|
||||
updateInitialState()
|
||||
fetchEarnNetworks()
|
||||
fetchTopEarnTokens()
|
||||
subscribeOnStoredFilters()
|
||||
subscribeOnNetworks()
|
||||
subscribeOnBatchFlow()
|
||||
|
|
@ -117,19 +114,22 @@ internal class EarnModel @Inject constructor(
|
|||
batchFlowManager.initialLoadingError,
|
||||
batchFlowManager.paginationStatus,
|
||||
) { items, error, paginationStatus ->
|
||||
val hasActiveFilters = state.value.selectedTypeFilter != EarnFilterTypeUM.All ||
|
||||
state.value.selectedNetworkFilter !is EarnFilterNetworkUM.AllNetworks
|
||||
error?.let(::handleBestOpportunitiesErrorAnalytics)
|
||||
val hasActiveFilters = state.value.earnFilterUM.selectedTypeFilter != EarnFilterTypeUM.All ||
|
||||
state.value.earnFilterUM.selectedNetworkFilter !is EarnFilterNetworkUM.AllNetworks
|
||||
EarnListStateManager.calculateState(
|
||||
items = items,
|
||||
error = error,
|
||||
paginationStatus = paginationStatus,
|
||||
hasActiveFilters = hasActiveFilters,
|
||||
onRetryClick = { batchFlowManager.reload() },
|
||||
onRetryClick = {
|
||||
batchFlowManager.reload()
|
||||
reloadEarnNetworks()
|
||||
},
|
||||
onLoadMore = { batchFlowManager.loadMore() },
|
||||
onClearFiltersClick = ::onClearFiltersClick,
|
||||
)
|
||||
}.onEach { bestOpportunitiesState ->
|
||||
) to error
|
||||
}.onEach { (bestOpportunitiesState, error) ->
|
||||
error?.let(::handleBestOpportunitiesErrorAnalytics)
|
||||
stateController.update(UpdateBestOpportunitiesStateTransformer(bestOpportunitiesState))
|
||||
}.launchIn(modelScope)
|
||||
}
|
||||
|
|
@ -156,23 +156,27 @@ internal class EarnModel @Inject constructor(
|
|||
|
||||
private fun subscribeOnStoredFilters() {
|
||||
modelScope.launch(dispatchers.default) {
|
||||
getEarnFilterUseCase()
|
||||
.collect { filter ->
|
||||
val typeFilterUM = EarnFilterTypeConverter().convert(filter.earnFilterType)
|
||||
val networkFilterUM = EarnFilterNetworkConverter().convert(filter.earnFilterNetwork)
|
||||
stateController.update(
|
||||
EarnFilterSelectedStateTransformer(
|
||||
filterType = typeFilterUM,
|
||||
filterNetwork = networkFilterUM,
|
||||
),
|
||||
)
|
||||
batchFlowManager.reload()
|
||||
}
|
||||
combine(
|
||||
getEarnFilterUseCase(),
|
||||
earnNetworks,
|
||||
) { filter, networks ->
|
||||
val typeFilterUM = EarnFilterTypeConverter().convert(filter.earnFilterType)
|
||||
val networkFilterUM = EarnFilterNetworkConverter().convert(filter.earnFilterNetwork)
|
||||
stateController.update(
|
||||
EarnFilterSelectedStateTransformer(
|
||||
filterType = typeFilterUM,
|
||||
filterNetwork = networkFilterUM,
|
||||
earnNetworks = networks,
|
||||
),
|
||||
)
|
||||
batchFlowManager.reload()
|
||||
}.collect()
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchTopEarnTokens() {
|
||||
modelScope.launch(dispatchers.default) {
|
||||
stateController.update(UpdateMostlyUsedStateLoadingTransformer())
|
||||
fetchTopEarnTokensUseCase()
|
||||
}
|
||||
}
|
||||
|
|
@ -183,13 +187,21 @@ internal class EarnModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun reloadEarnNetworks() {
|
||||
modelScope.launch(dispatchers.default) {
|
||||
if (earnNetworks.value.isLeft()) {
|
||||
fetchEarnNetworks()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* start of clicks area */
|
||||
private fun onTypeFilterClick() {
|
||||
val currentState = state.value
|
||||
bottomSheetNavigation.activate(
|
||||
FeedBottomSheetRoute.TypeFilter(
|
||||
params = EarnTypeFilterComponent.Params(
|
||||
selectedFilter = EarnFilterTypeUMConverter().convert(currentState.selectedTypeFilter),
|
||||
selectedFilter = EarnFilterTypeUMConverter().convert(currentState.earnFilterUM.selectedTypeFilter),
|
||||
onFilterSelected = ::onTypeFilterOptionSelected,
|
||||
onDismiss = { bottomSheetNavigation.dismiss() },
|
||||
),
|
||||
|
|
@ -210,7 +222,7 @@ internal class EarnModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun createNetworkFilters(): List<EarnFilterNetwork> {
|
||||
val selectedFilter = state.value.selectedNetworkFilter
|
||||
val selectedFilter = state.value.earnFilterUM.selectedNetworkFilter
|
||||
return buildList {
|
||||
add(
|
||||
EarnFilterNetwork.AllNetworks(
|
||||
|
|
@ -277,11 +289,14 @@ internal class EarnModel @Inject constructor(
|
|||
modelScope.launch(dispatchers.default) {
|
||||
setEarnFilterUseCase(
|
||||
EarnFilter(
|
||||
earnFilterNetwork = EarnFilterNetworkUMConverter().convert(state.value.selectedNetworkFilter),
|
||||
earnFilterNetwork = EarnFilterNetworkUMConverter().convert(
|
||||
value = state.value.earnFilterUM.selectedNetworkFilter,
|
||||
),
|
||||
earnFilterType = type,
|
||||
),
|
||||
)
|
||||
bottomSheetNavigation.dismiss()
|
||||
reloadEarnNetworks()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -290,7 +305,7 @@ internal class EarnModel @Inject constructor(
|
|||
setEarnFilterUseCase(
|
||||
EarnFilter(
|
||||
earnFilterNetwork = filter,
|
||||
earnFilterType = EarnFilterTypeUMConverter().convert(state.value.selectedTypeFilter),
|
||||
earnFilterType = EarnFilterTypeUMConverter().convert(state.value.earnFilterUM.selectedTypeFilter),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -319,11 +334,13 @@ internal class EarnModel @Inject constructor(
|
|||
is ApiResponseError.HttpException -> error.code.numericCode to error.message.orEmpty()
|
||||
else -> null to ""
|
||||
}
|
||||
analyticsEventHandler.send(
|
||||
EarnAnalyticsEvent.BestOpportunitiesLoadError(
|
||||
code = code,
|
||||
message = message,
|
||||
),
|
||||
)
|
||||
if (state.value.bestOpportunities !is EarnBestOpportunitiesUM.Error) {
|
||||
analyticsEventHandler.send(
|
||||
EarnAnalyticsEvent.BestOpportunitiesLoadError(
|
||||
code = code,
|
||||
message = message,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -23,7 +23,9 @@ internal fun createEarnTokensListConfig(
|
|||
earnNetworks.fold(
|
||||
ifLeft = { null },
|
||||
ifRight = { networks ->
|
||||
networks.filter(EarnNetwork::isAdded).map(EarnNetwork::networkId)
|
||||
networks.filter(EarnNetwork::isAdded)
|
||||
.map(EarnNetwork::networkId)
|
||||
.ifEmpty { listOf(NO_ONE_NETWORK) }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -34,4 +36,9 @@ internal fun createEarnTokensListConfig(
|
|||
networks = networks,
|
||||
isForEarn = isForEarn,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This id means that backend has to return empty result
|
||||
*/
|
||||
private const val NO_ONE_NETWORK = "-1"
|
||||
|
|
@ -26,7 +26,7 @@ internal sealed class EarnAnalyticsEvent(
|
|||
event = "Best Opportunities Filter Network Applied",
|
||||
params = mapOf(
|
||||
"Network Filter Type" to filterType.value,
|
||||
"NetworkId" to networkId.orEmpty(),
|
||||
"Network Id" to networkId.orEmpty(),
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -26,8 +26,10 @@ internal class EarnStateController @Inject constructor() {
|
|||
return EarnUM(
|
||||
mostlyUsed = EarnListUM.Loading,
|
||||
bestOpportunities = EarnBestOpportunitiesUM.Loading,
|
||||
selectedTypeFilter = EarnFilterTypeUM.All,
|
||||
selectedNetworkFilter = EarnFilterNetworkUM.AllNetworks(isSelected = true),
|
||||
earnFilterUM = EarnFilterUM(
|
||||
selectedTypeFilter = EarnFilterTypeUM.All,
|
||||
selectedNetworkFilter = EarnFilterNetworkUM.AllNetworks(isSelected = true),
|
||||
),
|
||||
onBackClick = {},
|
||||
onNetworkFilterClick = {},
|
||||
onTypeFilterClick = {},
|
||||
|
|
|
|||
|
|
@ -1,18 +1,24 @@
|
|||
package com.tangem.features.feed.model.earn.state.transformers
|
||||
|
||||
import com.tangem.domain.models.earn.EarnNetworks
|
||||
import com.tangem.features.feed.ui.earn.state.EarnFilterNetworkUM
|
||||
import com.tangem.features.feed.ui.earn.state.EarnFilterTypeUM
|
||||
import com.tangem.features.feed.ui.earn.state.EarnUM
|
||||
|
||||
internal class EarnFilterSelectedStateTransformer(
|
||||
private val earnNetworks: EarnNetworks,
|
||||
private val filterType: EarnFilterTypeUM,
|
||||
private val filterNetwork: EarnFilterNetworkUM,
|
||||
) : EarnUMTransformer {
|
||||
|
||||
override fun transform(prevState: EarnUM): EarnUM {
|
||||
return prevState.copy(
|
||||
selectedTypeFilter = filterType,
|
||||
selectedNetworkFilter = filterNetwork,
|
||||
earnFilterUM = prevState.earnFilterUM.copy(
|
||||
selectedTypeFilter = filterType,
|
||||
selectedNetworkFilter = filterNetwork,
|
||||
isNetworkFilterEnabled = earnNetworks.isRight(),
|
||||
isTypeFilterEnabled = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
package com.tangem.features.feed.model.earn.state.transformers
|
||||
|
||||
import com.tangem.features.feed.ui.earn.state.EarnFilterNetworkUM
|
||||
import com.tangem.features.feed.ui.earn.state.EarnUM
|
||||
|
||||
internal class EarnNetworkFilterSelectedStateTransformer(
|
||||
private val filter: EarnFilterNetworkUM,
|
||||
) : EarnUMTransformer {
|
||||
|
||||
override fun transform(prevState: EarnUM): EarnUM {
|
||||
return prevState.copy(selectedNetworkFilter = filter)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.features.feed.model.earn.state.transformers
|
||||
|
||||
import com.tangem.features.feed.ui.earn.state.EarnListUM
|
||||
import com.tangem.features.feed.ui.earn.state.EarnUM
|
||||
|
||||
internal class UpdateMostlyUsedStateLoadingTransformer : EarnUMTransformer {
|
||||
|
||||
override fun transform(prevState: EarnUM): EarnUM {
|
||||
return prevState.copy(mostlyUsed = EarnListUM.Loading)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.features.feed.model.news.list.statemanager
|
||||
|
||||
import com.tangem.common.ui.news.ArticleConfigUM
|
||||
import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM
|
||||
import com.tangem.domain.models.news.ShortArticle
|
||||
import com.tangem.domain.news.model.NewsListBatchingContext
|
||||
import com.tangem.domain.news.model.NewsListConfig
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.features.feed.model.news.list.statemanager
|
||||
|
||||
import com.tangem.common.ui.news.ArticleConfigUM
|
||||
import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
import com.tangem.features.feed.model.news.list.analytics.NewsListAnalyticsEvent
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package com.tangem.features.feed.ui.earn
|
|||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.*
|
||||
import androidx.compose.material3.Icon
|
||||
|
|
@ -14,21 +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.draw.clip
|
||||
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.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.*
|
||||
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.currency.icon.CurrencyIcon
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.components.list.InfiniteListHandler
|
||||
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
|
||||
|
|
@ -39,6 +38,7 @@ 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.features.feed.ui.earn.state.*
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
|
@ -91,12 +91,7 @@ internal fun EarnContent(state: EarnUM, modifier: Modifier = Modifier) {
|
|||
SpacerH(12.dp)
|
||||
BestOpportunitiesFilters(
|
||||
state = state.bestOpportunities,
|
||||
selectedNetworkFilterText = when (state.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(state.selectedNetworkFilter.text)
|
||||
},
|
||||
selectedTypeFilterText = state.selectedTypeFilterText,
|
||||
earnFilterUM = state.earnFilterUM,
|
||||
onNetworkFilterClick = state.onNetworkFilterClick,
|
||||
onTypeFilterClick = state.onTypeFilterClick,
|
||||
)
|
||||
|
|
@ -151,10 +146,12 @@ private fun MostlyUsedContent(state: EarnListUM, onScroll: () -> Unit) {
|
|||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
horizontal = 16.dp,
|
||||
vertical = 12.dp,
|
||||
),
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp)
|
||||
.background(
|
||||
color = TangemTheme.colors.background.action,
|
||||
shape = TangemTheme.shapes.roundedCornersXMedium,
|
||||
)
|
||||
.padding(vertical = 32.dp, horizontal = 12.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
UnableToLoadData(onRetryClick = st.onRetryClicked)
|
||||
|
|
@ -164,71 +161,17 @@ private fun MostlyUsedContent(state: EarnListUM, onScroll: () -> Unit) {
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MostlyUsedCard(item: EarnListItemUM, onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.width(148.dp)
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.clickable(onClick = onClick)
|
||||
.padding(12.dp),
|
||||
) {
|
||||
CurrencyIcon(
|
||||
modifier = Modifier.size(32.dp),
|
||||
state = item.currencyIconState,
|
||||
shouldDisplayNetwork = true,
|
||||
networkBadgeSize = 12.dp,
|
||||
networkBadgeBackground = TangemTheme.colors.background.action,
|
||||
)
|
||||
|
||||
SpacerH(8.dp)
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.weight(weight = 1f, fill = false),
|
||||
text = item.tokenName.resolveReference(),
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
SpacerW(4.dp)
|
||||
Text(
|
||||
text = item.symbol.resolveReference(),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
|
||||
SpacerH(2.dp)
|
||||
|
||||
Text(
|
||||
text = item.earnValue.resolveReference(),
|
||||
color = TangemTheme.colors.text.accent,
|
||||
style = TangemTheme.typography.caption1,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BestOpportunitiesFilters(
|
||||
state: EarnBestOpportunitiesUM,
|
||||
selectedNetworkFilterText: TextReference,
|
||||
selectedTypeFilterText: TextReference,
|
||||
earnFilterUM: EarnFilterUM,
|
||||
onNetworkFilterClick: () -> Unit,
|
||||
onTypeFilterClick: () -> Unit,
|
||||
) {
|
||||
when (state) {
|
||||
is EarnBestOpportunitiesUM.Loading -> FilterButtonsShimmer()
|
||||
else -> FilterButtons(
|
||||
selectedNetworkFilterText = selectedNetworkFilterText,
|
||||
selectedTypeFilterText = selectedTypeFilterText,
|
||||
isEnabled = state is EarnBestOpportunitiesUM.Content || state is EarnBestOpportunitiesUM.EmptyFiltered,
|
||||
earnFilterUM = earnFilterUM,
|
||||
onNetworkFilterClick = onNetworkFilterClick,
|
||||
onTypeFilterClick = onTypeFilterClick,
|
||||
)
|
||||
|
|
@ -307,9 +250,7 @@ private fun LazyListScope.bestOpportunitiesItems(state: EarnBestOpportunitiesUM)
|
|||
|
||||
@Composable
|
||||
private fun FilterButtons(
|
||||
selectedNetworkFilterText: TextReference,
|
||||
selectedTypeFilterText: TextReference,
|
||||
isEnabled: Boolean,
|
||||
earnFilterUM: EarnFilterUM,
|
||||
onNetworkFilterClick: () -> Unit,
|
||||
onTypeFilterClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
|
|
@ -319,10 +260,14 @@ private fun FilterButtons(
|
|||
) {
|
||||
SecondarySmallButton(
|
||||
config = SmallButtonConfig(
|
||||
text = selectedNetworkFilterText,
|
||||
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 = isEnabled,
|
||||
isEnabled = earnFilterUM.isNetworkFilterEnabled,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -330,10 +275,10 @@ private fun FilterButtons(
|
|||
|
||||
SecondarySmallButton(
|
||||
config = SmallButtonConfig(
|
||||
text = selectedTypeFilterText,
|
||||
text = earnFilterUM.selectedTypeFilter.text,
|
||||
onClick = onTypeFilterClick,
|
||||
icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_chevron_24),
|
||||
isEnabled = isEnabled,
|
||||
isEnabled = earnFilterUM.isTypeFilterEnabled,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -496,7 +441,7 @@ private fun EarnContentLoadingPreview() {
|
|||
) {
|
||||
EarnContent(
|
||||
state = previewEarnUM(
|
||||
mostlyUsed = EarnListUM.Loading,
|
||||
mostlyUsed = EarnListUM.Error(onRetryClicked = {}),
|
||||
bestOpportunities = EarnBestOpportunitiesUM.Loading,
|
||||
),
|
||||
)
|
||||
|
|
@ -588,8 +533,12 @@ private fun previewEarnUM(
|
|||
): EarnUM = EarnUM(
|
||||
mostlyUsed = mostlyUsed,
|
||||
bestOpportunities = bestOpportunities,
|
||||
selectedTypeFilter = EarnFilterTypeUM.All,
|
||||
selectedNetworkFilter = EarnFilterNetworkUM.AllNetworks(isSelected = true),
|
||||
earnFilterUM = EarnFilterUM(
|
||||
selectedTypeFilter = EarnFilterTypeUM.All,
|
||||
selectedNetworkFilter = EarnFilterNetworkUM.AllNetworks(isSelected = true),
|
||||
isTypeFilterEnabled = true,
|
||||
isNetworkFilterEnabled = true,
|
||||
),
|
||||
onBackClick = {},
|
||||
onNetworkFilterClick = {},
|
||||
onTypeFilterClick = {},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,198 @@
|
|||
package com.tangem.features.feed.ui.earn.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
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.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.SpacerH
|
||||
import com.tangem.core.ui.components.SpacerW
|
||||
import com.tangem.core.ui.ds.opportunities.OpportunitiesBG
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.ds.image.TangemIconUM
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.res.*
|
||||
import com.tangem.features.feed.ui.earn.state.EarnListItemUM
|
||||
|
||||
@Composable
|
||||
internal fun MostlyUsedCard(item: EarnListItemUM, onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
val isRedesignEnabled = LocalRedesignEnabled.current
|
||||
|
||||
if (isRedesignEnabled) {
|
||||
MostlyUsedCardV2(
|
||||
modifier = modifier,
|
||||
item = item,
|
||||
onClick = onClick,
|
||||
)
|
||||
} else {
|
||||
MostlyUsedCardV1(
|
||||
modifier = modifier,
|
||||
item = item,
|
||||
onClick = onClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MostlyUsedCardV2(item: EarnListItemUM, onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
OpportunitiesBG(
|
||||
modifier = modifier
|
||||
.width(148.dp)
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.clickable(onClick = onClick),
|
||||
icon = TangemIconUM.Currency(item.currencyIconState),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(12.dp)) {
|
||||
CurrencyIcon(
|
||||
modifier = Modifier.size(32.dp),
|
||||
state = item.currencyIconState,
|
||||
shouldDisplayNetwork = true,
|
||||
networkBadgeSize = 12.dp,
|
||||
networkBadgeBackground = TangemTheme.colors.background.action,
|
||||
)
|
||||
|
||||
SpacerH(22.dp)
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.Bottom,
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.weight(weight = 1f, fill = false),
|
||||
text = item.tokenName.resolveReference(),
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography2.bodySemibold16,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
SpacerW(4.dp)
|
||||
Text(
|
||||
text = item.symbol.resolveReference(),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography2.captionSemibold12,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
|
||||
SpacerH(2.dp)
|
||||
|
||||
Text(
|
||||
text = item.earnValue.resolveReference(),
|
||||
color = TangemTheme.colors2.text.status.positive,
|
||||
style = TangemTheme.typography2.captionSemibold12,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MostlyUsedCardV1(item: EarnListItemUM, onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.width(148.dp)
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.clickable(onClick = onClick)
|
||||
.padding(12.dp),
|
||||
) {
|
||||
CurrencyIcon(
|
||||
modifier = Modifier.size(32.dp),
|
||||
state = item.currencyIconState,
|
||||
shouldDisplayNetwork = true,
|
||||
networkBadgeSize = 12.dp,
|
||||
networkBadgeBackground = TangemTheme.colors.background.action,
|
||||
)
|
||||
|
||||
SpacerH(8.dp)
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.weight(weight = 1f, fill = false),
|
||||
text = item.tokenName.resolveReference(),
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
SpacerW(4.dp)
|
||||
Text(
|
||||
text = item.symbol.resolveReference(),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
|
||||
SpacerH(2.dp)
|
||||
|
||||
Text(
|
||||
text = item.earnValue.resolveReference(),
|
||||
color = TangemTheme.colors.text.accent,
|
||||
style = TangemTheme.typography.caption1,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Composable
|
||||
private fun EarnListItemPreviewV1() {
|
||||
TangemThemePreview {
|
||||
MostlyUsedCardV1(
|
||||
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 6.54%"),
|
||||
earnType = stringReference("Yield"),
|
||||
onItemClick = {},
|
||||
),
|
||||
onClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Composable
|
||||
private fun EarnListItemPreviewV2() {
|
||||
TangemThemePreviewRedesign {
|
||||
MostlyUsedCardV2(
|
||||
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 6.54%"),
|
||||
earnType = stringReference("Yield"),
|
||||
onItemClick = {},
|
||||
),
|
||||
onClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.features.feed.ui.earn.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
@Immutable
|
||||
internal data class EarnFilterUM(
|
||||
val selectedTypeFilter: EarnFilterTypeUM,
|
||||
val selectedNetworkFilter: EarnFilterNetworkUM,
|
||||
val isTypeFilterEnabled: Boolean = true,
|
||||
val isNetworkFilterEnabled: Boolean = true,
|
||||
)
|
||||
|
|
@ -1,20 +1,14 @@
|
|||
package com.tangem.features.feed.ui.earn.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
@Immutable
|
||||
internal data class EarnUM(
|
||||
val mostlyUsed: EarnListUM,
|
||||
val bestOpportunities: EarnBestOpportunitiesUM,
|
||||
val selectedTypeFilter: EarnFilterTypeUM,
|
||||
val selectedNetworkFilter: EarnFilterNetworkUM,
|
||||
val earnFilterUM: EarnFilterUM,
|
||||
val onBackClick: () -> Unit,
|
||||
val onNetworkFilterClick: () -> Unit,
|
||||
val onTypeFilterClick: () -> Unit,
|
||||
val onSliderScroll: () -> Unit,
|
||||
) {
|
||||
|
||||
val selectedTypeFilterText: TextReference
|
||||
get() = selectedTypeFilter.text
|
||||
}
|
||||
)
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.features.feed.ui.feed.components
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
|
|
@ -8,12 +9,18 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.components.SpacerW
|
||||
import com.tangem.core.ui.components.buttons.SecondarySmallButton
|
||||
import com.tangem.core.ui.components.buttons.SmallButtonConfig
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
@Composable
|
||||
internal fun Header(onSeeAllClick: () -> Unit, isLoading: Boolean = false, title: @Composable () -> Unit) {
|
||||
internal fun Header(
|
||||
onSeeAllClick: () -> Unit,
|
||||
isLoading: Boolean,
|
||||
shouldShowSeeAll: Boolean,
|
||||
title: @Composable () -> Unit,
|
||||
) {
|
||||
AnimatedContent(isLoading) { animatedState ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
|
|
@ -25,13 +32,18 @@ internal fun Header(onSeeAllClick: () -> Unit, isLoading: Boolean = false, title
|
|||
if (animatedState) {
|
||||
RectangleShimmer(modifier = Modifier.size(width = 104.dp, height = 18.dp))
|
||||
} else {
|
||||
title()
|
||||
SecondarySmallButton(
|
||||
config = SmallButtonConfig(
|
||||
text = TextReference.Res(R.string.common_see_all),
|
||||
onClick = onSeeAllClick,
|
||||
),
|
||||
)
|
||||
Box(modifier = Modifier.weight(1f)) {
|
||||
title()
|
||||
}
|
||||
SpacerW(8.dp)
|
||||
AnimatedVisibility(shouldShowSeeAll) {
|
||||
SecondarySmallButton(
|
||||
config = SmallButtonConfig(
|
||||
text = TextReference.Res(R.string.common_see_all),
|
||||
onClick = onSeeAllClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ 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.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import com.tangem.core.ui.R
|
||||
|
|
@ -33,10 +34,13 @@ internal fun EarnBlock(onSeeAllClick: () -> Unit, earnListUM: EarnListUM?, modif
|
|||
text = stringResourceSafe(R.string.markets_earn_common_title),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
},
|
||||
onSeeAllClick = onSeeAllClick,
|
||||
isLoading = earnListUM is EarnListUM.Loading,
|
||||
shouldShowSeeAll = earnListUM is EarnListUM.Content,
|
||||
)
|
||||
|
||||
SpacerH(12.dp)
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ 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.common.ui.news.DefaultLoadingArticle
|
||||
import com.tangem.common.ui.news.TrendingLoadingArticle
|
||||
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
|
||||
|
|
|
|||
|
|
@ -4,12 +4,7 @@ import androidx.compose.animation.AnimatedContent
|
|||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.togetherWith
|
||||
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.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.foundation.lazy.rememberLazyListState
|
||||
|
|
@ -19,6 +14,7 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import com.tangem.common.ui.markets.MarketsListItem
|
||||
|
|
@ -55,9 +51,13 @@ internal fun MarketBlock(marketChart: MarketChartUM?, feedListCallbacks: FeedLis
|
|||
text = stringResourceSafe(R.string.markets_common_title),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
},
|
||||
onSeeAllClick = { feedListCallbacks.onMarketOpenClick(SortByTypeUM.Rating) },
|
||||
shouldShowSeeAll = currentChart is MarketChartUM.Content,
|
||||
isLoading = currentChart is MarketChartUM.Loading,
|
||||
)
|
||||
|
||||
SpacerH(12.dp)
|
||||
|
|
@ -88,9 +88,13 @@ internal fun MarketPulseBlock(marketChartConfig: MarketChartConfig, feedListCall
|
|||
text = stringResourceSafe(R.string.markets_pulse_common_title),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
},
|
||||
onSeeAllClick = { onSeeAllClick() },
|
||||
shouldShowSeeAll = true,
|
||||
isLoading = marketChartConfig.marketCharts[marketChartConfig.currentSortByType] is MarketChartUM.Loading,
|
||||
)
|
||||
|
||||
LazyRow(
|
||||
|
|
|
|||
|
|
@ -2,28 +2,26 @@ package com.tangem.features.feed.ui.feed.components
|
|||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
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.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.onFirstVisible
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.common.ui.news.ArticleCard
|
||||
import com.tangem.common.ui.news.ArticleConfigUM
|
||||
import com.tangem.common.ui.news.ShowMoreArticlesCard
|
||||
import com.tangem.features.feed.ui.feed.components.articles.ArticleCard
|
||||
import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.SpacerW
|
||||
|
|
@ -31,16 +29,18 @@ 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.feed.state.FeedListCallbacks
|
||||
import com.tangem.features.feed.ui.feed.state.NewsUM
|
||||
import com.tangem.features.feed.ui.feed.state.NewsUMState
|
||||
import com.tangem.features.feed.ui.feed.state.*
|
||||
|
||||
private const val FOURTH_ITEM_INDEX = 3
|
||||
internal const val FOURTH_ITEM_INDEX = 3
|
||||
private const val GRADIENT_START = 0f
|
||||
private const val GRADIENT_END = 0.5f
|
||||
private val LinearGradientFirstPart = Color(0xFF635EEC)
|
||||
private val LinearGradientSecondPart = Color(0xFFE05AED)
|
||||
private const val LINEAR_GRADIENT_FIRST_PART_V2 = 0xFFA3A0FF
|
||||
private const val LINEAR_GRADIENT_SECOND_PART_V2 = 0xFFF79DFF
|
||||
|
||||
private const val LINEAR_GRADIENT_FIRST_PART_V1 = 0xFF635EEC
|
||||
private const val LINEAR_GRADIENT_SECOND_PART_V1 = 0xFFE05AED
|
||||
|
||||
@Composable
|
||||
internal fun NewsBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM, trendingArticle: ArticleConfigUM?) {
|
||||
|
|
@ -64,13 +64,23 @@ internal fun NewsBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM, trend
|
|||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
private fun NewsContentBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM, trendingArticle: ArticleConfigUM?) {
|
||||
val listState = rememberLazyListState()
|
||||
val articlesReadStatus = remember(news.content) {
|
||||
news.content.map { it.isViewed }
|
||||
val isRedesignEnabled = LocalRedesignEnabled.current
|
||||
val gradientStart = remember(isRedesignEnabled) {
|
||||
if (isRedesignEnabled) {
|
||||
Color(LINEAR_GRADIENT_FIRST_PART_V2)
|
||||
} else {
|
||||
Color(LINEAR_GRADIENT_FIRST_PART_V1)
|
||||
}
|
||||
}
|
||||
LaunchedEffect(articlesReadStatus) {
|
||||
listState.requestScrollToItem(0)
|
||||
|
||||
val gradientEnd = remember(isRedesignEnabled) {
|
||||
if (isRedesignEnabled) {
|
||||
Color(LINEAR_GRADIENT_SECOND_PART_V2)
|
||||
} else {
|
||||
Color(LINEAR_GRADIENT_SECOND_PART_V1)
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
Header(
|
||||
title = {
|
||||
|
|
@ -95,8 +105,8 @@ private fun NewsContentBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM,
|
|||
withStyle(
|
||||
SpanStyle().copy(
|
||||
brush = Brush.linearGradient(
|
||||
GRADIENT_START to LinearGradientFirstPart,
|
||||
GRADIENT_END to LinearGradientSecondPart,
|
||||
GRADIENT_START to gradientStart,
|
||||
GRADIENT_END to gradientEnd,
|
||||
),
|
||||
),
|
||||
) {
|
||||
|
|
@ -104,10 +114,14 @@ private fun NewsContentBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM,
|
|||
}
|
||||
},
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
},
|
||||
onSeeAllClick = { feedListCallbacks.onOpenAllNews(false) },
|
||||
isLoading = news.newsUMState == NewsUMState.LOADING,
|
||||
shouldShowSeeAll = news.newsUMState == NewsUMState.CONTENT,
|
||||
)
|
||||
SpacerH(12.dp)
|
||||
|
||||
|
|
@ -125,48 +139,19 @@ private fun NewsContentBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM,
|
|||
}
|
||||
}
|
||||
|
||||
LazyRow(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
state = listState,
|
||||
) {
|
||||
itemsIndexed(
|
||||
items = news.content,
|
||||
key = { _, article -> article.id },
|
||||
contentType = { _, _ -> "article" },
|
||||
) { index, article ->
|
||||
val articleModifier = if (index == FOURTH_ITEM_INDEX) {
|
||||
Modifier.onFirstVisible(
|
||||
minFractionVisible = 0.5f,
|
||||
callback = feedListCallbacks.onSliderScroll,
|
||||
)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
ArticleCard(
|
||||
articleConfigUM = article,
|
||||
onArticleClick = { feedListCallbacks.onArticleClick(article.id) },
|
||||
modifier = articleModifier
|
||||
.heightIn(min = 164.dp)
|
||||
.width(216.dp),
|
||||
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
|
||||
)
|
||||
}
|
||||
NewsSlider(
|
||||
NewsSliderConfig(
|
||||
callbacks = NewsSliderCallbacks(
|
||||
onOpenAllNews = { feedListCallbacks.onOpenAllNews(true) },
|
||||
onSliderScroll = feedListCallbacks.onSliderScroll,
|
||||
onSliderEndReached = feedListCallbacks.onSliderEndReached,
|
||||
onArticleClick = feedListCallbacks.onArticleClick,
|
||||
),
|
||||
content = news.content,
|
||||
shouldShowSeeAllNewsItem = true,
|
||||
),
|
||||
)
|
||||
|
||||
item(contentType = "show_more") {
|
||||
ShowMoreArticlesCard(
|
||||
modifier = Modifier
|
||||
.width(216.dp)
|
||||
.heightIn(min = 164.dp)
|
||||
.onFirstVisible(
|
||||
minFractionVisible = 0.5f,
|
||||
callback = feedListCallbacks.onSliderEndReached,
|
||||
),
|
||||
onClick = { feedListCallbacks.onOpenAllNews(true) },
|
||||
)
|
||||
}
|
||||
}
|
||||
SpacerH(32.dp)
|
||||
}
|
||||
}
|
||||
|
|
@ -181,10 +166,14 @@ private fun NewsErrorBlock(onRetryClick: () -> Unit) {
|
|||
text = stringResourceSafe(R.string.common_news),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
},
|
||||
onSeeAllClick = {},
|
||||
shouldShowSeeAll = false,
|
||||
isLoading = false,
|
||||
)
|
||||
SpacerH(12.dp)
|
||||
BlockCard(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.features.feed.ui.feed.components
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.tangem.core.ui.res.LocalRedesignEnabled
|
||||
import com.tangem.features.feed.ui.feed.state.NewsSliderConfig
|
||||
|
||||
@Composable
|
||||
internal fun NewsSlider(newsSliderConfig: NewsSliderConfig) {
|
||||
val isRedesignEnabled = LocalRedesignEnabled.current
|
||||
if (isRedesignEnabled) {
|
||||
NewsSliderV2(newsSliderConfig)
|
||||
} else {
|
||||
NewsSliderV1(newsSliderConfig)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
package com.tangem.features.feed.ui.feed.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.onFirstVisible
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.features.feed.ui.feed.components.articles.ArticleCard
|
||||
import com.tangem.features.feed.ui.feed.components.articles.ShowMoreArticlesCard
|
||||
import com.tangem.core.ui.components.block.TangemBlockCardColors
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.feed.ui.feed.state.NewsSliderConfig
|
||||
|
||||
@Composable
|
||||
internal fun NewsSliderV1(newsSliderConfig: NewsSliderConfig) {
|
||||
LazyRow(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
state = rememberLazyListState(),
|
||||
) {
|
||||
itemsIndexed(
|
||||
items = newsSliderConfig.content,
|
||||
key = { index, _ -> index },
|
||||
contentType = { _, _ -> "article" },
|
||||
) { index, article ->
|
||||
val articleModifier = if (index == FOURTH_ITEM_INDEX) {
|
||||
Modifier.onFirstVisible(
|
||||
minFractionVisible = 0.5f,
|
||||
callback = newsSliderConfig.callbacks.onSliderScroll,
|
||||
)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
ArticleCard(
|
||||
articleConfigUM = article,
|
||||
onArticleClick = { newsSliderConfig.callbacks.onArticleClick(article.id) },
|
||||
modifier = articleModifier
|
||||
.heightIn(min = 164.dp)
|
||||
.width(216.dp),
|
||||
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
|
||||
)
|
||||
}
|
||||
|
||||
if (newsSliderConfig.shouldShowSeeAllNewsItem) {
|
||||
item(contentType = "show_more") {
|
||||
ShowMoreArticlesCard(
|
||||
modifier = Modifier
|
||||
.width(216.dp)
|
||||
.heightIn(min = 164.dp)
|
||||
.onFirstVisible(
|
||||
minFractionVisible = 0.5f,
|
||||
callback = newsSliderConfig.callbacks.onSliderEndReached,
|
||||
),
|
||||
onClick = newsSliderConfig.callbacks.onOpenAllNews,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
package com.tangem.features.feed.ui.feed.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.Layout
|
||||
import androidx.compose.ui.layout.onFirstVisible
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Constraints
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.features.feed.ui.feed.components.articles.ArticleCard
|
||||
import com.tangem.features.feed.ui.feed.components.articles.ShowMoreArticlesCard
|
||||
import com.tangem.core.ui.extensions.conditional
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.feed.ui.feed.state.NewsSliderConfig
|
||||
|
||||
private val dividerSpacerWidth = 20.dp
|
||||
|
||||
@Suppress("MagicNumber", "LongMethod")
|
||||
@Composable
|
||||
internal fun NewsSliderV2(newsSliderConfig: NewsSliderConfig) {
|
||||
val density = LocalDensity.current
|
||||
val dividerWidthPx = with(density) { 1.dp.roundToPx() }
|
||||
val spacerWidthPx = with(density) { dividerSpacerWidth.roundToPx() }
|
||||
|
||||
LazyRow(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
state = rememberLazyListState(),
|
||||
) {
|
||||
itemsIndexed(
|
||||
items = newsSliderConfig.content,
|
||||
key = { index, _ -> index },
|
||||
contentType = { _, _ -> "article" },
|
||||
) { index, article ->
|
||||
val articleModifier = Modifier.conditional(
|
||||
condition = index == FOURTH_ITEM_INDEX,
|
||||
modifier = {
|
||||
onFirstVisible(
|
||||
minFractionVisible = 0.5f,
|
||||
callback = newsSliderConfig.callbacks.onSliderScroll,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
val shouldShowDivider = newsSliderConfig.shouldShowSeeAllNewsItem ||
|
||||
index < newsSliderConfig.content.size - 1
|
||||
|
||||
// have to use layout cause LazyRow has not fixed height and divider can not be measured
|
||||
Layout(
|
||||
modifier = Modifier,
|
||||
content = {
|
||||
ArticleCard(
|
||||
articleConfigUM = article,
|
||||
onArticleClick = { newsSliderConfig.callbacks.onArticleClick(article.id) },
|
||||
modifier = articleModifier
|
||||
.fillMaxHeight()
|
||||
.width(220.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.width(dividerSpacerWidth))
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(1.dp)
|
||||
.background(TangemTheme.colors2.border.neutral.secondary),
|
||||
)
|
||||
Spacer(modifier = Modifier.width(dividerSpacerWidth))
|
||||
},
|
||||
) { measurables, constraints ->
|
||||
val cardPlaceable = measurables[0].measure(constraints)
|
||||
val height = cardPlaceable.height
|
||||
|
||||
if (shouldShowDivider) {
|
||||
val leftSpacer = measurables[1].measure(Constraints.fixed(spacerWidthPx, height))
|
||||
val divider = measurables[2].measure(Constraints.fixed(dividerWidthPx, height))
|
||||
val rightSpacer = measurables[3].measure(Constraints.fixed(spacerWidthPx, height))
|
||||
val totalWidth = cardPlaceable.width + leftSpacer.width + divider.width + rightSpacer.width
|
||||
|
||||
layout(totalWidth, height) {
|
||||
cardPlaceable.place(0, 0)
|
||||
leftSpacer.place(cardPlaceable.width, 0)
|
||||
divider.place(cardPlaceable.width + leftSpacer.width, 0)
|
||||
rightSpacer.place(cardPlaceable.width + leftSpacer.width + divider.width, 0)
|
||||
}
|
||||
} else {
|
||||
layout(cardPlaceable.width, height) {
|
||||
cardPlaceable.place(0, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (newsSliderConfig.shouldShowSeeAllNewsItem) {
|
||||
item(contentType = "show_more") {
|
||||
ShowMoreArticlesCard(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.width(216.dp)
|
||||
.onFirstVisible(
|
||||
minFractionVisible = 0.5f,
|
||||
callback = newsSliderConfig.callbacks.onSliderEndReached,
|
||||
),
|
||||
onClick = newsSliderConfig.callbacks.onOpenAllNews,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package com.tangem.features.feed.ui.feed.components.articles
|
||||
|
||||
import androidx.compose.material3.CardColors
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.core.ui.components.block.TangemBlockCardColors
|
||||
import com.tangem.core.ui.res.LocalRedesignEnabled
|
||||
|
||||
@Composable
|
||||
fun ArticleCard(
|
||||
articleConfigUM: ArticleConfigUM,
|
||||
onArticleClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
colors: CardColors = TangemBlockCardColors,
|
||||
) {
|
||||
val isRedesignEnabled = LocalRedesignEnabled.current
|
||||
|
||||
if (isRedesignEnabled) {
|
||||
ArticleCardV2(
|
||||
articleConfigUM = articleConfigUM,
|
||||
onArticleClick = onArticleClick,
|
||||
modifier = modifier,
|
||||
)
|
||||
} else {
|
||||
ArticleCardV1(
|
||||
articleConfigUM = articleConfigUM,
|
||||
onArticleClick = onArticleClick,
|
||||
modifier = modifier,
|
||||
colors = colors,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,273 @@
|
|||
package com.tangem.features.feed.ui.feed.components.articles
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.CardColors
|
||||
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.vector.ImageVector
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
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 com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.SpacerHMax
|
||||
import com.tangem.core.ui.components.block.BlockCard
|
||||
import com.tangem.core.ui.components.block.TangemBlockCardColors
|
||||
import com.tangem.core.ui.components.label.entity.LabelUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableSet
|
||||
|
||||
@Composable
|
||||
internal fun ArticleCardV1(
|
||||
articleConfigUM: ArticleConfigUM,
|
||||
onArticleClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
colors: CardColors = TangemBlockCardColors,
|
||||
) {
|
||||
if (articleConfigUM.isTrending) {
|
||||
TrendingArticle(
|
||||
articleConfigUM = articleConfigUM,
|
||||
modifier = modifier,
|
||||
onArticleClick = onArticleClick,
|
||||
colors = colors,
|
||||
)
|
||||
} else {
|
||||
DefaultArticle(
|
||||
articleConfigUM = articleConfigUM,
|
||||
modifier = modifier,
|
||||
onArticleClick = onArticleClick,
|
||||
colors = colors,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TrendingArticle(
|
||||
articleConfigUM: ArticleConfigUM,
|
||||
onArticleClick: () -> Unit,
|
||||
colors: CardColors,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(colors.containerColor)
|
||||
.clickable { onArticleClick() },
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier.matchParentSize(),
|
||||
painter = painterResource(R.drawable.bg_trending_article),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.FillWidth,
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = 24.dp, horizontal = 16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.background(
|
||||
color = TangemTheme.colors.icon.accent.copy(alpha = 0.1f),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
)
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
text = stringResourceSafe(R.string.feed_trending_now),
|
||||
style = TangemTheme.typography.caption1,
|
||||
color = TangemTheme.colors.icon.accent,
|
||||
)
|
||||
|
||||
SpacerH(12.dp)
|
||||
|
||||
Text(
|
||||
text = articleConfigUM.title,
|
||||
color = if (articleConfigUM.isViewed) {
|
||||
TangemTheme.colors.text.tertiary
|
||||
} else {
|
||||
TangemTheme.colors.text.primary1
|
||||
},
|
||||
style = TangemTheme.typography.h3,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
|
||||
SpacerH(8.dp)
|
||||
|
||||
ArticleInfo(
|
||||
score = articleConfigUM.score,
|
||||
createdAt = articleConfigUM.createdAt.resolveReference(),
|
||||
)
|
||||
|
||||
SpacerH(32.dp)
|
||||
|
||||
Tags(
|
||||
modifier = Modifier.padding(horizontal = 46.dp),
|
||||
tags = articleConfigUM.tags.toImmutableList(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun ShowMoreArticlesCardV1(modifier: Modifier = Modifier, onClick: () -> Unit) {
|
||||
BlockCard(
|
||||
modifier = modifier,
|
||||
onClick = onClick,
|
||||
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(vertical = 31.dp, horizontal = 12.dp),
|
||||
) {
|
||||
Image(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_show_more_news_48),
|
||||
contentDescription = stringResourceSafe(R.string.common_show_more),
|
||||
)
|
||||
|
||||
SpacerH(16.dp)
|
||||
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.news_all_news),
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.news_stay_in_the_loop),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DefaultArticle(
|
||||
articleConfigUM: ArticleConfigUM,
|
||||
onArticleClick: () -> Unit,
|
||||
colors: CardColors,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(colors.containerColor)
|
||||
.clickable { onArticleClick() }
|
||||
.padding(12.dp),
|
||||
) {
|
||||
ArticleInfo(
|
||||
score = articleConfigUM.score,
|
||||
createdAt = articleConfigUM.createdAt.resolveReference(),
|
||||
)
|
||||
|
||||
SpacerH(8.dp)
|
||||
|
||||
Text(
|
||||
text = articleConfigUM.title,
|
||||
color = if (articleConfigUM.isViewed) {
|
||||
TangemTheme.colors.text.tertiary
|
||||
} else {
|
||||
TangemTheme.colors.text.primary1
|
||||
},
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
maxLines = 3,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
|
||||
SpacerH(16.dp)
|
||||
|
||||
SpacerHMax()
|
||||
|
||||
Tags(tags = articleConfigUM.tags.toImmutableList())
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun TagsPreview() {
|
||||
TangemThemePreview {
|
||||
Tags(
|
||||
tags = persistentListOf(
|
||||
LabelUM(TextReference.Str("Hype")),
|
||||
LabelUM(TextReference.Str("BTC")),
|
||||
LabelUM(TextReference.Str("Supply")),
|
||||
LabelUM(TextReference.Str("Demand")),
|
||||
LabelUM(TextReference.Str("Best rate")),
|
||||
LabelUM(TextReference.Str("Breaking news")),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360, backgroundColor = 0xFF000000)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun ArticleCardsPreview() {
|
||||
val tags = listOf(
|
||||
LabelUM(TextReference.Str("Hype")),
|
||||
LabelUM(TextReference.Str("BTC")),
|
||||
LabelUM(TextReference.Str("Supply")),
|
||||
LabelUM(TextReference.Str("Demand")),
|
||||
LabelUM(TextReference.Str("Breaking news")),
|
||||
).toImmutableSet()
|
||||
|
||||
val config = ArticleConfigUM(
|
||||
id = 1,
|
||||
title = "Bitcoin ETFs log 4th straight day of inflows (+\$550M)",
|
||||
score = 9.5f,
|
||||
createdAt = TextReference.Str("1h ago"),
|
||||
isTrending = true,
|
||||
tags = tags,
|
||||
isViewed = false,
|
||||
)
|
||||
|
||||
TangemThemePreview {
|
||||
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
|
||||
ArticleCard(
|
||||
articleConfigUM = config,
|
||||
onArticleClick = {},
|
||||
)
|
||||
|
||||
SpacerH(20.dp)
|
||||
|
||||
ArticleCard(
|
||||
articleConfigUM = config.copy(isViewed = true),
|
||||
onArticleClick = {},
|
||||
)
|
||||
|
||||
SpacerH(20.dp)
|
||||
|
||||
ArticleCard(
|
||||
articleConfigUM = config.copy(isTrending = false),
|
||||
onArticleClick = {},
|
||||
)
|
||||
|
||||
SpacerH(20.dp)
|
||||
|
||||
ArticleCard(
|
||||
articleConfigUM = config.copy(isTrending = false, isViewed = true),
|
||||
onArticleClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,382 @@
|
|||
package com.tangem.features.feed.ui.feed.components.articles
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.*
|
||||
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.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
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.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.AndroidUiModes.UI_MODE_NIGHT_NO
|
||||
import androidx.compose.ui.tooling.preview.AndroidUiModes.UI_MODE_NIGHT_YES
|
||||
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.SpacerH
|
||||
import com.tangem.core.ui.components.SpacerW
|
||||
import com.tangem.core.ui.components.label.entity.LabelUM
|
||||
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.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.LocalIsInDarkTheme
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableSet
|
||||
|
||||
@Composable
|
||||
internal fun ArticleCardV2(
|
||||
articleConfigUM: ArticleConfigUM,
|
||||
onArticleClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (articleConfigUM.isTrending) {
|
||||
TrendingArticle(
|
||||
modifier = modifier,
|
||||
articleConfigUM = articleConfigUM,
|
||||
onArticleClick = onArticleClick,
|
||||
)
|
||||
} else {
|
||||
DefaultArticle(
|
||||
modifier = modifier,
|
||||
articleConfigUM = articleConfigUM,
|
||||
onArticleClick = onArticleClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TrendingArticle(
|
||||
articleConfigUM: ArticleConfigUM,
|
||||
onArticleClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
TrendingArticleBackground(
|
||||
modifier = modifier,
|
||||
onClick = onArticleClick,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
) {
|
||||
DayAndRatingInfo(rating = stringReference("${articleConfigUM.score}"))
|
||||
|
||||
SpacerH(8.dp)
|
||||
|
||||
Text(
|
||||
text = articleConfigUM.title,
|
||||
color = if (articleConfigUM.isViewed) {
|
||||
TangemTheme.colors2.text.neutral.tertiary
|
||||
} else {
|
||||
TangemTheme.colors2.text.neutral.primary
|
||||
},
|
||||
style = TangemTheme.typography2.headingSemibold20,
|
||||
textAlign = TextAlign.Start,
|
||||
)
|
||||
|
||||
SpacerH(18.dp)
|
||||
|
||||
Text(
|
||||
text = articleConfigUM.createdAt.resolveReference(),
|
||||
style = TangemTheme.typography2.captionSemibold12,
|
||||
color = TangemTheme.colors2.text.neutral.secondary,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
|
||||
SpacerH(18.dp)
|
||||
|
||||
Tags(tags = articleConfigUM.tags.toImmutableList())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun ShowMoreArticlesCardV2(modifier: Modifier = Modifier, onClick: () -> Unit) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.clip(RoundedCornerShape(20.dp))
|
||||
.background(color = TangemTheme.colors2.surface.level3)
|
||||
.clickable(onClick = onClick)
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = TangemTheme.colors2.border.neutral.primary,
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
)
|
||||
.padding(vertical = 41.dp, horizontal = 16.dp),
|
||||
) {
|
||||
Image(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_show_more_news_48),
|
||||
contentDescription = stringResourceSafe(R.string.common_show_more),
|
||||
)
|
||||
|
||||
SpacerH(10.dp)
|
||||
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.news_all_news),
|
||||
style = TangemTheme.typography2.bodyRegular16,
|
||||
color = TangemTheme.colors2.text.neutral.primary,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.news_stay_in_the_loop),
|
||||
style = TangemTheme.typography2.captionSemibold12,
|
||||
color = TangemTheme.colors2.text.neutral.secondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DefaultArticle(
|
||||
articleConfigUM: ArticleConfigUM,
|
||||
onArticleClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.background(TangemTheme.colors2.surface.level2)
|
||||
.clickable { onArticleClick() }
|
||||
.padding(vertical = 16.dp, horizontal = 10.dp),
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
RatingInfo(rating = stringReference("${articleConfigUM.score}"))
|
||||
}
|
||||
|
||||
SpacerH(8.dp)
|
||||
|
||||
Text(
|
||||
text = articleConfigUM.title,
|
||||
color = if (articleConfigUM.isViewed) {
|
||||
TangemTheme.colors2.text.neutral.tertiary
|
||||
} else {
|
||||
TangemTheme.colors2.text.neutral.primary
|
||||
},
|
||||
style = TangemTheme.typography2.bodyRegular16,
|
||||
minLines = 3,
|
||||
maxLines = 3,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
|
||||
Text(
|
||||
modifier = Modifier.padding(vertical = 20.dp),
|
||||
text = articleConfigUM.createdAt.resolveReference(),
|
||||
style = TangemTheme.typography2.captionSemibold12,
|
||||
color = TangemTheme.colors2.text.neutral.secondary,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
|
||||
Tags(tags = articleConfigUM.tags.toImmutableList())
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
@Composable
|
||||
private fun TrendingArticleBackground(
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable BoxScope.() -> Unit,
|
||||
) {
|
||||
val isDarkTheme = LocalIsInDarkTheme.current
|
||||
|
||||
val bgColor = remember(isDarkTheme) {
|
||||
if (isDarkTheme) {
|
||||
Color(TRENDING_NIGHT_BG)
|
||||
} else {
|
||||
Color(TRENDING_LIGHT_BG)
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.clip(RoundedCornerShape(20.dp))
|
||||
.drawBehind {
|
||||
drawRect(bgColor)
|
||||
|
||||
val w = size.width
|
||||
val h = size.height
|
||||
val radiusScale = (w + h) / 2f
|
||||
|
||||
drawRect(
|
||||
brush = Brush.radialGradient(
|
||||
colors = listOf(
|
||||
Color(0xFF7C16F1).copy(alpha = .8f),
|
||||
Color.Transparent,
|
||||
),
|
||||
center = Offset(w / 2f, 2.4f * h),
|
||||
radius = radiusScale * 1.57f,
|
||||
),
|
||||
)
|
||||
|
||||
drawRect(
|
||||
brush = Brush.radialGradient(
|
||||
colors = listOf(
|
||||
Color(0xFF3360FF).copy(alpha = .7f),
|
||||
Color.Transparent,
|
||||
),
|
||||
center = Offset(w / 2f, 2.95f * h),
|
||||
radius = radiusScale * 1.9f,
|
||||
),
|
||||
)
|
||||
|
||||
drawRect(
|
||||
brush = Brush.radialGradient(
|
||||
colors = listOf(
|
||||
Color(0xFFFF9408).copy(alpha = .45f),
|
||||
Color.Transparent,
|
||||
),
|
||||
center = Offset(-0.38f * w, 2.1f * h),
|
||||
radius = radiusScale * 1.41f,
|
||||
),
|
||||
)
|
||||
|
||||
drawRect(
|
||||
brush = Brush.radialGradient(
|
||||
colors = listOf(
|
||||
Color(0xFFFC2424).copy(alpha = .5f),
|
||||
Color.Transparent,
|
||||
),
|
||||
center = Offset(1.188f * w, 2.37f * h),
|
||||
radius = radiusScale * 1.41f,
|
||||
),
|
||||
)
|
||||
}
|
||||
.border(width = 1.dp, color = bgColor.copy(.1f))
|
||||
.clickable(onClick = onClick),
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DayAndRatingInfo(rating: TextReference, modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
RatingInfo(rating)
|
||||
|
||||
SpacerW(8.dp)
|
||||
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.feed_trending_now),
|
||||
style = TangemTheme.typography2.captionSemibold12,
|
||||
color = TangemTheme.colors2.text.neutral.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RatingInfo(rating: TextReference) {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_wrapped_circle_star_16),
|
||||
tint = TangemTheme.colors2.fill.status.attention,
|
||||
contentDescription = null,
|
||||
)
|
||||
|
||||
SpacerW(2.dp)
|
||||
|
||||
Text(
|
||||
text = rating.resolveReference(),
|
||||
color = TangemTheme.colors2.text.status.attention,
|
||||
style = TangemTheme.typography2.captionSemibold12,
|
||||
)
|
||||
}
|
||||
|
||||
private const val TRENDING_NIGHT_BG = 0xFF1F1F1F
|
||||
private const val TRENDING_LIGHT_BG = 0xFFFFFFFF
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun TagsPreview() {
|
||||
TangemThemePreviewRedesign {
|
||||
Tags(
|
||||
tags = persistentListOf(
|
||||
LabelUM(TextReference.Str("Hype")),
|
||||
LabelUM(TextReference.Str("BTC")),
|
||||
LabelUM(TextReference.Str("Supply")),
|
||||
LabelUM(TextReference.Str("Demand")),
|
||||
LabelUM(TextReference.Str("Best rate")),
|
||||
LabelUM(TextReference.Str("Breaking news")),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(widthDp = 360, uiMode = UI_MODE_NIGHT_YES)
|
||||
@Preview(widthDp = 360, uiMode = UI_MODE_NIGHT_NO)
|
||||
@Composable
|
||||
private fun ArticleCardsPreview() {
|
||||
val tags = listOf(
|
||||
LabelUM(TextReference.Str("Hype")),
|
||||
LabelUM(TextReference.Str("BTC")),
|
||||
LabelUM(TextReference.Str("Supply")),
|
||||
LabelUM(TextReference.Str("Demand")),
|
||||
LabelUM(TextReference.Str("Breaking news")),
|
||||
).toImmutableSet()
|
||||
|
||||
val config = ArticleConfigUM(
|
||||
id = 1,
|
||||
title = "Bitcoin ETFs log 4th straight day of inflows (+\$550M)",
|
||||
score = 9.5f,
|
||||
createdAt = TextReference.Str("1h ago"),
|
||||
isTrending = true,
|
||||
tags = tags,
|
||||
isViewed = false,
|
||||
)
|
||||
|
||||
TangemThemePreviewRedesign {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
ArticleCardV2(
|
||||
articleConfigUM = config,
|
||||
onArticleClick = {},
|
||||
)
|
||||
|
||||
SpacerH(20.dp)
|
||||
|
||||
ArticleCardV2(
|
||||
articleConfigUM = config.copy(isViewed = true),
|
||||
onArticleClick = {},
|
||||
)
|
||||
|
||||
SpacerH(20.dp)
|
||||
|
||||
ArticleCardV2(
|
||||
articleConfigUM = config.copy(isTrending = false),
|
||||
onArticleClick = {},
|
||||
)
|
||||
|
||||
SpacerH(20.dp)
|
||||
|
||||
ArticleCardV2(
|
||||
articleConfigUM = config.copy(isTrending = false, isViewed = true),
|
||||
onArticleClick = {},
|
||||
)
|
||||
|
||||
SpacerH(20.dp)
|
||||
|
||||
ShowMoreArticlesCardV2(onClick = {})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.tangem.features.feed.ui.feed.components.articles
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.components.label.entity.LabelUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import kotlinx.collections.immutable.ImmutableSet
|
||||
|
||||
@Immutable
|
||||
data class ArticleConfigUM(
|
||||
val id: Int,
|
||||
val title: String,
|
||||
val score: Float,
|
||||
val createdAt: TextReference,
|
||||
val isTrending: Boolean,
|
||||
val tags: ImmutableSet<LabelUM>,
|
||||
val isViewed: Boolean,
|
||||
)
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
package com.tangem.features.feed.ui.feed.components.articles
|
||||
|
||||
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.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.label.Label
|
||||
import com.tangem.core.ui.components.label.entity.LabelUM
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun ArticleHeader(
|
||||
title: String,
|
||||
createdAt: String,
|
||||
score: Float,
|
||||
tags: ImmutableList<LabelUM>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
ArticleInfo(
|
||||
score = score,
|
||||
createdAt = createdAt,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
Text(
|
||||
text = title,
|
||||
style = TangemTheme.typography.h2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
|
||||
if (tags.isNotEmpty()) {
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
tags.forEach { tag ->
|
||||
Label(
|
||||
state = tag,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package com.tangem.features.feed.ui.feed.components.articles
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.size
|
||||
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.drawWithCache
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
@Composable
|
||||
internal fun ArticleInfo(score: Float, createdAt: String, modifier: Modifier = Modifier) {
|
||||
val dotColor = TangemTheme.colors.text.secondary
|
||||
Row(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Image(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_start_circle_12),
|
||||
contentDescription = null,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = score.toString(),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
)
|
||||
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.size(4.dp)
|
||||
.drawWithCache {
|
||||
val radius = size.minDimension / 2f
|
||||
onDrawBehind {
|
||||
drawCircle(
|
||||
color = dotColor,
|
||||
radius = radius,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
Text(
|
||||
text = createdAt,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
package com.tangem.features.feed.ui.feed.components.articles
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
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.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.TangemTheme
|
||||
|
||||
@Composable
|
||||
fun TrendingLoadingArticle(modifier: Modifier = Modifier) {
|
||||
BlockCard(
|
||||
modifier = modifier,
|
||||
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 24.dp, horizontal = 16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
RectangleShimmer(modifier = Modifier.size(width = 96.dp, height = 24.dp), radius = 8.dp)
|
||||
SpacerH(12.dp)
|
||||
RectangleShimmer(modifier = Modifier.size(width = 285.dp, height = 18.dp), radius = 4.dp)
|
||||
SpacerH(6.dp)
|
||||
RectangleShimmer(modifier = Modifier.size(width = 190.dp, height = 18.dp), radius = 4.dp)
|
||||
SpacerH(14.dp)
|
||||
RectangleShimmer(modifier = Modifier.size(width = 110.dp, height = 18.dp), radius = 4.dp)
|
||||
SpacerH(32.dp)
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
RectangleShimmer(modifier = Modifier.size(width = 64.dp, height = 24.dp), radius = 8.dp)
|
||||
RectangleShimmer(modifier = Modifier.size(width = 64.dp, height = 24.dp), radius = 8.dp)
|
||||
RectangleShimmer(modifier = Modifier.size(width = 64.dp, height = 24.dp), radius = 8.dp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DefaultLoadingArticle(modifier: Modifier = Modifier) {
|
||||
BlockCard(
|
||||
modifier = modifier,
|
||||
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(12.dp)) {
|
||||
RectangleShimmer(modifier = Modifier.size(width = 110.dp, height = 16.dp), radius = 4.dp)
|
||||
SpacerH(12.dp)
|
||||
RectangleShimmer(modifier = Modifier.size(width = 142.dp, height = 18.dp), radius = 4.dp)
|
||||
SpacerH(6.dp)
|
||||
RectangleShimmer(modifier = Modifier.size(width = 176.dp, height = 18.dp), radius = 4.dp)
|
||||
SpacerH(6.dp)
|
||||
RectangleShimmer(modifier = Modifier.size(width = 120.dp, height = 18.dp), radius = 4.dp)
|
||||
SpacerH(16.dp)
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
RectangleShimmer(modifier = Modifier.size(width = 72.dp, height = 24.dp), radius = 8.dp)
|
||||
RectangleShimmer(modifier = Modifier.size(width = 72.dp, height = 24.dp), radius = 8.dp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.features.feed.ui.feed.components.articles
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.core.ui.res.LocalRedesignEnabled
|
||||
|
||||
@Composable
|
||||
fun ShowMoreArticlesCard(modifier: Modifier = Modifier, onClick: () -> Unit) {
|
||||
val isRedesignEnabled: Boolean = LocalRedesignEnabled.current
|
||||
if (isRedesignEnabled) {
|
||||
ShowMoreArticlesCardV2(modifier = modifier, onClick = onClick)
|
||||
} else {
|
||||
ShowMoreArticlesCardV1(modifier = modifier, onClick = onClick)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
package com.tangem.features.feed.ui.feed.components.articles
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.Placeable
|
||||
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.components.label.Label
|
||||
import com.tangem.core.ui.components.label.entity.LabelUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.utils.StringsSigns
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
private val TAG_SPACING = 4.dp
|
||||
|
||||
/**
|
||||
* A layout that displays a list of tags, automatically calculating how many can fit
|
||||
* in a single line. If not all tags fit, it shows an overflow indicator (e.g., "+N").
|
||||
*
|
||||
* @param tags The immutable list of [LabelUM] to display.
|
||||
* @param modifier The modifier to be applied to the layout.
|
||||
*/
|
||||
@Composable
|
||||
internal fun Tags(tags: ImmutableList<LabelUM>, modifier: Modifier = Modifier) {
|
||||
if (tags.isEmpty()) return
|
||||
|
||||
SubcomposeLayout(modifier = modifier) { constraints ->
|
||||
val spacingPx = TAG_SPACING.roundToPx()
|
||||
|
||||
val tagPlaceables = subcompose(ContentSlot.Tags) {
|
||||
tags.forEach { tag ->
|
||||
Label(state = tag)
|
||||
}
|
||||
}.map { it.measure(constraints) }
|
||||
|
||||
val layoutInfo = calculateLayoutInfo(
|
||||
maxWidth = constraints.maxWidth,
|
||||
spacingPx = spacingPx,
|
||||
tagPlaceables = tagPlaceables,
|
||||
)
|
||||
|
||||
val overflowPlaceable = layoutInfo.overflowCount.takeIf { it > 0 }?.let {
|
||||
subcompose(ContentSlot.Overflow) {
|
||||
OverflowLabel(count = it)
|
||||
}.first().measure(constraints)
|
||||
}
|
||||
|
||||
val visiblePlaceables = tagPlaceables.take(layoutInfo.visibleCount)
|
||||
val allVisibleItems = visiblePlaceables + listOfNotNull(overflowPlaceable)
|
||||
|
||||
val width = calculateRowWidth(
|
||||
placeables = allVisibleItems,
|
||||
spacingPx = spacingPx,
|
||||
).coerceIn(constraints.minWidth, constraints.maxWidth)
|
||||
|
||||
val height = allVisibleItems.maxOfOrNull { it.height } ?: 0
|
||||
|
||||
layout(width = width, height = height) {
|
||||
var xPosition = 0
|
||||
allVisibleItems.forEachIndexed { index, placeable ->
|
||||
if (index > 0) {
|
||||
xPosition += spacingPx
|
||||
}
|
||||
val yPosition = height - placeable.height
|
||||
placeable.placeRelative(xPosition, yPosition)
|
||||
xPosition += placeable.width
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates how many tags can be displayed and how many are overflowing.
|
||||
*/
|
||||
private fun SubcomposeMeasureScope.calculateLayoutInfo(
|
||||
maxWidth: Int,
|
||||
spacingPx: Int,
|
||||
tagPlaceables: List<Placeable>,
|
||||
): TagsLayoutInfo {
|
||||
val totalWidthWithoutOverflow = calculateRowWidth(tagPlaceables, spacingPx)
|
||||
|
||||
if (totalWidthWithoutOverflow <= maxWidth) {
|
||||
return TagsLayoutInfo(visibleCount = tagPlaceables.size, overflowCount = 0)
|
||||
}
|
||||
|
||||
for (count in tagPlaceables.indices.reversed()) {
|
||||
val visibleTags = tagPlaceables.take(count)
|
||||
val overflowCount = tagPlaceables.size - count
|
||||
|
||||
val tempOverflowPlaceable = subcompose("overflow_temp_$overflowCount") {
|
||||
OverflowLabel(count = overflowCount)
|
||||
}.first().measure(Constraints())
|
||||
|
||||
val widthWithOverflow = calculateRowWidth(
|
||||
placeables = visibleTags + tempOverflowPlaceable,
|
||||
spacingPx = spacingPx,
|
||||
)
|
||||
|
||||
if (widthWithOverflow <= maxWidth) {
|
||||
return TagsLayoutInfo(visibleCount = count, overflowCount = overflowCount)
|
||||
}
|
||||
}
|
||||
|
||||
return TagsLayoutInfo(visibleCount = 0, overflowCount = tagPlaceables.size)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OverflowLabel(count: Int) {
|
||||
Label(
|
||||
state = LabelUM(
|
||||
text = TextReference.Str("${StringsSigns.PLUS}$count"),
|
||||
maxLines = 1,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun calculateRowWidth(placeables: List<Placeable>, spacingPx: Int): Int {
|
||||
if (placeables.isEmpty()) return 0
|
||||
val tagsWidth = placeables.sumOf { it.width }
|
||||
val spacingWidth = spacingPx * (placeables.size - 1).coerceAtLeast(0)
|
||||
return tagsWidth + spacingWidth
|
||||
}
|
||||
|
||||
private data class TagsLayoutInfo(val visibleCount: Int, val overflowCount: Int)
|
||||
|
||||
private enum class ContentSlot { Tags, Overflow }
|
||||
|
|
@ -2,7 +2,7 @@ package com.tangem.features.feed.ui.feed.preview
|
|||
|
||||
import com.tangem.common.ui.charts.state.MarketChartRawData
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
import com.tangem.common.ui.news.ArticleConfigUM
|
||||
import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.components.label.entity.LabelLeadingContentUM
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package com.tangem.features.feed.ui.feed.state
|
|||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
import com.tangem.common.ui.news.ArticleConfigUM
|
||||
import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
|
||||
import com.tangem.features.feed.ui.earn.state.EarnListUM
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.features.feed.ui.feed.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Immutable
|
||||
internal data class NewsSliderConfig(
|
||||
val shouldShowSeeAllNewsItem: Boolean,
|
||||
val content: ImmutableList<ArticleConfigUM>,
|
||||
val callbacks: NewsSliderCallbacks,
|
||||
)
|
||||
|
||||
@Immutable
|
||||
internal data class NewsSliderCallbacks(
|
||||
val onOpenAllNews: () -> Unit,
|
||||
val onSliderScroll: () -> Unit,
|
||||
val onSliderEndReached: () -> Unit,
|
||||
val onArticleClick: (id: Int) -> Unit,
|
||||
)
|
||||
|
|
@ -2,30 +2,24 @@ package com.tangem.features.feed.ui.market.detailed.components
|
|||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.onFirstVisible
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.common.ui.news.ArticleCard
|
||||
import com.tangem.core.ui.components.UnableToLoadData
|
||||
import com.tangem.core.ui.components.block.TangemBlockCardColors
|
||||
import com.tangem.core.ui.components.items.DescriptionItem
|
||||
import com.tangem.core.ui.components.items.DescriptionPlaceholder
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.features.feed.ui.feed.components.NewsSlider
|
||||
import com.tangem.features.feed.ui.feed.state.NewsSliderCallbacks
|
||||
import com.tangem.features.feed.ui.feed.state.NewsSliderConfig
|
||||
import com.tangem.features.feed.ui.market.detailed.state.MarketsTokenDetailsUM
|
||||
import com.tangem.features.feed.ui.market.detailed.state.MarketsTokenDetailsUM.RelatedNews
|
||||
|
||||
private const val FOURTH_ITEM_INDEX = 3
|
||||
|
||||
@Suppress("CanBeNonNullable") // TODO will be removed after [REDACTED_JIRA]
|
||||
internal fun LazyListScope.tokenMarketDetailsBody(
|
||||
state: MarketsTokenDetailsUM.Body,
|
||||
|
|
@ -207,13 +201,6 @@ private fun LazyListScope.loadingInfoBlocks() {
|
|||
|
||||
private fun LazyListScope.relatedNews(relatedNews: RelatedNews) {
|
||||
item("related-news") {
|
||||
val listState = rememberLazyListState()
|
||||
val articlesReadStatus = remember(relatedNews.articles) {
|
||||
relatedNews.articles.map { it.isViewed }
|
||||
}
|
||||
LaunchedEffect(articlesReadStatus) {
|
||||
listState.requestScrollToItem(0)
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
|
|
@ -231,35 +218,18 @@ private fun LazyListScope.relatedNews(relatedNews: RelatedNews) {
|
|||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
|
||||
LazyRow(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
state = listState,
|
||||
) {
|
||||
itemsIndexed(
|
||||
items = relatedNews.articles,
|
||||
key = { index, article -> article.id },
|
||||
) { index, article ->
|
||||
val articleModifier = if (index == FOURTH_ITEM_INDEX) {
|
||||
Modifier.onFirstVisible(
|
||||
minFractionVisible = 0.5f,
|
||||
callback = relatedNews.onScroll,
|
||||
)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
|
||||
ArticleCard(
|
||||
articleConfigUM = article,
|
||||
onArticleClick = { relatedNews.onArticledClicked(article.id) },
|
||||
modifier = articleModifier
|
||||
.heightIn(min = 164.dp)
|
||||
.width(216.dp),
|
||||
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
|
||||
)
|
||||
}
|
||||
}
|
||||
NewsSlider(
|
||||
NewsSliderConfig(
|
||||
callbacks = NewsSliderCallbacks(
|
||||
onOpenAllNews = {}, // not applicable here
|
||||
onSliderScroll = relatedNews.onScroll,
|
||||
onSliderEndReached = {}, // not applicable here
|
||||
onArticleClick = relatedNews.onArticledClicked,
|
||||
),
|
||||
content = relatedNews.articles,
|
||||
shouldShowSeeAllNewsItem = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package com.tangem.features.feed.ui.market.detailed.state
|
|||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.common.ui.charts.state.MarketChartDataProducer
|
||||
import com.tangem.common.ui.news.ArticleConfigUM
|
||||
import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.event.StateEvent
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ import androidx.compose.ui.unit.dp
|
|||
import coil.compose.SubcomposeAsyncImage
|
||||
import coil.request.CachePolicy
|
||||
import coil.request.ImageRequest
|
||||
import com.tangem.common.ui.news.ArticleHeader
|
||||
import com.tangem.features.feed.ui.feed.components.articles.ArticleHeader
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.*
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonSize
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.common.ui.news.ArticleConfigUM
|
||||
import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.chip.Chip
|
||||
import com.tangem.core.ui.components.chip.entity.ChipUM
|
||||
|
|
|
|||
|
|
@ -16,9 +16,9 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.common.ui.news.ArticleCard
|
||||
import com.tangem.common.ui.news.ArticleConfigUM
|
||||
import com.tangem.common.ui.news.DefaultLoadingArticle
|
||||
import com.tangem.features.feed.ui.feed.components.articles.ArticleCard
|
||||
import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM
|
||||
import com.tangem.features.feed.ui.feed.components.articles.DefaultLoadingArticle
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.UnableToLoadData
|
||||
import com.tangem.core.ui.components.block.TangemBlockCardColors
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.features.feed.ui.news.list.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.common.ui.news.ArticleConfigUM
|
||||
import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM
|
||||
import com.tangem.core.ui.components.chip.entity.ChipUM
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue