Updated on 2026-08-14

This commit is contained in:
Tangem 2025-03-26 22:17:53 +05:00
parent b0fc7b3057
commit 80d00dfb6b
26 changed files with 1003 additions and 56 deletions

View file

@ -14,6 +14,11 @@ fun SpacerH(height: Dp, modifier: Modifier = Modifier) {
Spacer(modifier = modifier.height(height))
}
@Composable
fun SpacerH2(modifier: Modifier = Modifier) {
SpacerH(2.dp, modifier)
}
@Composable
fun SpacerH4(modifier: Modifier = Modifier) {
SpacerH(4.dp, modifier)

View file

@ -1,4 +1,4 @@
package com.tangem.features.markets.tokenlist.impl.ui.components
package com.tangem.core.ui.components
import android.content.res.Configuration
import androidx.compose.foundation.layout.Arrangement
@ -8,16 +8,16 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.R
import com.tangem.core.ui.components.buttons.SecondarySmallButton
import com.tangem.core.ui.components.buttons.SmallButtonConfig
import com.tangem.core.ui.extensions.resourceReference
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.markets.impl.R
@Composable
internal fun UnableToLoadData(onRetryClick: () -> Unit, modifier: Modifier = Modifier) {
fun UnableToLoadData(onRetryClick: () -> Unit, modifier: Modifier = Modifier) {
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),

View file

@ -109,7 +109,7 @@ private fun BoxScope.ContentIconContainer(
)
if (icon.topBadgeIconResId != null && shouldShowTopBadge) {
TopBadge(
CurrencyIconTopBadge(
modifier = Modifier
.offset(x = networkBadgeOffset, y = -networkBadgeOffset)
.align(Alignment.TopEnd),
@ -120,7 +120,7 @@ private fun BoxScope.ContentIconContainer(
}
if (icon.showCustomBadge) {
BottomBadge(
CurrencyIconBottomBadge(
modifier = Modifier.align(Alignment.BottomEnd),
)
}

View file

@ -0,0 +1,32 @@
package com.tangem.core.ui.components.currency.icon
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.ui.res.TangemTheme
@Composable
internal fun CurrencyIconBottomBadge(modifier: Modifier = Modifier) {
Box(
modifier = modifier
.size(TangemTheme.dimens.size12)
.background(
color = TangemTheme.colors.background.primary,
shape = CircleShape,
),
) {
Box(
modifier = Modifier
.padding(all = TangemTheme.dimens.spacing2)
.matchParentSize()
.background(
color = TangemTheme.colors.icon.informative,
shape = CircleShape,
),
)
}
}

View file

@ -14,7 +14,7 @@ import androidx.compose.ui.res.painterResource
import com.tangem.core.ui.res.TangemTheme
@Composable
internal fun TopBadge(
fun CurrencyIconTopBadge(
@DrawableRes iconResId: Int,
alpha: Float,
colorFilter: ColorFilter?,
@ -38,26 +38,4 @@ internal fun TopBadge(
contentDescription = null,
)
}
}
@Composable
internal fun BottomBadge(modifier: Modifier = Modifier) {
Box(
modifier = modifier
.size(TangemTheme.dimens.size12)
.background(
color = TangemTheme.colors.background.primary,
shape = CircleShape,
),
) {
Box(
modifier = Modifier
.padding(all = TangemTheme.dimens.spacing2)
.matchParentSize()
.background(
color = TangemTheme.colors.icon.informative,
shape = CircleShape,
),
)
}
}

View file

@ -11,6 +11,7 @@ import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusManager
@ -36,7 +37,11 @@ import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
@Composable
fun SearchBar(state: SearchBarUM, modifier: Modifier = Modifier, colors: TextFieldColors = TangemSearchBarColors) {
fun SearchBar(
state: SearchBarUM,
modifier: Modifier = Modifier,
colors: TextFieldColors = TangemSearchBarDefaults.defaultTextFieldColors,
) {
val keyboardController = LocalSoftwareKeyboardController.current
val focusManager = LocalFocusManager.current
val interactionSource = remember { MutableInteractionSource() }
@ -161,18 +166,30 @@ private fun ClearButton(
}
}
private val TangemSearchBarColors: TextFieldColors
@Composable
get() = TextFieldDefaults.colors().copy(
focusedContainerColor = TangemTheme.colors.field.primary,
unfocusedContainerColor = TangemTheme.colors.field.primary,
focusedTextColor = TangemTheme.colors.text.primary1,
unfocusedTextColor = TangemTheme.colors.text.primary1,
cursorColor = TangemTheme.colors.icon.primary1,
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
disabledIndicatorColor = Color.Transparent,
)
object TangemSearchBarDefaults {
val defaultTextFieldColors: TextFieldColors
@Composable @Stable get() = TextFieldDefaults.colors().copy(
focusedContainerColor = TangemTheme.colors.field.primary,
unfocusedContainerColor = TangemTheme.colors.field.primary,
focusedTextColor = TangemTheme.colors.text.primary1,
unfocusedTextColor = TangemTheme.colors.text.primary1,
cursorColor = TangemTheme.colors.icon.primary1,
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
disabledIndicatorColor = Color.Transparent,
)
val secondaryTextFieldColors: TextFieldColors
@Composable @Stable get() = TextFieldDefaults.colors().copy(
focusedContainerColor = TangemTheme.colors.field.focused,
unfocusedContainerColor = TangemTheme.colors.field.focused,
focusedTextColor = TangemTheme.colors.text.primary1,
unfocusedTextColor = TangemTheme.colors.text.primary1,
cursorColor = TangemTheme.colors.icon.primary1,
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
disabledIndicatorColor = Color.Transparent,
)
}
// region Previews
@Preview(widthDp = 328)

View file

@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="76dp"
android:height="76dp"
android:viewportWidth="76"
android:viewportHeight="76">
<path
android:pathData="M26.284,20.9C23.136,20.9 20.584,23.451 20.584,26.6C20.584,29.748 23.136,32.3 26.284,32.3C29.432,32.3 31.984,29.748 31.984,26.6C31.984,23.451 29.432,20.9 26.284,20.9Z"
android:fillColor="#C9C9C9"/>
<path
android:pathData="M9.118,18.911L33.829,4.644C34.961,3.99 36.411,4.418 37.007,5.581C37.564,6.666 37.172,7.996 36.116,8.606L11.602,22.758V47.139L20.57,40.834C21.884,39.911 23.61,39.822 25.012,40.604L35.544,46.484L50.911,35.182C52.29,34.169 54.147,34.097 55.599,35.003L64.399,40.49V25.046C64.399,23.782 65.423,22.758 66.686,22.758C67.949,22.758 68.973,23.782 68.973,25.046V53.468C68.973,54.961 68.176,56.341 66.883,57.088L40.09,72.557C38.797,73.303 37.204,73.303 35.911,72.557L9.118,57.088C7.825,56.341 7.028,54.961 7.028,53.468V22.531C7.028,21.038 7.825,19.658 9.118,18.911ZM53.406,39.026L64.399,45.881V53.24L38,68.481L11.602,53.24V52.731L22.999,44.719L33.535,50.602C34.965,51.4 36.73,51.29 38.049,50.319L53.406,39.026Z"
android:fillColor="#C9C9C9"
android:fillType="evenOdd"/>
<path
android:pathData="M56.891,1.843C55.743,1.843 54.813,2.774 54.813,3.921V9.265H48.875C47.727,9.265 46.797,10.196 46.797,11.343C46.797,12.491 47.727,13.421 48.875,13.421H54.813V18.765C54.813,19.913 55.743,20.843 56.891,20.843C58.038,20.843 58.969,19.913 58.969,18.765V13.421H64.906C66.054,13.421 66.984,12.491 66.984,11.343C66.984,10.196 66.054,9.265 64.906,9.265H58.969V3.921C58.969,2.774 58.038,1.843 56.891,1.843Z"
android:fillColor="#C9C9C9"/>
</vector>

View file

@ -16,7 +16,7 @@ import com.tangem.common.ui.charts.state.MarketChartLook
import com.tangem.common.ui.charts.state.rememberMarketChartState
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM
import com.tangem.features.markets.tokenlist.impl.ui.components.UnableToLoadData
import com.tangem.core.ui.components.UnableToLoadData
@Composable
internal fun MarketTokenDetailsChart(

View file

@ -9,7 +9,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM
import com.tangem.features.markets.tokenlist.impl.ui.components.UnableToLoadData
import com.tangem.core.ui.components.UnableToLoadData
internal fun LazyListScope.tokenMarketDetailsBody(
state: MarketsTokenDetailsUM.Body,

View file

@ -10,6 +10,7 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
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.list.InfiniteListHandler

View file

@ -0,0 +1,9 @@
package com.tangem.features.nft.collections.entity
internal data class NFTCollectionAssetUM(
val id: String,
val name: String,
val imageUrl: String?,
val price: NFTSalePriceUM,
val onItemClick: () -> Unit,
)

View file

@ -0,0 +1,16 @@
package com.tangem.features.nft.collections.entity
import androidx.compose.runtime.Immutable
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()
}
}

View file

@ -0,0 +1,14 @@
package com.tangem.features.nft.collections.entity
import androidx.annotation.DrawableRes
import com.tangem.core.ui.extensions.TextReference
internal data class NFTCollectionUM(
val id: String,
val name: String,
@DrawableRes val networkIconId: Int,
val logoUrl: String?,
val description: TextReference,
val assets: NFTCollectionAssetsListUM,
val onExpandClick: () -> Unit,
)

View file

@ -0,0 +1,29 @@
package com.tangem.features.nft.collections.entity
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import kotlinx.collections.immutable.ImmutableList
@Immutable
internal sealed class NFTCollectionsUM {
data class Empty(
val onReceiveClick: () -> Unit,
) : NFTCollectionsUM()
data class Loading(
val onReceiveClick: () -> Unit,
) : NFTCollectionsUM()
data class Failed(
val onRetryClick: () -> Unit,
val onReceiveClick: () -> Unit,
) : NFTCollectionsUM()
data class Content(
val search: SearchBarUM,
val collections: ImmutableList<NFTCollectionUM>,
val warnings: ImmutableList<NFTCollectionsWarningUM>,
val onReceiveClick: () -> Unit,
) : NFTCollectionsUM()
}

View file

@ -0,0 +1,5 @@
package com.tangem.features.nft.collections.entity
import com.tangem.core.ui.components.notifications.NotificationConfig
internal data class NFTCollectionsWarningUM(val config: NotificationConfig)

View file

@ -0,0 +1,10 @@
package com.tangem.features.nft.collections.entity
import androidx.compose.runtime.Immutable
@Immutable
internal sealed class NFTSalePriceUM {
data object Loading : NFTSalePriceUM()
data object Failed : NFTSalePriceUM()
data class Content(val price: String) : NFTSalePriceUM()
}

View file

@ -0,0 +1,178 @@
package com.tangem.features.nft.collections.ui
import android.content.res.Configuration
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
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.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 com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.currency.icon.CurrencyIconTopBadge
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
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
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
Column(
modifier = modifier,
) {
Row(
modifier = Modifier
.clickable(onClick = state.onExpandClick)
.padding(
vertical = TangemTheme.dimens.spacing16,
horizontal = TangemTheme.dimens.spacing12,
),
verticalAlignment = Alignment.CenterVertically,
) {
Logo(state)
Text(state)
val rotation by animateFloatAsState(
targetValue = if (isExpanded) {
CHEVRON_ROTATION_EXPANDED
} else {
CHEVRON_ROTATION_COLLAPSED
},
label = "chevron_rotation",
)
Icon(
modifier = Modifier
.rotate(rotation)
.size(TangemTheme.dimens.size24),
painter = painterResource(id = R.drawable.ic_chevron_24),
tint = if (isExpanded) {
TangemTheme.colors.icon.primary1
} else {
TangemTheme.colors.icon.inactive
},
contentDescription = null,
)
}
}
}
@Composable
private fun Logo(state: NFTCollectionUM) {
val networkBadgeOffset = TangemTheme.dimens.spacing6
Box(
modifier = Modifier,
) {
SubcomposeAsyncImage(
modifier = Modifier
.align(Alignment.CenterStart)
.size(TangemTheme.dimens.size36),
model = state.logoUrl,
loading = {
RectangleShimmer(radius = TangemTheme.dimens.radius8)
},
error = {
Box(
modifier = Modifier
.clip(shape = TangemTheme.shapes.roundedCorners8)
.background(TangemTheme.colors.field.primary),
)
},
contentDescription = null,
)
CurrencyIconTopBadge(
modifier = Modifier
.offset(x = networkBadgeOffset, y = -networkBadgeOffset)
.align(Alignment.TopEnd),
iconResId = state.networkIconId,
alpha = 1f,
colorFilter = null,
)
}
}
@Composable
private fun RowScope.Text(state: NFTCollectionUM) {
Column(
modifier = Modifier
.weight(1f)
.padding(
horizontal = TangemTheme.dimens.spacing12,
),
) {
Text(
modifier = Modifier,
text = state.name,
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.primary1,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Text(
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing2),
text = state.description.resolveReference(),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
@Preview(widthDp = 360, showBackground = true)
@Preview(widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_NFTCollection(@PreviewParameter(NFTCollectionProvider::class) item: NFTCollectionUM) {
TangemThemePreview {
NFTCollection(
state = item,
)
}
}
private class NFTCollectionProvider : CollectionPreviewParameterProvider<NFTCollectionUM>(
collection = listOf(
NFTCollectionUM(
id = "item1",
name = "Nethers",
logoUrl = "",
networkIconId = R.drawable.img_eth_22,
description = TextReference.Str("3 items"),
assets = NFTCollectionAssetsListUM.Collapsed,
onExpandClick = { },
),
NFTCollectionUM(
id = "item2",
name = "Nethers",
logoUrl = "",
networkIconId = R.drawable.img_eth_22,
description = TextReference.Str("3 items"),
assets = NFTCollectionAssetsListUM.Expanded.Loading(
itemsCount = 3,
),
onExpandClick = { },
),
),
)

View file

@ -0,0 +1,100 @@
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
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.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 com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.SpacerH12
import com.tangem.core.ui.components.SpacerH2
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.nft.collections.entity.NFTCollectionAssetUM
import com.tangem.features.nft.collections.entity.NFTSalePriceUM
@Composable
internal fun NFTCollectionAsset(state: NFTCollectionAssetUM, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.clickable { state.onItemClick() },
) {
SubcomposeAsyncImage(
modifier = Modifier
.aspectRatio(1f),
model = state.imageUrl,
loading = {
RectangleShimmer(radius = TangemTheme.dimens.radius16)
},
error = {
Box(
modifier = Modifier
.clip(shape = TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.field.primary),
)
},
contentDescription = null,
)
SpacerH12()
Text(
text = state.name,
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.primary1,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
SpacerH2()
NFTSalePrice(
state = state.price,
)
}
}
@Preview(widthDp = 180, showBackground = true)
@Preview(widthDp = 180, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_NFTCollectionAsset(
@PreviewParameter(NFTCollectionAssetProvider::class) state: NFTCollectionAssetUM,
) {
TangemThemePreview {
NFTCollectionAsset(
state = state,
)
}
}
private class NFTCollectionAssetProvider : CollectionPreviewParameterProvider<NFTCollectionAssetUM>(
collection = listOf(
NFTCollectionAssetUM(
id = "item1",
name = "Nethers #0853",
imageUrl = "img",
price = NFTSalePriceUM.Loading,
onItemClick = { },
),
NFTCollectionAssetUM(
id = "item2",
name = "Nethers #0854",
imageUrl = "img",
price = NFTSalePriceUM.Failed,
onItemClick = { },
),
NFTCollectionAssetUM(
id = "item3",
name = "Nethers #0855",
imageUrl = "img",
price = NFTSalePriceUM.Content("0.05 ETH"),
onItemClick = { },
),
),
)

View file

@ -0,0 +1,47 @@
package com.tangem.features.nft.collections.ui
import android.content.res.Configuration
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
@Composable
internal fun NFTCollectionAssetLoading(modifier: Modifier = Modifier) {
Column(
modifier = modifier,
) {
RectangleShimmer(
modifier = Modifier
.aspectRatio(1f),
)
SpacerH12()
TextShimmer(
modifier = Modifier
.width(TangemTheme.dimens.size86),
style = TangemTheme.typography.subtitle2,
textSizeHeight = true,
)
SpacerH2()
TextShimmer(
modifier = Modifier
.width(TangemTheme.dimens.size48),
style = TangemTheme.typography.caption2,
textSizeHeight = true,
)
}
}
@Preview(widthDp = 180, showBackground = true)
@Preview(widthDp = 180, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_NFTCollectionAsset() {
TangemThemePreview {
NFTCollectionAssetLoading()
}
}

View file

@ -0,0 +1,14 @@
package com.tangem.features.nft.collections.ui
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.features.nft.collections.entity.NFTCollectionsWarningUM
@Composable
internal fun NFTCollectionWarning(state: NFTCollectionsWarningUM, modifier: Modifier = Modifier) {
Notification(
modifier = modifier,
config = state.config,
)
}

View file

@ -0,0 +1,318 @@
package com.tangem.features.nft.collections.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
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.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.util.fastForEach
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.UnableToLoadData
import com.tangem.core.ui.components.fields.SearchBar
import com.tangem.core.ui.components.fields.TangemSearchBarDefaults
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.resourceReference
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) {
val listState = rememberLazyListState()
Box(
modifier = modifier
.fillMaxSize()
.background(TangemTheme.colors.background.secondary)
.padding(
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
bottom = TangemTheme.dimens.spacing16,
),
) {
Column(
modifier = Modifier,
) {
SearchBar(
state = state.search,
colors = TangemSearchBarDefaults.secondaryTextFieldColors,
)
state.warnings.fastForEach {
NFTCollectionWarning(
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing16),
state = it,
)
}
LazyColumn(
modifier = Modifier
.padding(
top = TangemTheme.dimens.spacing16,
)
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.primary),
state = listState,
) {
state.collections.fastForEach { collection ->
item(key = collection.id) {
NFTCollection(
modifier = Modifier.fillMaxWidth(),
state = collection,
)
}
when (val assets = collection.assets) {
is NFTCollectionAssetsListUM.Collapsed -> Unit
is NFTCollectionAssetsListUM.Expanded.Loading -> {
assetsListLoading(
collectionId = collection.id,
content = assets,
)
}
is NFTCollectionAssetsListUM.Expanded.Failed -> {
assetsListFailed(
collectionId = collection.id,
content = assets,
)
}
is NFTCollectionAssetsListUM.Expanded.Content -> {
assetsListContent(
collectionId = collection.id,
content = assets,
)
}
}
}
}
}
PrimaryButton(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.BottomCenter),
text = stringResourceSafe(R.string.nft_collections_receive),
onClick = { },
)
}
}
private fun LazyListScope.assetsListLoading(
collectionId: String,
content: NFTCollectionAssetsListUM.Expanded.Loading,
) {
val itemsCount = content.itemsCount
val rowCount = (itemsCount + 1) / 2
repeat(rowCount) { rowIndex ->
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),
)
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) {
item(
key = "failed_$collectionId",
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(TangemTheme.dimens.size142),
contentAlignment = Alignment.Center,
) {
UnableToLoadData(
onRetryClick = content.onRetryClick,
)
}
}
}
private fun LazyListScope.assetsListContent(
collectionId: String,
content: NFTCollectionAssetsListUM.Expanded.Content,
) {
val items = content.items
val itemsCount = items.size
val rowCount = (itemsCount + 1) / 2
repeat(rowCount) { rowIndex ->
val item1 = items[rowIndex * 2]
val item2 = items.getOrNull(rowIndex * 2 + 1)
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,
)
if (item2 == null) {
Box(
modifier = Modifier
.weight(1f)
.padding(paddingValues),
)
} else {
NFTCollectionAsset(
modifier = Modifier
.weight(1f)
.padding(paddingValues),
state = item2,
)
}
}
}
}
}
@Suppress("LongMethod")
@Preview(widthDp = 360, showBackground = true)
@Preview(widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_NFTCollectionsContent() {
TangemThemePreview {
NFTCollectionsContent(
state = NFTCollectionsUM.Content(
search = SearchBarUM(
placeholderText = resourceReference(R.string.common_search),
query = "",
onQueryChange = {},
isActive = false,
onActiveChange = { },
),
collections = persistentListOf(
NFTCollectionUM(
id = "item1",
name = "Nethers",
logoUrl = "",
networkIconId = R.drawable.img_eth_22,
description = TextReference.Str("3 items"),
assets = NFTCollectionAssetsListUM.Collapsed,
onExpandClick = { },
),
NFTCollectionUM(
id = "item2",
name = "Nethers",
logoUrl = "",
networkIconId = R.drawable.img_eth_22,
description = TextReference.Str("3 items"),
assets = NFTCollectionAssetsListUM.Expanded.Loading(
itemsCount = 1,
),
onExpandClick = { },
),
NFTCollectionUM(
id = "item3",
name = "Nethers",
logoUrl = "",
networkIconId = R.drawable.img_eth_22,
description = TextReference.Str("3 items"),
assets = NFTCollectionAssetsListUM.Expanded.Failed(
onRetryClick = { },
),
onExpandClick = { },
),
NFTCollectionUM(
id = "item4",
name = "Nethers",
logoUrl = "",
networkIconId = R.drawable.img_eth_22,
description = TextReference.Str("3 items"),
assets = NFTCollectionAssetsListUM.Expanded.Content(
items = persistentListOf(
NFTCollectionAssetUM(
id = "item1",
name = "Nethers #0854",
imageUrl = "img",
price = NFTSalePriceUM.Content("0.05 ETH"),
onItemClick = { },
),
NFTCollectionAssetUM(
id = "item2",
name = "Nethers #0855",
imageUrl = "img",
price = NFTSalePriceUM.Loading,
onItemClick = { },
),
NFTCollectionAssetUM(
id = "item3",
name = "Nethers #0856",
imageUrl = "img",
price = NFTSalePriceUM.Failed,
onItemClick = { },
),
),
),
onExpandClick = { },
),
),
warnings = persistentListOf(
NFTCollectionsWarningUM(
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,
),
),
),
onReceiveClick = { },
),
)
}
}

View file

@ -0,0 +1,74 @@
package com.tangem.features.nft.collections.ui
import android.content.res.Configuration
import androidx.compose.foundation.Image
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.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.PrimaryButton
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.NFTCollectionsUM
import com.tangem.features.nft.impl.R
@Composable
internal fun NFTCollectionsEmpty(state: NFTCollectionsUM.Empty, modifier: Modifier = Modifier) {
Box(
modifier = modifier
.fillMaxSize()
.padding(TangemTheme.dimens.spacing16),
) {
Column(
modifier = Modifier
.padding(TangemTheme.dimens.spacing16)
.align(Alignment.Center),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Image(
painter = painterResource(R.drawable.ic_nft_placeholder_76),
contentDescription = null,
)
Text(
modifier = Modifier.padding(top = TangemTheme.dimens.spacing24),
text = stringResource(R.string.nft_collections_empty_title),
style = TangemTheme.typography.h3,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
)
Text(
modifier = Modifier.padding(top = TangemTheme.dimens.spacing8),
text = stringResource(R.string.nft_collections_empty_description),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
)
PrimaryButton(
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing48)
.wrapContentWidth(),
text = stringResourceSafe(R.string.nft_collections_receive),
onClick = state.onReceiveClick,
)
}
}
}
@Preview(widthDp = 360, showBackground = true)
@Preview(widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_NFTCollectionsEmpty() {
TangemThemePreview {
NFTCollectionsEmpty(
state = NFTCollectionsUM.Empty(
onReceiveClick = { },
),
)
}
}

View file

@ -0,0 +1,54 @@
package com.tangem.features.nft.collections.ui
import android.content.res.Configuration
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.UnableToLoadData
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.NFTCollectionsUM
import com.tangem.features.nft.impl.R
@Composable
internal fun NFTCollectionsFailed(state: NFTCollectionsUM.Failed, modifier: Modifier = Modifier) {
Box(
modifier = modifier
.fillMaxSize()
.padding(TangemTheme.dimens.spacing16),
) {
UnableToLoadData(
modifier = Modifier
.align(Alignment.Center),
onRetryClick = state.onRetryClick,
)
PrimaryButton(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.BottomCenter),
text = stringResourceSafe(R.string.nft_collections_receive),
onClick = { },
)
}
}
@Preview(widthDp = 360, showBackground = true)
@Preview(widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_NFTCollectionsFailed() {
TangemThemePreview {
NFTCollectionsFailed(
state = NFTCollectionsUM.Failed(
onRetryClick = { },
onReceiveClick = { },
),
)
}
}

View file

@ -0,0 +1,40 @@
package com.tangem.features.nft.collections.ui
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.nft.collections.entity.NFTSalePriceUM
@Composable
internal fun NFTSalePrice(state: NFTSalePriceUM, modifier: Modifier = Modifier) {
when (state) {
is NFTSalePriceUM.Content -> {
Text(
modifier = modifier,
text = state.price,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
is NFTSalePriceUM.Failed -> {
Text(
modifier = modifier,
text = "",
style = TangemTheme.typography.caption2,
)
}
is NFTSalePriceUM.Loading -> {
TextShimmer(
modifier = modifier
.width(TangemTheme.dimens.size48),
style = TangemTheme.typography.caption2,
)
}
}
}

View file

@ -6,11 +6,9 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.key
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
@ -18,6 +16,7 @@ import androidx.compose.ui.util.fastForEachIndexed
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.components.SpacerH12
import com.tangem.core.ui.components.fields.SearchBar
import com.tangem.core.ui.components.fields.TangemSearchBarDefaults
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.components.tokenlist.TokenListItem
@ -74,16 +73,7 @@ internal fun TokenList(state: TokenListUM, modifier: Modifier = Modifier) {
private fun SearchBar(searchBarUM: SearchBarUM) {
SearchBar(
state = searchBarUM,
colors = TextFieldDefaults.colors().copy(
focusedContainerColor = TangemTheme.colors.field.focused,
unfocusedContainerColor = TangemTheme.colors.field.focused,
focusedTextColor = TangemTheme.colors.text.primary1,
unfocusedTextColor = TangemTheme.colors.text.primary1,
cursorColor = TangemTheme.colors.icon.primary1,
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
disabledIndicatorColor = Color.Transparent,
),
colors = TangemSearchBarDefaults.secondaryTextFieldColors,
)
}

View file

@ -394,7 +394,7 @@ private fun RowContentContainer(
@Preview(widthDp = 360)
@Preview(widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_WalletNFTItem_InLight(@PreviewParameter(WalletNFTItemProvider::class) state: WalletNFTItemUM) {
private fun Preview_WalletNFTItem(@PreviewParameter(WalletNFTItemProvider::class) state: WalletNFTItemUM) {
TangemThemePreview {
WalletNFTItem(
state = state,