diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 68238443cf..16dff9ff2e 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -495,6 +495,10 @@ Pull this up or tap the search bar to add tokens directly from the market Add tokens NFC is not available on your device + NFT collections + Tap here to receive first NFT + %1$d NFTs in %2$d collection + Unable to load the data Set up a single access code to protect all your devices. Protect Set an individual access code for each card or ring later. diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/nft/WalletNFTItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/nft/WalletNFTItem.kt new file mode 100644 index 0000000000..9bbb13232a --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/nft/WalletNFTItem.kt @@ -0,0 +1,433 @@ +package com.tangem.core.ui.components.nft + +import android.content.res.Configuration +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex +import coil.compose.SubcomposeAsyncImage +import com.tangem.core.ui.R +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.TextShimmer +import com.tangem.core.ui.components.nft.state.WalletNFTItemUM +import com.tangem.core.ui.components.nft.state.WalletNFTItemUM.Content.CollectionPreview +import com.tangem.core.ui.components.text.applyBladeBrush +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.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +@Composable +fun WalletNFTItem(state: WalletNFTItemUM, onClick: () -> Unit) { + when (state) { + is WalletNFTItemUM.Hidden -> {} + is WalletNFTItemUM.Empty -> WalletNFTItemEmpty( + onClick = onClick, + ) + is WalletNFTItemUM.Failed -> WalletNFTItemFailed() + is WalletNFTItemUM.Loading -> WalletNFTItemLoading() + + is WalletNFTItemUM.Content -> WalletNFTItemContent( + previews = state.previews, + collectionsCount = state.collectionsCount, + assetsCount = state.assetsCount, + isFlickering = state.isFlickering, + onClick = onClick, + ) + } +} + +@Composable +fun WalletNFTItemEmpty(onClick: () -> Unit) { + RowContentContainer( + icon = { + Image( + modifier = Modifier, + painter = painterResource(R.drawable.ic_nft_empty_36), + contentDescription = null, + ) + }, + text = { + Text( + text = stringResourceSafe(R.string.nft_wallet_title), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.primary1, + ) + Text( + text = stringResourceSafe(R.string.nft_wallet_receive_nft), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + }, + action = { + ChevronArrow() + }, + onClick = onClick, + ) +} + +@Composable +fun WalletNFTItemContent( + onClick: () -> Unit, + previews: ImmutableList, + collectionsCount: Int, + assetsCount: Int, + isFlickering: Boolean, +) { + RowContentContainer( + icon = { + CollectionsPreviews(previews) + }, + text = { + Text( + text = stringResourceSafe(R.string.nft_wallet_title), + style = TangemTheme.typography.subtitle2.applyBladeBrush( + isEnabled = isFlickering, + textColor = TangemTheme.colors.text.primary1, + ), + ) + Text( + text = stringResourceSafe( + id = R.string.nft_wallet_count, + assetsCount, + collectionsCount, + ), + style = TangemTheme.typography.caption2.applyBladeBrush( + isEnabled = isFlickering, + textColor = TangemTheme.colors.text.tertiary, + ), + ) + }, + action = { + ChevronArrow() + }, + onClick = onClick, + ) +} + +@Composable +fun WalletNFTItemFailed() { + RowContentContainer( + icon = { + CollectionsPreviewsPlaceholder() + }, + text = { + Text( + text = stringResourceSafe(R.string.nft_wallet_title), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + ) + Text( + text = stringResourceSafe(R.string.nft_wallet_unable_to_load), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + }, + ) +} + +@Composable +fun WalletNFTItemLoading() { + RowContentContainer( + icon = { + CollectionsPreviewsPlaceholder() + }, + text = { + TextShimmer( + modifier = Modifier.width(TangemTheme.dimens.size110), + style = TangemTheme.typography.caption2, + textSizeHeight = true, + ) + TextShimmer( + modifier = Modifier + .width(TangemTheme.dimens.size80) + .padding(top = TangemTheme.dimens.spacing4), + style = TangemTheme.typography.caption2, + textSizeHeight = true, + ) + }, + ) +} + +@Composable +private fun CollectionsPreviewsPlaceholder() { + RectangleShimmer( + modifier = Modifier + .size(TangemTheme.dimens.size36), + radius = TangemTheme.dimens.radius8, + ) +} + +@Composable +@Suppress("MagicNumber") +private fun BoxScope.CollectionsPreviews(urls: ImmutableList) { + val modifiers = when (urls.size) { + 1 -> previews1Modifiers() + 2 -> previews2Modifiers() + 3 -> previews3Modifiers() + else -> previews4Modifiers() + } + Box( + modifier = Modifier + .size(TangemTheme.dimens.size36), + ) { + urls.take(modifiers.size).forEachIndexed { index, s -> + val modifier = modifiers[index] + when (s) { + is CollectionPreview.Image -> { + SubcomposeAsyncImage( + modifier = modifier, + model = s, + loading = { + RectangleShimmer(radius = 0.dp) + }, + contentDescription = null, + ) + } + is CollectionPreview.More -> { + Image( + modifier = modifier + .background(TangemTheme.colors.stroke.primary), + painter = painterResource(R.drawable.ic_nft_preview_more_16), + contentDescription = null, + ) + } + } + } + } +} + +@Composable +private fun BoxScope.previews1Modifiers(): List = listOf( + // single image + Modifier + .size(TangemTheme.dimens.size36) + .clip(RoundedCornerShape(TangemTheme.dimens.radius6)), +) + +@Composable +private fun BoxScope.previews2Modifiers(): List = listOf( + Modifier + .size(TangemTheme.dimens.size22) + .padding( + top = TangemTheme.dimens.spacing1, + ) + .clip(RoundedCornerShape(TangemTheme.dimens.radius6)) + .align(Alignment.TopStart), + Modifier + .zIndex(1f) + .clip(RoundedCornerShape(TangemTheme.dimens.radius6)) + .background(TangemTheme.colors.background.primary) + .padding( + top = TangemTheme.dimens.spacing2, + start = TangemTheme.dimens.spacing2, + ) + .size(TangemTheme.dimens.size22) + .clip(RoundedCornerShape(TangemTheme.dimens.radius6)) + .align(Alignment.BottomEnd), +) + +@Composable +private fun BoxScope.previews3Modifiers(): List = listOf( + Modifier + .padding( + top = TangemTheme.dimens.spacing1, + bottom = TangemTheme.dimens.spacing1, + ) + .size(TangemTheme.dimens.size16) + .clip(RoundedCornerShape(5.dp)) + .align(Alignment.TopStart), + Modifier + .zIndex(1f) + .padding( + top = TangemTheme.dimens.radius4, + ) + .clip(RoundedCornerShape(TangemTheme.dimens.radius6)) + .background(TangemTheme.colors.background.primary) + .padding( + start = TangemTheme.dimens.spacing2, + top = TangemTheme.dimens.spacing2, + end = TangemTheme.dimens.spacing1, + bottom = TangemTheme.dimens.spacing2, + ) + .size(TangemTheme.dimens.size18) + .clip(RoundedCornerShape(TangemTheme.dimens.radius6)) + .align(Alignment.TopEnd), + Modifier + .padding( + start = TangemTheme.dimens.spacing6, + bottom = TangemTheme.dimens.spacing1, + ) + .clip(RoundedCornerShape(TangemTheme.dimens.radius4)) + .size(TangemTheme.dimens.size14) + .align(Alignment.BottomStart), +) + +@Composable +private fun BoxScope.previews4Modifiers(): List = listOf( + Modifier + .clip(RoundedCornerShape(TangemTheme.dimens.radius4)) + .size(TangemTheme.dimens.size16) + .align(Alignment.TopStart), + Modifier + .clip(RoundedCornerShape(TangemTheme.dimens.radius4)) + .size(TangemTheme.dimens.size16) + .align(Alignment.TopEnd), + Modifier + .clip(RoundedCornerShape(TangemTheme.dimens.radius4)) + .size(TangemTheme.dimens.size16) + .align(Alignment.BottomStart), + Modifier + .clip(RoundedCornerShape(TangemTheme.dimens.radius4)) + .size(TangemTheme.dimens.size16) + .align(Alignment.BottomEnd), +) + +@Composable +private fun ChevronArrow() { + Icon( + modifier = Modifier, + painter = painterResource(R.drawable.ic_chevron_right_24), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + ) +} + +@Composable +private fun RowContentContainer( + modifier: Modifier = Modifier, + onClick: () -> Unit = {}, + icon: @Composable BoxScope.() -> Unit, + text: @Composable ColumnScope.() -> Unit, + action: @Composable BoxScope.() -> Unit = {}, +) { + Card( + modifier = modifier.fillMaxWidth(), + shape = RoundedCornerShape(TangemTheme.dimens.radius16), + colors = CardDefaults.cardColors( + containerColor = TangemTheme.colors.background.primary, + contentColor = TangemTheme.colors.text.primary1, + disabledContainerColor = TangemTheme.colors.background.primary, + disabledContentColor = TangemTheme.colors.text.primary1, + ), + onClick = onClick, + enabled = true, + ) { + Row( + modifier = Modifier + .padding( + horizontal = TangemTheme.dimens.spacing12, + vertical = TangemTheme.dimens.spacing16, + ), + verticalAlignment = Alignment.CenterVertically, + ) { + Row( + modifier = modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Box( + contentAlignment = Alignment.Center, + content = icon, + ) + Column( + modifier = Modifier + .padding( + horizontal = TangemTheme.dimens.spacing12, + ) + .weight(1f), + horizontalAlignment = Alignment.Start, + content = text, + ) + Box( + modifier = Modifier.heightIn(min = TangemTheme.dimens.size24), + contentAlignment = Alignment.CenterEnd, + content = action, + ) + } + } + } +} + +@Preview(widthDp = 360) +@Preview(widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_WalletNFTItem_InLight(@PreviewParameter(WalletNFTItemProvider::class) state: WalletNFTItemUM) { + TangemThemePreview { + WalletNFTItem( + state = state, + onClick = {}, + ) + } +} + +private class WalletNFTItemProvider : CollectionPreviewParameterProvider( + collection = listOf( + WalletNFTItemUM.Empty, + WalletNFTItemUM.Loading, + WalletNFTItemUM.Failed, + WalletNFTItemUM.Content( + previews = persistentListOf( + CollectionPreview.Image("img1"), + ), + assetsCount = 125, + collectionsCount = 11, + isFlickering = true, + ), + WalletNFTItemUM.Content( + previews = persistentListOf( + CollectionPreview.Image("img1"), + CollectionPreview.Image("img2"), + ), + assetsCount = 125, + collectionsCount = 11, + isFlickering = false, + ), + WalletNFTItemUM.Content( + previews = persistentListOf( + CollectionPreview.Image("img1"), + CollectionPreview.Image("img2"), + CollectionPreview.Image("img3"), + ), + assetsCount = 125, + collectionsCount = 11, + isFlickering = false, + ), + WalletNFTItemUM.Content( + previews = persistentListOf( + CollectionPreview.Image("img1"), + CollectionPreview.Image("img2"), + CollectionPreview.Image("img3"), + CollectionPreview.Image("img4"), + ), + assetsCount = 125, + collectionsCount = 11, + isFlickering = false, + ), + WalletNFTItemUM.Content( + previews = persistentListOf( + CollectionPreview.Image("img1"), + CollectionPreview.Image("img2"), + CollectionPreview.Image("img3"), + CollectionPreview.More, + ), + assetsCount = 125, + collectionsCount = 11, + isFlickering = true, + ), + ), +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/nft/state/WalletNFTItemUM.kt b/core/ui/src/main/java/com/tangem/core/ui/components/nft/state/WalletNFTItemUM.kt new file mode 100644 index 0000000000..458fd52e53 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/nft/state/WalletNFTItemUM.kt @@ -0,0 +1,29 @@ +package com.tangem.core.ui.components.nft.state + +import androidx.compose.runtime.Immutable +import kotlinx.collections.immutable.ImmutableList + +@Immutable +sealed class WalletNFTItemUM { + + data object Hidden : WalletNFTItemUM() + + data object Loading : WalletNFTItemUM() + + data object Empty : WalletNFTItemUM() + + data object Failed : WalletNFTItemUM() + + data class Content( + val previews: ImmutableList, + val collectionsCount: Int, + val assetsCount: Int, + val isFlickering: Boolean, + ) : WalletNFTItemUM() { + + sealed class CollectionPreview { + data class Image(val url: String) : CollectionPreview() + data object More : CollectionPreview() + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_nft_empty_36.xml b/core/ui/src/main/res/drawable/ic_nft_empty_36.xml new file mode 100644 index 0000000000..b246aaf6e1 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_nft_empty_36.xml @@ -0,0 +1,13 @@ + + + + diff --git a/core/ui/src/main/res/drawable/ic_nft_preview_more_16.xml b/core/ui/src/main/res/drawable/ic_nft_preview_more_16.xml new file mode 100644 index 0000000000..e7a9118384 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_nft_preview_more_16.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt index cbf8bafda7..764f57f277 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt @@ -106,9 +106,7 @@ internal class WalletSettingsModel @Inject constructor( renameWallet = { openRenameWalletDialog(userWallet, dialogNavigation) }, isNFTFeatureEnabled = isNFTFeatureEnabled, isNFTEnabled = isNFTEnabled, - onCheckedNFTChange = { isChecked -> - onCheckedNFTChange(isChecked) - }, + onCheckedNFTChange = ::onCheckedNFTChange, forgetWallet = { val message = DialogMessage( message = resourceReference(R.string.user_wallet_list_delete_prompt),