Updated on 2026-08-14
This commit is contained in:
parent
aea7f5f415
commit
c83ce2ada3
53 changed files with 1062 additions and 238 deletions
|
|
@ -27,7 +27,10 @@ dependencies {
|
|||
implementation(projects.core.datasource)
|
||||
|
||||
/** Domain modules */
|
||||
implementation(projects.domain.nft)
|
||||
implementation(projects.domain.nft.models)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.wallets.models)
|
||||
|
||||
/** Common */
|
||||
implementation(projects.common.ui)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,17 @@
|
|||
package com.tangem.features.nft
|
||||
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.features.nft.collections.DefaultNFTCollectionsComponent
|
||||
import com.tangem.features.nft.collections.model.NFTCollectionsModel
|
||||
import com.tangem.features.nft.component.NFTCollectionsComponent
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import dagger.multibindings.ClassKey
|
||||
import dagger.multibindings.IntoMap
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
|
|
@ -16,4 +23,17 @@ internal object NFTFeatureModule {
|
|||
fun provideFeatureToggles(featureTogglesManager: FeatureTogglesManager): NFTFeatureToggles {
|
||||
return DefaultNFTFeatureToggles(featureTogglesManager)
|
||||
}
|
||||
}
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface NFTFeatureModuleBinds {
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindComponentFactory(impl: DefaultNFTCollectionsComponent.Factory): NFTCollectionsComponent.Factory
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(NFTCollectionsModel::class)
|
||||
fun bindModel(model: NFTCollectionsModel): Model
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package com.tangem.features.nft.collections
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.features.nft.collections.model.NFTCollectionsModel
|
||||
import com.tangem.features.nft.collections.ui.NFTCollections
|
||||
import com.tangem.features.nft.component.NFTCollectionsComponent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
internal class DefaultNFTCollectionsComponent @AssistedInject constructor(
|
||||
@Assisted context: AppComponentContext,
|
||||
@Assisted private val params: NFTCollectionsComponent.Params,
|
||||
) : NFTCollectionsComponent, AppComponentContext by context {
|
||||
|
||||
private val model: NFTCollectionsModel = getOrCreateModel(params)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.state.collectAsStateWithLifecycle()
|
||||
|
||||
NFTCollections(state, modifier)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : NFTCollectionsComponent.Factory {
|
||||
override fun create(
|
||||
context: AppComponentContext,
|
||||
params: NFTCollectionsComponent.Params,
|
||||
): DefaultNFTCollectionsComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -5,12 +5,8 @@ import kotlinx.collections.immutable.ImmutableList
|
|||
|
||||
@Immutable
|
||||
internal sealed class NFTCollectionAssetsListUM {
|
||||
data object Collapsed : NFTCollectionAssetsListUM()
|
||||
|
||||
@Immutable
|
||||
sealed class Expanded : NFTCollectionAssetsListUM() {
|
||||
data class Loading(val itemsCount: Int) : Expanded()
|
||||
data class Failed(val onRetryClick: () -> Unit) : Expanded()
|
||||
data class Content(val items: ImmutableList<NFTCollectionAssetUM>) : Expanded()
|
||||
}
|
||||
data object Init : NFTCollectionAssetsListUM()
|
||||
data class Loading(val itemsCount: Int) : NFTCollectionAssetsListUM()
|
||||
data class Failed(val onRetryClick: () -> Unit) : NFTCollectionAssetsListUM()
|
||||
data class Content(val items: ImmutableList<NFTCollectionAssetUM>) : NFTCollectionAssetsListUM()
|
||||
}
|
||||
|
|
@ -10,5 +10,6 @@ internal data class NFTCollectionUM(
|
|||
val logoUrl: String?,
|
||||
val description: TextReference,
|
||||
val assets: NFTCollectionAssetsListUM,
|
||||
val isExpanded: Boolean,
|
||||
val onExpandClick: () -> Unit,
|
||||
)
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.features.nft.collections.entity
|
||||
|
||||
internal data class NFTCollectionsStateUM(
|
||||
val onBackClick: () -> Unit,
|
||||
val content: NFTCollectionsUM,
|
||||
)
|
||||
|
|
@ -2,4 +2,7 @@ package com.tangem.features.nft.collections.entity
|
|||
|
||||
import com.tangem.core.ui.components.notifications.NotificationConfig
|
||||
|
||||
internal data class NFTCollectionsWarningUM(val config: NotificationConfig)
|
||||
internal data class NFTCollectionsWarningUM(
|
||||
val id: String,
|
||||
val config: NotificationConfig,
|
||||
)
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package com.tangem.features.nft.collections.entity.transformer
|
||||
|
||||
import com.tangem.domain.nft.models.NFTCollection
|
||||
import com.tangem.features.nft.collections.entity.NFTCollectionsStateUM
|
||||
import com.tangem.features.nft.collections.entity.NFTCollectionsUM
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
internal class ChangeCollectionExpandedStateTransformer(
|
||||
private val collectionId: NFTCollection.Identifier,
|
||||
private val onFirstExpanded: () -> Unit,
|
||||
) : Transformer<NFTCollectionsStateUM> {
|
||||
|
||||
override fun transform(prevState: NFTCollectionsStateUM): NFTCollectionsStateUM = prevState.copy(
|
||||
content = when (prevState.content) {
|
||||
is NFTCollectionsUM.Empty,
|
||||
is NFTCollectionsUM.Loading,
|
||||
is NFTCollectionsUM.Failed,
|
||||
-> prevState.content
|
||||
is NFTCollectionsUM.Content -> prevState.content.copy(
|
||||
collections = prevState.content.collections.map {
|
||||
if (it.id == collectionId.toString()) {
|
||||
if (!it.isExpanded) {
|
||||
onFirstExpanded()
|
||||
}
|
||||
it.copy(isExpanded = !it.isExpanded)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}.toPersistentList(),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.features.nft.collections.entity.transformer
|
||||
|
||||
import com.tangem.features.nft.collections.entity.NFTCollectionsStateUM
|
||||
import com.tangem.features.nft.collections.entity.NFTCollectionsUM
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
internal class ToggleSearchBarTransformer(private val isActive: Boolean) : Transformer<NFTCollectionsStateUM> {
|
||||
|
||||
override fun transform(prevState: NFTCollectionsStateUM): NFTCollectionsStateUM = prevState.copy(
|
||||
content = when (val content = prevState.content) {
|
||||
is NFTCollectionsUM.Content -> content.copy(
|
||||
search = content.search.copy(
|
||||
isActive = isActive,
|
||||
),
|
||||
)
|
||||
is NFTCollectionsUM.Empty,
|
||||
is NFTCollectionsUM.Loading,
|
||||
is NFTCollectionsUM.Failed,
|
||||
-> content
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
package com.tangem.features.nft.collections.entity.transformer
|
||||
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import com.tangem.core.ui.components.notifications.NotificationConfig
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.getActiveIconRes
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.domain.nft.models.*
|
||||
import com.tangem.features.nft.collections.entity.*
|
||||
import com.tangem.features.nft.impl.R
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class UpdateDataStateTransformer(
|
||||
private val nftCollections: List<NFTCollections>,
|
||||
private val searchQuery: String,
|
||||
private val onReceiveClick: () -> Unit,
|
||||
private val onRetryClick: () -> Unit,
|
||||
private val onExpandCollectionClick: (NFTCollection) -> Unit,
|
||||
private val onRetryAssetsClick: (NFTCollection) -> Unit,
|
||||
private val onAssetClick: (NFTAsset) -> Unit,
|
||||
private val initialSearchBarFactory: () -> SearchBarUM,
|
||||
) : Transformer<NFTCollectionsStateUM> {
|
||||
|
||||
override fun transform(prevState: NFTCollectionsStateUM): NFTCollectionsStateUM = prevState.copy(
|
||||
content = when {
|
||||
nftCollections.allCollectionsFailed() ->
|
||||
NFTCollectionsUM.Failed(onRetryClick, onReceiveClick)
|
||||
nftCollections.anyCollectionFailed() && nftCollections.allLoadedCollectionsEmpty() ->
|
||||
NFTCollectionsUM.Failed(onRetryClick, onReceiveClick)
|
||||
nftCollections.allCollectionsLoaded() && nftCollections.allCollectionsEmpty() ->
|
||||
NFTCollectionsUM.Empty(onReceiveClick)
|
||||
!nftCollections.allCollectionsLoaded() && nftCollections.allCollectionsEmpty() ->
|
||||
NFTCollectionsUM.Loading(onReceiveClick)
|
||||
else -> {
|
||||
NFTCollectionsUM.Content(
|
||||
search = if (prevState.content is NFTCollectionsUM.Content) {
|
||||
prevState.content.search.copy(
|
||||
query = searchQuery,
|
||||
)
|
||||
} else {
|
||||
initialSearchBarFactory()
|
||||
},
|
||||
collections = nftCollections
|
||||
.map { it.content }
|
||||
.asSequence()
|
||||
.filterIsInstance<NFTCollections.Content.Collections>()
|
||||
.map { it.collections.orEmpty().transform(prevState, searchQuery) }
|
||||
.flatten()
|
||||
.toPersistentList(),
|
||||
warnings = transformNotifications(),
|
||||
onReceiveClick = onReceiveClick,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
private fun List<NFTCollection>.transform(
|
||||
state: NFTCollectionsStateUM,
|
||||
query: String,
|
||||
): ImmutableList<NFTCollectionUM> = mapNotNull {
|
||||
if (query.isEmpty() || it.name?.lowercase()?.contains(query.lowercase()) == true) {
|
||||
NFTCollectionUM(
|
||||
id = it.id.toString(),
|
||||
networkIconId = getActiveIconRes(it.network.id.value),
|
||||
name = it.name.orEmpty(),
|
||||
description = TextReference.PluralRes(
|
||||
R.plurals.nft_collections_count,
|
||||
it.count,
|
||||
wrappedList(it.count),
|
||||
),
|
||||
logoUrl = it.logoUrl,
|
||||
assets = it.transformAssets(),
|
||||
onExpandClick = {
|
||||
onExpandCollectionClick(it)
|
||||
},
|
||||
isExpanded = it.isExpanded(state),
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}.toPersistentList()
|
||||
|
||||
private fun transformNotifications(): ImmutableList<NFTCollectionsWarningUM> = buildList {
|
||||
if (nftCollections.anyCollectionFailed()) {
|
||||
add(
|
||||
NFTCollectionsWarningUM(
|
||||
id = "loading troubles",
|
||||
config = NotificationConfig(
|
||||
title = TextReference.Res(R.string.nft_collections_warning_title),
|
||||
subtitle = TextReference.Res(R.string.nft_collections_warning_subtitle),
|
||||
iconResId = R.drawable.ic_alert_triangle_20,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}.toPersistentList()
|
||||
|
||||
private fun NFTCollection.transformAssets(): NFTCollectionAssetsListUM = when (val assets = this.assets) {
|
||||
is NFTCollection.Assets.Empty -> NFTCollectionAssetsListUM.Init
|
||||
is NFTCollection.Assets.Loading -> NFTCollectionAssetsListUM.Loading(count)
|
||||
is NFTCollection.Assets.Failed -> NFTCollectionAssetsListUM.Failed { onRetryAssetsClick(this) }
|
||||
is NFTCollection.Assets.Value -> NFTCollectionAssetsListUM.Content(
|
||||
items = assets
|
||||
.items
|
||||
.map { it.transform() }
|
||||
.toPersistentList(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun NFTAsset.transform(): NFTCollectionAssetUM = NFTCollectionAssetUM(
|
||||
id = id.toString(),
|
||||
name = name.orEmpty(),
|
||||
imageUrl = media?.url,
|
||||
price = when (val salePrice = salePrice) {
|
||||
is NFTSalePrice.Empty -> NFTSalePriceUM.Failed
|
||||
is NFTSalePrice.Loading -> NFTSalePriceUM.Loading
|
||||
is NFTSalePrice.Error -> NFTSalePriceUM.Failed
|
||||
is NFTSalePrice.Value -> NFTSalePriceUM.Content(salePrice.value.toString())
|
||||
},
|
||||
onItemClick = {
|
||||
onAssetClick(this)
|
||||
},
|
||||
)
|
||||
|
||||
private fun NFTCollection.isExpanded(state: NFTCollectionsStateUM): Boolean =
|
||||
(state.content as? NFTCollectionsUM.Content)
|
||||
?.collections
|
||||
?.firstOrNull { it.id == id.toString() }
|
||||
?.isExpanded
|
||||
?: false
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.features.nft.collections.entity.transformer
|
||||
|
||||
import com.tangem.features.nft.collections.entity.NFTCollectionsStateUM
|
||||
import com.tangem.features.nft.collections.entity.NFTCollectionsUM
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
internal class UpdateSearchQueryTransformer(private val newQuery: String) : Transformer<NFTCollectionsStateUM> {
|
||||
|
||||
override fun transform(prevState: NFTCollectionsStateUM): NFTCollectionsStateUM = prevState.copy(
|
||||
content = when (val content = prevState.content) {
|
||||
is NFTCollectionsUM.Content -> content.copy(
|
||||
search = content.search.copy(
|
||||
query = newQuery,
|
||||
),
|
||||
)
|
||||
is NFTCollectionsUM.Empty,
|
||||
is NFTCollectionsUM.Loading,
|
||||
is NFTCollectionsUM.Failed,
|
||||
-> content
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
package com.tangem.features.nft.collections.model
|
||||
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.core.ui.components.fields.InputManager
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.domain.nft.FetchNFTCollectionAssetsUseCase
|
||||
import com.tangem.domain.nft.GetNFTCollectionsUseCase
|
||||
import com.tangem.domain.nft.models.NFTAsset
|
||||
import com.tangem.domain.nft.models.NFTCollection
|
||||
import com.tangem.features.nft.collections.entity.*
|
||||
import com.tangem.features.nft.collections.entity.transformer.ChangeCollectionExpandedStateTransformer
|
||||
import com.tangem.features.nft.collections.entity.transformer.ToggleSearchBarTransformer
|
||||
import com.tangem.features.nft.collections.entity.transformer.UpdateDataStateTransformer
|
||||
import com.tangem.features.nft.collections.entity.transformer.UpdateSearchQueryTransformer
|
||||
import com.tangem.features.nft.component.NFTCollectionsComponent
|
||||
import com.tangem.features.nft.impl.R
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@ModelScoped
|
||||
internal class NFTCollectionsModel @Inject constructor(
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val router: Router,
|
||||
private val searchManager: InputManager,
|
||||
private val getNFTCollectionsUseCase: GetNFTCollectionsUseCase,
|
||||
private val fetchNFTCollectionAssetsUseCase: FetchNFTCollectionAssetsUseCase,
|
||||
paramsContainer: ParamsContainer,
|
||||
) : Model() {
|
||||
|
||||
val state: StateFlow<NFTCollectionsStateUM> get() = _state
|
||||
|
||||
private val _state = MutableStateFlow(
|
||||
value = NFTCollectionsStateUM(
|
||||
onBackClick = ::navigateBack,
|
||||
content = NFTCollectionsUM.Loading(::onReceiveClick),
|
||||
),
|
||||
)
|
||||
|
||||
private val params: NFTCollectionsComponent.Params = paramsContainer.require()
|
||||
|
||||
init {
|
||||
subscribeToNFTCollections()
|
||||
}
|
||||
|
||||
private fun subscribeToNFTCollections() {
|
||||
combine(
|
||||
flow = getNFTCollectionsUseCase(params.userWalletId),
|
||||
flow2 = searchManager.query.distinctUntilChanged(),
|
||||
) { nftCollections, query ->
|
||||
_state.update {
|
||||
UpdateDataStateTransformer(
|
||||
nftCollections = nftCollections,
|
||||
searchQuery = query,
|
||||
onReceiveClick = ::onReceiveClick,
|
||||
onRetryClick = ::onRetryClick,
|
||||
onExpandCollectionClick = ::onExpandCollectionClick,
|
||||
onRetryAssetsClick = ::onRetryAssetsClick,
|
||||
onAssetClick = ::onAssetClick,
|
||||
initialSearchBarFactory = ::getInitialSearchBar,
|
||||
).transform(it)
|
||||
}
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun onSearchQueryChange(newQuery: String) {
|
||||
modelScope.launch {
|
||||
_state.update { UpdateSearchQueryTransformer(newQuery).transform(it) }
|
||||
|
||||
searchManager.update(newQuery)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getInitialSearchBar(): SearchBarUM = SearchBarUM(
|
||||
placeholderText = resourceReference(R.string.common_search),
|
||||
query = "",
|
||||
isActive = false,
|
||||
onQueryChange = ::onSearchQueryChange,
|
||||
onActiveChange = ::toggleSearchBar,
|
||||
)
|
||||
|
||||
private fun toggleSearchBar(isActive: Boolean) {
|
||||
_state.update {
|
||||
ToggleSearchBarTransformer(isActive).transform(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onExpandCollectionClick(collection: NFTCollection) {
|
||||
_state.update {
|
||||
ChangeCollectionExpandedStateTransformer(
|
||||
collectionId = collection.id,
|
||||
onFirstExpanded = { onFirstExpanded(collection) },
|
||||
).transform(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onFirstExpanded(collection: NFTCollection) {
|
||||
loadCollectionAssets(collection)
|
||||
}
|
||||
|
||||
private fun onRetryAssetsClick(collection: NFTCollection) {
|
||||
loadCollectionAssets(collection)
|
||||
}
|
||||
|
||||
private fun onRetryClick() {
|
||||
// TODO refresh all
|
||||
}
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
private fun onAssetClick(asset: NFTAsset) {
|
||||
// TODO move to details
|
||||
}
|
||||
|
||||
private fun onReceiveClick() {
|
||||
// TODO move to receive
|
||||
}
|
||||
|
||||
private fun navigateBack() {
|
||||
router.pop()
|
||||
}
|
||||
|
||||
private fun loadCollectionAssets(collection: NFTCollection) {
|
||||
modelScope.launch {
|
||||
fetchNFTCollectionAssetsUseCase(
|
||||
userWalletId = params.userWalletId,
|
||||
network = collection.network,
|
||||
collectionId = collection.id,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -13,12 +13,15 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import coil.compose.SubcomposeAsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconTopBadge
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
|
@ -28,13 +31,14 @@ import com.tangem.core.ui.res.TangemThemePreview
|
|||
import com.tangem.features.nft.collections.entity.NFTCollectionAssetsListUM
|
||||
import com.tangem.features.nft.collections.entity.NFTCollectionUM
|
||||
import com.tangem.features.nft.impl.R
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
private const val CHEVRON_ROTATION_EXPANDED = 180f
|
||||
private const val CHEVRON_ROTATION_COLLAPSED = 0f
|
||||
|
||||
@Composable
|
||||
internal fun NFTCollection(state: NFTCollectionUM, modifier: Modifier = Modifier) {
|
||||
val isExpanded = state.assets is NFTCollectionAssetsListUM.Expanded
|
||||
val isExpanded = state.isExpanded
|
||||
|
||||
Column(
|
||||
modifier = modifier,
|
||||
|
|
@ -87,8 +91,12 @@ private fun Logo(state: NFTCollectionUM) {
|
|||
SubcomposeAsyncImage(
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterStart)
|
||||
.size(TangemTheme.dimens.size36),
|
||||
model = state.logoUrl,
|
||||
.size(TangemTheme.dimens.size36)
|
||||
.clip(TangemTheme.shapes.roundedCorners8),
|
||||
model = ImageRequest.Builder(LocalContext.current)
|
||||
.data(state.logoUrl)
|
||||
.crossfade(true)
|
||||
.build(),
|
||||
loading = {
|
||||
RectangleShimmer(radius = TangemTheme.dimens.radius8)
|
||||
},
|
||||
|
|
@ -99,6 +107,7 @@ private fun Logo(state: NFTCollectionUM) {
|
|||
.background(TangemTheme.colors.field.primary),
|
||||
)
|
||||
},
|
||||
contentScale = ContentScale.Crop,
|
||||
contentDescription = null,
|
||||
)
|
||||
CurrencyIconTopBadge(
|
||||
|
|
@ -160,7 +169,8 @@ private class NFTCollectionProvider : CollectionPreviewParameterProvider<NFTColl
|
|||
logoUrl = "",
|
||||
networkIconId = R.drawable.img_eth_22,
|
||||
description = TextReference.Str("3 items"),
|
||||
assets = NFTCollectionAssetsListUM.Collapsed,
|
||||
assets = NFTCollectionAssetsListUM.Content(persistentListOf()),
|
||||
isExpanded = false,
|
||||
onExpandClick = { },
|
||||
),
|
||||
NFTCollectionUM(
|
||||
|
|
@ -169,10 +179,11 @@ private class NFTCollectionProvider : CollectionPreviewParameterProvider<NFTColl
|
|||
logoUrl = "",
|
||||
networkIconId = R.drawable.img_eth_22,
|
||||
description = TextReference.Str("3 items"),
|
||||
assets = NFTCollectionAssetsListUM.Expanded.Loading(
|
||||
assets = NFTCollectionAssetsListUM.Loading(
|
||||
itemsCount = 3,
|
||||
),
|
||||
onExpandClick = { },
|
||||
isExpanded = true,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -2,7 +2,6 @@ package com.tangem.features.nft.collections.ui
|
|||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
|
|
@ -10,11 +9,14 @@ import androidx.compose.material3.Text
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import coil.compose.SubcomposeAsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.components.SpacerH12
|
||||
import com.tangem.core.ui.components.SpacerH2
|
||||
|
|
@ -26,13 +28,16 @@ import com.tangem.features.nft.collections.entity.NFTSalePriceUM
|
|||
@Composable
|
||||
internal fun NFTCollectionAsset(state: NFTCollectionAssetUM, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.clickable { state.onItemClick() },
|
||||
modifier = modifier,
|
||||
) {
|
||||
SubcomposeAsyncImage(
|
||||
modifier = Modifier
|
||||
.aspectRatio(1f),
|
||||
model = state.imageUrl,
|
||||
.aspectRatio(1f)
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium),
|
||||
model = ImageRequest.Builder(LocalContext.current)
|
||||
.data(state.imageUrl)
|
||||
.crossfade(true)
|
||||
.build(),
|
||||
loading = {
|
||||
RectangleShimmer(radius = TangemTheme.dimens.radius16)
|
||||
},
|
||||
|
|
@ -43,6 +48,7 @@ internal fun NFTCollectionAsset(state: NFTCollectionAssetUM, modifier: Modifier
|
|||
.background(TangemTheme.colors.field.primary),
|
||||
)
|
||||
},
|
||||
contentScale = ContentScale.Crop,
|
||||
contentDescription = null,
|
||||
)
|
||||
SpacerH12()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
package com.tangem.features.nft.collections.ui
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.nft.collections.entity.NFTCollectionsStateUM
|
||||
import com.tangem.features.nft.collections.entity.NFTCollectionsUM
|
||||
import com.tangem.features.nft.impl.R
|
||||
|
||||
@Composable
|
||||
internal fun NFTCollections(state: NFTCollectionsStateUM, modifier: Modifier = Modifier) {
|
||||
BackHandler(onBack = state.onBackClick)
|
||||
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
containerColor = TangemTheme.colors.background.secondary,
|
||||
topBar = {
|
||||
AppBarWithBackButton(
|
||||
modifier = Modifier.statusBarsPadding(),
|
||||
onBackClick = state.onBackClick,
|
||||
text = stringResourceSafe(id = R.string.nft_collections_title),
|
||||
iconRes = R.drawable.ic_back_24,
|
||||
)
|
||||
},
|
||||
content = { innerPadding ->
|
||||
AnimatedContent(
|
||||
targetState = state.content,
|
||||
contentKey = { it::class },
|
||||
label = "NFT Collections",
|
||||
) {
|
||||
val contentModifier = Modifier
|
||||
.padding(innerPadding)
|
||||
.fillMaxSize()
|
||||
when (val content = it) {
|
||||
is NFTCollectionsUM.Content -> NFTCollectionsContent(content, contentModifier)
|
||||
is NFTCollectionsUM.Empty -> NFTCollectionsEmpty(content, contentModifier)
|
||||
is NFTCollectionsUM.Failed -> NFTCollectionsFailed(content, contentModifier)
|
||||
is NFTCollectionsUM.Loading -> Unit
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -1,12 +1,16 @@
|
|||
package com.tangem.features.nft.collections.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
|
|
@ -24,17 +28,12 @@ import com.tangem.core.ui.extensions.stringResourceSafe
|
|||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.nft.collections.entity.*
|
||||
import com.tangem.features.nft.collections.entity.NFTCollectionAssetUM
|
||||
import com.tangem.features.nft.collections.entity.NFTCollectionAssetsListUM
|
||||
import com.tangem.features.nft.collections.entity.NFTCollectionUM
|
||||
import com.tangem.features.nft.collections.entity.NFTCollectionsUM
|
||||
import com.tangem.features.nft.collections.entity.NFTSalePriceUM
|
||||
import com.tangem.features.nft.impl.R
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
internal fun NFTCollectionsContent(state: NFTCollectionsUM.Content, modifier: Modifier = Modifier) {
|
||||
internal fun NFTCollectionsContent(content: NFTCollectionsUM.Content, modifier: Modifier = Modifier) {
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
Box(
|
||||
|
|
@ -48,29 +47,34 @@ internal fun NFTCollectionsContent(state: NFTCollectionsUM.Content, modifier: Mo
|
|||
),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
SearchBar(
|
||||
state = state.search,
|
||||
state = content.search,
|
||||
colors = TangemSearchBarDefaults.secondaryTextFieldColors,
|
||||
)
|
||||
state.warnings.fastForEach {
|
||||
NFTCollectionWarning(
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing16),
|
||||
state = it,
|
||||
)
|
||||
content.warnings.fastForEach {
|
||||
key(it.id) {
|
||||
NFTCollectionWarning(
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing16),
|
||||
state = it,
|
||||
)
|
||||
}
|
||||
}
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
top = TangemTheme.dimens.spacing16,
|
||||
bottom = TangemTheme.dimens.spacing60,
|
||||
)
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(TangemTheme.colors.background.primary),
|
||||
state = listState,
|
||||
) {
|
||||
state.collections.fastForEach { collection ->
|
||||
content.collections.fastForEach { collection ->
|
||||
item(key = collection.id) {
|
||||
NFTCollection(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
|
|
@ -78,23 +82,26 @@ internal fun NFTCollectionsContent(state: NFTCollectionsUM.Content, modifier: Mo
|
|||
)
|
||||
}
|
||||
when (val assets = collection.assets) {
|
||||
is NFTCollectionAssetsListUM.Collapsed -> Unit
|
||||
is NFTCollectionAssetsListUM.Expanded.Loading -> {
|
||||
is NFTCollectionAssetsListUM.Init -> Unit
|
||||
is NFTCollectionAssetsListUM.Loading -> {
|
||||
assetsListLoading(
|
||||
collectionId = collection.id,
|
||||
content = assets,
|
||||
expanded = collection.isExpanded,
|
||||
)
|
||||
}
|
||||
is NFTCollectionAssetsListUM.Expanded.Failed -> {
|
||||
is NFTCollectionAssetsListUM.Failed -> {
|
||||
assetsListFailed(
|
||||
collectionId = collection.id,
|
||||
content = assets,
|
||||
expanded = collection.isExpanded,
|
||||
)
|
||||
}
|
||||
is NFTCollectionAssetsListUM.Expanded.Content -> {
|
||||
is NFTCollectionAssetsListUM.Content -> {
|
||||
assetsListContent(
|
||||
collectionId = collection.id,
|
||||
content = assets,
|
||||
expanded = collection.isExpanded,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -106,14 +113,15 @@ internal fun NFTCollectionsContent(state: NFTCollectionsUM.Content, modifier: Mo
|
|||
.fillMaxWidth()
|
||||
.align(Alignment.BottomCenter),
|
||||
text = stringResourceSafe(R.string.nft_collections_receive),
|
||||
onClick = { },
|
||||
onClick = content.onReceiveClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.assetsListLoading(
|
||||
collectionId: String,
|
||||
content: NFTCollectionAssetsListUM.Expanded.Loading,
|
||||
content: NFTCollectionAssetsListUM.Loading,
|
||||
expanded: Boolean,
|
||||
) {
|
||||
val itemsCount = content.itemsCount
|
||||
val rowCount = (itemsCount + 1) / 2
|
||||
|
|
@ -121,60 +129,71 @@ private fun LazyListScope.assetsListLoading(
|
|||
item(
|
||||
key = "loading_${collectionId}_$rowIndex",
|
||||
) {
|
||||
val paddingValues = PaddingValues(
|
||||
start = TangemTheme.dimens.spacing6,
|
||||
top = TangemTheme.dimens.spacing6,
|
||||
end = TangemTheme.dimens.spacing6,
|
||||
bottom = TangemTheme.dimens.spacing20,
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(TangemTheme.dimens.spacing6)
|
||||
.animateItem(),
|
||||
) {
|
||||
NFTCollectionAssetLoading(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(paddingValues),
|
||||
AnimatedVisibility(visible = expanded) {
|
||||
val paddingValues = PaddingValues(
|
||||
start = TangemTheme.dimens.spacing6,
|
||||
top = TangemTheme.dimens.spacing6,
|
||||
end = TangemTheme.dimens.spacing6,
|
||||
bottom = TangemTheme.dimens.spacing20,
|
||||
)
|
||||
if (rowIndex == rowCount - 1 && itemsCount % 2 != 0) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(paddingValues),
|
||||
)
|
||||
} else {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(TangemTheme.dimens.spacing6)
|
||||
.animateContentSize(),
|
||||
) {
|
||||
NFTCollectionAssetLoading(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(paddingValues),
|
||||
)
|
||||
if (rowIndex == rowCount - 1 && itemsCount % 2 != 0) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(paddingValues),
|
||||
)
|
||||
} else {
|
||||
NFTCollectionAssetLoading(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(paddingValues),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.assetsListFailed(collectionId: String, content: NFTCollectionAssetsListUM.Expanded.Failed) {
|
||||
private fun LazyListScope.assetsListFailed(
|
||||
collectionId: String,
|
||||
content: NFTCollectionAssetsListUM.Failed,
|
||||
expanded: Boolean,
|
||||
) {
|
||||
item(
|
||||
key = "failed_$collectionId",
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(TangemTheme.dimens.size142),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
UnableToLoadData(
|
||||
onRetryClick = content.onRetryClick,
|
||||
)
|
||||
AnimatedVisibility(visible = expanded) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(TangemTheme.dimens.size142)
|
||||
.animateContentSize(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
UnableToLoadData(
|
||||
onRetryClick = content.onRetryClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.assetsListContent(
|
||||
collectionId: String,
|
||||
content: NFTCollectionAssetsListUM.Expanded.Content,
|
||||
content: NFTCollectionAssetsListUM.Content,
|
||||
expanded: Boolean,
|
||||
) {
|
||||
val items = content.items
|
||||
val itemsCount = items.size
|
||||
|
|
@ -185,36 +204,43 @@ private fun LazyListScope.assetsListContent(
|
|||
item(
|
||||
key = "content_${collectionId}_${item1.id}_${item2?.id}",
|
||||
) {
|
||||
val paddingValues = PaddingValues(
|
||||
start = TangemTheme.dimens.spacing6,
|
||||
top = TangemTheme.dimens.spacing6,
|
||||
end = TangemTheme.dimens.spacing6,
|
||||
bottom = TangemTheme.dimens.spacing20,
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(TangemTheme.dimens.spacing6)
|
||||
.animateItem(),
|
||||
) {
|
||||
NFTCollectionAsset(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(paddingValues),
|
||||
state = item1,
|
||||
AnimatedVisibility(visible = expanded) {
|
||||
val paddingValues = PaddingValues(
|
||||
start = TangemTheme.dimens.spacing6,
|
||||
top = TangemTheme.dimens.spacing6,
|
||||
end = TangemTheme.dimens.spacing6,
|
||||
bottom = TangemTheme.dimens.spacing20,
|
||||
)
|
||||
if (item2 == null) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(paddingValues),
|
||||
)
|
||||
} else {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(TangemTheme.dimens.spacing6)
|
||||
.animateContentSize(),
|
||||
) {
|
||||
NFTCollectionAsset(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.clickable { item1.onItemClick() }
|
||||
.padding(paddingValues),
|
||||
state = item2,
|
||||
state = item1,
|
||||
)
|
||||
if (item2 == null) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(paddingValues),
|
||||
)
|
||||
} else {
|
||||
NFTCollectionAsset(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.clickable { item2.onItemClick() }
|
||||
.padding(paddingValues),
|
||||
state = item2,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -228,7 +254,7 @@ private fun LazyListScope.assetsListContent(
|
|||
private fun Preview_NFTCollectionsContent() {
|
||||
TangemThemePreview {
|
||||
NFTCollectionsContent(
|
||||
state = NFTCollectionsUM.Content(
|
||||
content = NFTCollectionsUM.Content(
|
||||
search = SearchBarUM(
|
||||
placeholderText = resourceReference(R.string.common_search),
|
||||
query = "",
|
||||
|
|
@ -243,7 +269,8 @@ private fun Preview_NFTCollectionsContent() {
|
|||
logoUrl = "",
|
||||
networkIconId = R.drawable.img_eth_22,
|
||||
description = TextReference.Str("3 items"),
|
||||
assets = NFTCollectionAssetsListUM.Collapsed,
|
||||
assets = NFTCollectionAssetsListUM.Content(persistentListOf()),
|
||||
isExpanded = false,
|
||||
onExpandClick = { },
|
||||
),
|
||||
NFTCollectionUM(
|
||||
|
|
@ -252,9 +279,10 @@ private fun Preview_NFTCollectionsContent() {
|
|||
logoUrl = "",
|
||||
networkIconId = R.drawable.img_eth_22,
|
||||
description = TextReference.Str("3 items"),
|
||||
assets = NFTCollectionAssetsListUM.Expanded.Loading(
|
||||
assets = NFTCollectionAssetsListUM.Loading(
|
||||
itemsCount = 1,
|
||||
),
|
||||
isExpanded = true,
|
||||
onExpandClick = { },
|
||||
),
|
||||
NFTCollectionUM(
|
||||
|
|
@ -263,9 +291,10 @@ private fun Preview_NFTCollectionsContent() {
|
|||
logoUrl = "",
|
||||
networkIconId = R.drawable.img_eth_22,
|
||||
description = TextReference.Str("3 items"),
|
||||
assets = NFTCollectionAssetsListUM.Expanded.Failed(
|
||||
assets = NFTCollectionAssetsListUM.Failed(
|
||||
onRetryClick = { },
|
||||
),
|
||||
isExpanded = true,
|
||||
onExpandClick = { },
|
||||
),
|
||||
NFTCollectionUM(
|
||||
|
|
@ -274,7 +303,7 @@ private fun Preview_NFTCollectionsContent() {
|
|||
logoUrl = "",
|
||||
networkIconId = R.drawable.img_eth_22,
|
||||
description = TextReference.Str("3 items"),
|
||||
assets = NFTCollectionAssetsListUM.Expanded.Content(
|
||||
assets = NFTCollectionAssetsListUM.Content(
|
||||
items = persistentListOf(
|
||||
NFTCollectionAssetUM(
|
||||
id = "item1",
|
||||
|
|
@ -299,11 +328,13 @@ private fun Preview_NFTCollectionsContent() {
|
|||
),
|
||||
),
|
||||
),
|
||||
isExpanded = true,
|
||||
onExpandClick = { },
|
||||
),
|
||||
),
|
||||
warnings = persistentListOf(
|
||||
NFTCollectionsWarningUM(
|
||||
id = "loading troubles",
|
||||
config = NotificationConfig(
|
||||
title = TextReference.Res(R.string.nft_collections_warning_title),
|
||||
subtitle = TextReference.Res(R.string.nft_collections_warning_subtitle),
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ internal fun NFTCollectionsFailed(state: NFTCollectionsUM.Failed, modifier: Modi
|
|||
.fillMaxWidth()
|
||||
.align(Alignment.BottomCenter),
|
||||
text = stringResourceSafe(R.string.nft_collections_receive),
|
||||
onClick = { },
|
||||
onClick = state.onReceiveClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue