diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/badge/TangemBadge.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/badge/TangemBadge.kt index 1c9fd7cf14..f41322e237 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/badge/TangemBadge.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/badge/TangemBadge.kt @@ -29,6 +29,29 @@ import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreviewRedesign +/** + * Tangem badge component to display a small piece of information with optional icon. + * [Figma](https://www.figma.com/design/RU7AIgwHtGdMfy83T5UOoR/Core-Library?node-id=8441-83535&m=dev) + * + * @param badgeUM [TangemBadgeUM] containing all the badge parameters. + * @param modifier Modifier to be applied to the badge. + * +[REDACTED_AUTHOR] + */ +@Composable +fun TangemBadge(badgeUM: TangemBadgeUM, modifier: Modifier = Modifier) { + TangemBadge( + text = badgeUM.text, + iconRes = badgeUM.iconRes, + size = badgeUM.size, + shape = badgeUM.shape, + color = badgeUM.color, + type = badgeUM.type, + iconPosition = badgeUM.iconPosition, + modifier = modifier, + ) +} + /** * Tangem badge component to display a small piece of information with optional icon. * [Figma](https://www.figma.com/design/RU7AIgwHtGdMfy83T5UOoR/Core-Library?node-id=8441-83535&m=dev) diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/badge/TangemBadgeUM.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/badge/TangemBadgeUM.kt new file mode 100644 index 0000000000..e28cbdd6ba --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/badge/TangemBadgeUM.kt @@ -0,0 +1,27 @@ +package com.tangem.core.ui.ds.badge + +import androidx.annotation.DrawableRes +import com.tangem.core.ui.ds.badge.TangemBadgeSize.X9 +import com.tangem.core.ui.extensions.TextReference + +/** + * UI model for [TangemBadge] component + * + * @param text TextReference for the badge label. + * @param modifier Modifier to be applied to the badge. + * @param iconRes Drawable resource ID for the icon to be displayed in the badge. + * @param size [TangemBadgeSize] defining the size of the badge. + * @param shape [TangemBadgeShape] defining the shape of the badge. + * @param color [TangemBadgeColor] defining the color scheme of the badge. + * @param type [TangemBadgeType] defining the style of the badge. + * @param iconPosition [TangemBadgeIconPosition] defining icon position of the badge. + */ +class TangemBadgeUM( + val text: TextReference, + @DrawableRes val iconRes: Int? = null, + val size: TangemBadgeSize = X9, + val shape: TangemBadgeShape = TangemBadgeShape.Default, + val color: TangemBadgeColor = TangemBadgeColor.Gray, + val type: TangemBadgeType = TangemBadgeType.Solid, + val iconPosition: TangemBadgeIconPosition = TangemBadgeIconPosition.Start, +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/TangemRowContainer.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/TangemRowContainer.kt new file mode 100644 index 0000000000..b1a24c2067 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/TangemRowContainer.kt @@ -0,0 +1,189 @@ +package com.tangem.core.ui.ds.row + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.layout.Measurable +import androidx.compose.ui.layout.Placeable +import androidx.compose.ui.layout.layoutId +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.unit.Constraints +import com.tangem.core.ui.res.TangemTheme +import kotlin.math.max + +/** + * A custom layout composable that arranges its children in a row with specific layout IDs. + */ +internal enum class TangemRowLayoutId { + HEAD, START_TOP, END_TOP, START_BOTTOM, END_BOTTOM, TAIL, EXTRA_TOP +} + +/** + * A custom layout composable that arranges its children in a row with specific layout IDs. + * + * @param modifier Modifier to be applied to the layout. + * @param contentPadding Padding values to be applied around the content. + * @param content Composable content to be laid out within the TangemRow. + */ +@Suppress("LongMethod") +@Composable +internal fun TangemRowContainer( + modifier: Modifier = Modifier, + contentPadding: PaddingValues = PaddingValues(TangemTheme.dimens2.x3), + content: @Composable () -> Unit, +) { + val density = LocalDensity.current + val localDirection = LocalLayoutDirection.current + val verticalPadding = with(density) { TangemTheme.dimens2.x1.roundToPx() } + val contentTopPadding = with(density) { contentPadding.calculateTopPadding().roundToPx() } + val contentBottomPadding = with(density) { contentPadding.calculateBottomPadding().roundToPx() } + val contentStartPadding = with(density) { contentPadding.calculateLeftPadding(localDirection).roundToPx() } + val contentEndPadding = with(density) { contentPadding.calculateRightPadding(localDirection).roundToPx() } + Layout( + content = content, + modifier = modifier, + ) { measurables, constraints -> + val layoutWidth = constraints.maxWidth - contentStartPadding - contentEndPadding + + val startTopMinWidth = (layoutWidth * TITLE_MIN_WIDTH_COEFFICIENT).toInt() + val startBottomMinWidth = (layoutWidth * PRICE_MIN_WIDTH_COEFFICIENT).toInt() + + // Head composable measurement + val headPlaceable = measurables.measure( + layoutId = TangemRowLayoutId.HEAD, + constraints = constraints, + ) + + // Tail composable measurement + val tailPlaceable = measurables.measure( + layoutId = TangemRowLayoutId.TAIL, + constraints = constraints, + ) + + val availableWidthForBody = layoutWidth - headPlaceable.widthOrZero() - tailPlaceable.widthOrZero() + + // End top composable width must take the whole free space but is not greater the Start top min size + val endTopPlaceable = measurables.measure( + layoutId = TangemRowLayoutId.END_TOP, + constraints = constraints.copy( + minWidth = 0, + maxWidth = availableWidthForBody - startTopMinWidth, + ), + ) + + // End bottom composable width must take the whole free space but is not greater the Start bottom min size + val endBottomPlaceable = measurables.measure( + layoutId = TangemRowLayoutId.END_BOTTOM, + constraints = constraints.copy( + minWidth = 0, + maxWidth = availableWidthForBody - startBottomMinWidth, + ), + ) + + /* Start top composable will take take the whole REMAINING width space width but no less than minimum */ + val startTopPlaceable = measurables.measure( + layoutId = TangemRowLayoutId.START_TOP, + constraints = constraints.copy( + minWidth = startTopMinWidth, + maxWidth = max( + a = startTopMinWidth, + b = availableWidthForBody - endTopPlaceable.widthOrZero(), + ), + ), + ) + + /* Start bottom composable will take take the whole REMAINING width space width but no less than minimum */ + val startBottomPlaceable = measurables.measure( + layoutId = TangemRowLayoutId.START_BOTTOM, + constraints = constraints.copy( + minWidth = startBottomMinWidth, + maxWidth = max( + a = startBottomMinWidth, + b = availableWidthForBody - endBottomPlaceable.widthOrZero(), + ), + ), + ) + + val extraTopPlaceable = measurables.measure( + layoutId = TangemRowLayoutId.EXTRA_TOP, + constraints = constraints, + ) + + val mainLayoutHeight = maxOf( + headPlaceable.heightOrZero(), + tailPlaceable.heightOrZero(), + startTopPlaceable.heightOrZero() + startBottomPlaceable.heightOrZero() + verticalPadding, + endTopPlaceable.heightOrZero() + endBottomPlaceable.heightOrZero() + verticalPadding, + ) + + val mainContentTopPadding = if (extraTopPlaceable != null) { + extraTopPlaceable.heightOrZero() + } else { + contentTopPadding + } + + val layoutHeight = mainLayoutHeight + mainContentTopPadding + contentBottomPadding + + layout(width = constraints.maxWidth, height = layoutHeight) { + extraTopPlaceable?.placeRelative(x = 0, y = 0) + + headPlaceable?.placeRelative( + x = contentStartPadding, + y = mainContentTopPadding + (mainLayoutHeight - headPlaceable.height).div(other = 2), + ) + + startTopPlaceable?.placeRelative( + x = contentStartPadding + headPlaceable.widthOrZero(), + y = mainContentTopPadding + if (startBottomPlaceable == null) { + (mainLayoutHeight - startTopPlaceable.height).div(2) + } else { + 0 + }, + ) + + startBottomPlaceable?.placeRelative( + x = contentStartPadding + headPlaceable.widthOrZero(), + y = mainContentTopPadding + if (startTopPlaceable == null) { + (mainLayoutHeight - startBottomPlaceable.height).div(2) + } else { + startTopPlaceable.heightOrZero() + verticalPadding + }, + ) + + endTopPlaceable?.placeRelative( + x = layoutWidth - endTopPlaceable.widthOrZero() - tailPlaceable.widthOrZero() + contentEndPadding, + y = mainContentTopPadding + if (endBottomPlaceable == null) { + (mainLayoutHeight - endTopPlaceable.height).div(2) + } else { + 0 + }, + ) + + endBottomPlaceable?.placeRelative( + x = layoutWidth - endBottomPlaceable.widthOrZero() - tailPlaceable.widthOrZero() + contentEndPadding, + y = mainContentTopPadding + if (endTopPlaceable == null) { + (mainLayoutHeight - endBottomPlaceable.height).div(2) + } else { + endTopPlaceable.heightOrZero() + verticalPadding + }, + ) + + tailPlaceable?.placeRelative( + x = layoutWidth - tailPlaceable.width + contentEndPadding, + y = mainContentTopPadding + (mainLayoutHeight - tailPlaceable.height).div(other = 2), + ) + } + } +} + +private const val TITLE_MIN_WIDTH_COEFFICIENT = 0.3 +private const val PRICE_MIN_WIDTH_COEFFICIENT = 0.32 + +private fun List.measure(layoutId: TangemRowLayoutId, constraints: Constraints): Placeable? { + return firstOrNull { it.layoutId == layoutId }?.measure(constraints) +} + +private fun Placeable?.widthOrZero(): Int = this?.width ?: 0 +private fun Placeable?.heightOrZero(): Int = this?.height ?: 0 \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/TangemTokenRow.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/TangemTokenRow.kt new file mode 100644 index 0000000000..8d904b5ce7 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/TangemTokenRow.kt @@ -0,0 +1,168 @@ +package com.tangem.core.ui.ds.row.token + +import android.content.res.Configuration +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.layout.layoutId +import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.core.ui.components.currency.icon.CurrencyIcon +import com.tangem.core.ui.ds.row.TangemRowContainer +import com.tangem.core.ui.ds.row.TangemRowLayoutId +import com.tangem.core.ui.ds.row.token.internal.* +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreviewRedesign +import com.tangem.core.ui.test.TokenElementsTestTags +import org.burnoutcrew.reorderable.ReorderableLazyListState + +/** + * Composable function that represents a Tangem token row in a list. + * + * [Token Row](https://www.figma.com/design/RU7AIgwHtGdMfy83T5UOoR/Core-Library?node-id=8207-17583&t=k8dyaykorsNocGVq-4) + * + * @param tokenRowUM The user model containing the data for the token row. + * @param isBalanceHidden A boolean indicating whether the balance should be hidden. + * @param reorderableTokenListState The state of the reorderable lazy list, if applicable. + * @param modifier The modifier to be applied to the row. + */ +@Composable +fun TangemTokenRow( + tokenRowUM: TangemTokenRowUM, + isBalanceHidden: Boolean, + reorderableTokenListState: ReorderableLazyListState?, + modifier: Modifier = Modifier, +) { + TangemRowContainer( + content = { + CurrencyIcon( + state = tokenRowUM.iconState, + modifier = Modifier + .layoutId(layoutId = TangemRowLayoutId.HEAD) + .padding(end = TangemTheme.dimens2.x2) + .testTag(TokenElementsTestTags.TOKEN_ICON), + ) + + TokenRowPromoBanner( + promoBannerUM = tokenRowUM.promoBannerUM, + modifier = Modifier + .layoutId(layoutId = TangemRowLayoutId.EXTRA_TOP) + .testTag(TokenElementsTestTags.TOKEN_YIELD_PROMO_BANNER) + .padding(horizontal = TangemTheme.dimens2.x3) + .fillMaxWidth(), + ) + + TokenRowTitle( + titleUM = tokenRowUM.titleUM, + modifier = Modifier + .layoutId(layoutId = TangemRowLayoutId.START_TOP) + .padding(end = TangemTheme.dimens2.x2) + .testTag(tag = TokenElementsTestTags.TOKEN_TITLE), + ) + + TokenRowSubtitle( + subtitleUM = tokenRowUM.subtitleUM, + modifier = Modifier + .layoutId(layoutId = TangemRowLayoutId.START_BOTTOM) + .padding(end = TangemTheme.dimens2.x2) + .testTag(tag = TokenElementsTestTags.TOKEN_PRICE), + ) + + TokenRowEndTopContent( + endContentUM = tokenRowUM.topEndContentUM, + isBalanceHidden = isBalanceHidden, + modifier = Modifier + .layoutId(layoutId = TangemRowLayoutId.END_TOP) + .testTag(tag = TokenElementsTestTags.TOKEN_FIAT_AMOUNT), + ) + + TokenRowEndBottomContent( + endContentUM = tokenRowUM.bottomEndContentUM, + isBalanceHidden = isBalanceHidden, + modifier = Modifier + .layoutId(layoutId = TangemRowLayoutId.END_BOTTOM) + .testTag(tag = TokenElementsTestTags.TOKEN_CRYPTO_AMOUNT), + ) + + TokenRowTail( + tailUM = tokenRowUM.tailUM, + reorderableTokenListState = reorderableTokenListState, + modifier = Modifier + .layoutId(layoutId = TangemRowLayoutId.TAIL) + .testTag(TokenElementsTestTags.TOKEN_NON_FIAT_BLOCK), + ) + }, + modifier = modifier + .tokenClickable(tokenRowUM = tokenRowUM), + ) +} + +@OptIn(ExperimentalFoundationApi::class) +private fun Modifier.tokenClickable(tokenRowUM: TangemTokenRowUM): Modifier = composed { + val hapticFeedback = LocalHapticFeedback.current + + val onClick = tokenRowUM.onItemClick + val onLongClick = tokenRowUM.onItemLongClick + val onHapticLongClick = if (onLongClick != null) { + { + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + onLongClick(tokenRowUM) + } + } else { + null + } + + when { + onClick == null && onLongClick == null -> this + onClick == null && onLongClick != null -> combinedClickable(onClick = {}, onLongClick = onHapticLongClick) + onClick != null && onLongClick == null -> combinedClickable(onClick = { onClick(tokenRowUM) }) + onClick != null && onLongClick != null -> { + combinedClickable(onClick = { onClick(tokenRowUM) }, onLongClick = onHapticLongClick) + } + else -> this + } +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun TangemTokenRow_Preview( + @PreviewParameter(TangemTokenRowPreviewProvider::class) tokenRowUM: TangemTokenRowUM, +) { + TangemThemePreviewRedesign { + TangemTokenRow( + tokenRowUM = tokenRowUM, + isBalanceHidden = false, + reorderableTokenListState = null, + modifier = Modifier.background(TangemTheme.colors2.surface.level1), + ) + } +} + +private class TangemTokenRowPreviewProvider : CollectionPreviewParameterProvider( + collection = listOf( + TangemTokenRowPreviewData.defaultState, + TangemTokenRowPreviewData.defaultEllipsisState, + TangemTokenRowPreviewData.tokenState, + TangemTokenRowPreviewData.customTokenState, + TangemTokenRowPreviewData.draggableState, + TangemTokenRowPreviewData.draggableStateV2, + TangemTokenRowPreviewData.loadingState, + TangemTokenRowPreviewData.unreachableState, + TangemTokenRowPreviewData.accountState, + TangemTokenRowPreviewData.accountLetterState, + TangemTokenRowPreviewData.accountEllipsisState, + TangemTokenRowPreviewData.promoBannerState, + ), +) +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/TangemTokenRowUM.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/TangemTokenRowUM.kt new file mode 100644 index 0000000000..8ef54a567b --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/TangemTokenRowUM.kt @@ -0,0 +1,172 @@ +package com.tangem.core.ui.ds.row.token + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.components.marketprice.PriceChangeState +import com.tangem.core.ui.ds.badge.TangemBadgeUM +import com.tangem.core.ui.extensions.ColorReference2 +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.res.TangemTheme +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +@Immutable +sealed class TangemTokenRowUM { + + /** Unique id */ + abstract val id: String + + /** Token icon state */ + abstract val iconState: CurrencyIconState + + /** Token title UM (in one row with [topEndContentUM]) */ + abstract val titleUM: TitleUM + + /** Token subtitle UM (in one row with [bottomEndContentUM]) */ + abstract val subtitleUM: SubtitleUM + + /** Top end content UM (e.i. fiat amount in one row with [titleUM]) */ + abstract val topEndContentUM: EndContentUM + + /** Bottom end content UM (e.i. crypto amount in one row with [subtitleUM]) */ + abstract val bottomEndContentUM: EndContentUM + + /** Token row tail UM */ + abstract val tailUM: TailUM + + /** Promo banner UM */ + abstract val promoBannerUM: PromoBannerUM + + /** Callback which will be called when an item is clicked */ + abstract val onItemClick: ((TangemTokenRowUM) -> Unit)? + + /** Callback which will be called when an item is long clicked */ + abstract val onItemLongClick: ((TangemTokenRowUM) -> Unit)? + + /** + * Content state of [TangemTokenRowUM] + */ + data class Content( + override val id: String, + override val iconState: CurrencyIconState, + override val titleUM: TitleUM, + override val subtitleUM: SubtitleUM, + override val topEndContentUM: EndContentUM, + override val bottomEndContentUM: EndContentUM, + override val promoBannerUM: PromoBannerUM = PromoBannerUM.Empty, + override val tailUM: TailUM = TailUM.Empty, + override val onItemClick: ((TangemTokenRowUM) -> Unit)?, + override val onItemLongClick: ((TangemTokenRowUM) -> Unit)?, + ) : TangemTokenRowUM() + + /** + * Loading state of [TangemTokenRowUM] + */ + data class Loading( + override val id: String, + override val iconState: CurrencyIconState, + override val titleUM: TitleUM = TitleUM.Loading, + override val subtitleUM: SubtitleUM = SubtitleUM.Loading, + ) : TangemTokenRowUM() { + override val topEndContentUM: EndContentUM = EndContentUM.Loading + override val bottomEndContentUM: EndContentUM = EndContentUM.Loading + override val promoBannerUM: PromoBannerUM = PromoBannerUM.Empty + override val tailUM: TailUM = TailUM.Empty + override val onItemClick: ((TangemTokenRowUM) -> Unit)? = null + override val onItemLongClick: ((TangemTokenRowUM) -> Unit)? = null + } + + /** + * Actionable state of [TangemTokenRowUM] + */ + data class Actionable( + override val id: String, + override val iconState: CurrencyIconState, + override val titleUM: TitleUM, + override val subtitleUM: SubtitleUM, + override val tailUM: TailUM, + override val onItemClick: ((TangemTokenRowUM) -> Unit)?, + override val onItemLongClick: ((TangemTokenRowUM) -> Unit)?, + override val topEndContentUM: EndContentUM = EndContentUM.Empty, + override val bottomEndContentUM: EndContentUM = EndContentUM.Empty, + ) : TangemTokenRowUM() { + override val promoBannerUM: PromoBannerUM = PromoBannerUM.Empty + } + + @Immutable + sealed class TitleUM { + + data class Content( + val text: TextReference, + val hasPending: Boolean = false, + val isAvailable: Boolean = true, + val badge: TangemBadgeUM? = null, + val onBadgeClick: (() -> Unit)? = null, + ) : TitleUM() + + data object Loading : TitleUM() + + data object Empty : TitleUM() + } + + @Immutable + sealed class SubtitleUM { + + data class Content( + val text: TextReference, + val isAvailable: Boolean = true, + val isFlickering: Boolean = false, + val icons: ImmutableList = persistentListOf(), + val priceChangeUM: PriceChangeState = PriceChangeState.Unknown, + val badge: TangemBadgeUM? = null, + ) : SubtitleUM() + + data object Loading : SubtitleUM() + + data object Empty : SubtitleUM() + } + + @Immutable + sealed class EndContentUM { + + data class Content( + val text: TextReference, + val isAvailable: Boolean = true, + val isFlickering: Boolean = false, + val icons: ImmutableList = persistentListOf(), + val priceChangeUM: PriceChangeState = PriceChangeState.Unknown, + ) : EndContentUM() + + data object Loading : EndContentUM() + + data object Empty : EndContentUM() + } + + @Immutable + sealed class PromoBannerUM { + data class Content( + val title: TextReference, + val onPromoBannerClick: () -> Unit, + val onCloseClick: () -> Unit, + val onPromoShown: () -> Unit = {}, + ) : PromoBannerUM() + + data object Empty : PromoBannerUM() + } + + @Immutable + sealed class TailUM { + data class Text( + val text: TextReference, + ) : TailUM() + + data object Draggable : TailUM() + + data object Empty : TailUM() + } + + data class IconUM( + val iconRes: Int, + val tintReference: ColorReference2 = ColorReference2 { TangemTheme.colors2.graphic.neutral.primary }, + ) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TangemTokenRowPreviewData.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TangemTokenRowPreviewData.kt new file mode 100644 index 0000000000..aee2125611 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TangemTokenRowPreviewData.kt @@ -0,0 +1,266 @@ +package com.tangem.core.ui.ds.row.token.internal + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.SpanStyle +import com.tangem.core.ui.R +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.components.marketprice.PriceChangeState +import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.ds.row.token.TangemTokenRowUM +import com.tangem.core.ui.extensions.combinedReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.styledResourceReference +import com.tangem.core.ui.extensions.styledStringReference +import com.tangem.core.ui.res.TangemColorPalette +import com.tangem.core.ui.res.TangemTheme +import com.tangem.utils.StringsSigns +import kotlinx.collections.immutable.persistentListOf +import java.util.UUID + +internal object TangemTokenRowPreviewData { + + private val priceChangeState: PriceChangeState.Content + get() = PriceChangeState.Content( + type = PriceChangeType.UP, + valueInPercent = "2.45%", + ) + + val promoBannerUM: TangemTokenRowUM.PromoBannerUM.Content + get() = TangemTokenRowUM.PromoBannerUM.Content( + title = stringReference("Earn yield by supplying your crypto assets"), + onPromoBannerClick = {}, + onPromoShown = {}, + onCloseClick = {}, + ) + + val titleUM: TangemTokenRowUM.TitleUM.Content + get() = TangemTokenRowUM.TitleUM.Content( + text = stringReference(value = "Polygon"), + hasPending = true, + ) + + val subtitleUM: TangemTokenRowUM.SubtitleUM.Content + get() = TangemTokenRowUM.SubtitleUM.Content( + text = stringReference(value = "$ 0.6631"), + priceChangeUM = priceChangeState, + ) + + val topEndContentUM: TangemTokenRowUM.EndContentUM.Content + get() = TangemTokenRowUM.EndContentUM.Content( + text = combinedReference( + stringReference("$ 500"), + styledStringReference(".17", { + SpanStyle( + color = TangemTheme.colors2.text.neutral.secondary, + fontWeight = TangemTheme.typography2.bodyRegular16.fontWeight, + ) + }), + ), + ) + + val bottomEndContentUM: TangemTokenRowUM.EndContentUM.Content + get() = TangemTokenRowUM.EndContentUM.Content( + text = stringReference("Title"), + ) + + private val accountResIcon: CurrencyIconState.CryptoPortfolio.Icon + get() = CurrencyIconState.CryptoPortfolio.Icon( + resId = R.drawable.ic_rounded_star_24, + color = Color.Blue, + isGrayscale = false, + ) + private val accountLetterIcon: CurrencyIconState.CryptoPortfolio.Letter + get() = CurrencyIconState.CryptoPortfolio.Letter( + char = stringReference("A"), + color = Color.Blue, + isGrayscale = false, + ) + + private val coinIconState + get() = CurrencyIconState.CoinIcon( + url = null, + fallbackResId = R.drawable.img_polygon_22, + isGrayscale = false, + shouldShowCustomBadge = false, + ) + + private val tokenIconState + get() = CurrencyIconState.TokenIcon( + url = null, + topBadgeIconResId = R.drawable.img_polygon_22, + fallbackTint = TangemColorPalette.Black, + fallbackBackground = TangemColorPalette.Meadow, + isGrayscale = false, + shouldShowCustomBadge = false, + ) + + private val customTokenIconState + get() = CurrencyIconState.CustomTokenIcon( + tint = TangemColorPalette.Black, + background = TangemColorPalette.Meadow, + topBadgeIconResId = R.drawable.img_polygon_22, + isGrayscale = false, + ) + + val defaultState: TangemTokenRowUM.Content + get() = TangemTokenRowUM.Content( + id = UUID.randomUUID().toString(), + iconState = coinIconState, + titleUM = titleUM, + subtitleUM = subtitleUM, + topEndContentUM = topEndContentUM, + bottomEndContentUM = bottomEndContentUM, + promoBannerUM = TangemTokenRowUM.PromoBannerUM.Empty, + tailUM = TangemTokenRowUM.TailUM.Empty, + onItemClick = {}, + onItemLongClick = {}, + ) + + val defaultEllipsisState: TangemTokenRowUM.Content + get() = defaultState.copy( + titleUM = titleUM.copy(text = stringReference("Polygon Polygon Polygon Polygon")), + subtitleUM = subtitleUM, + topEndContentUM = TangemTokenRowUM.EndContentUM.Content( + text = combinedReference( + stringReference("$ 500"), + styledStringReference(".11232131237", { + SpanStyle( + color = TangemTheme.colors2.text.neutral.secondary, + fontWeight = TangemTheme.typography2.bodyRegular16.fontWeight, + ) + }), + ), + icons = persistentListOf( + TangemTokenRowUM.IconUM(R.drawable.ic_staking_mini_10), + TangemTokenRowUM.IconUM(R.drawable.ic_attention_12), + TangemTokenRowUM.IconUM(R.drawable.ic_error_sync_24), + ), + ), + bottomEndContentUM = bottomEndContentUM, + promoBannerUM = TangemTokenRowUM.PromoBannerUM.Empty, + tailUM = TangemTokenRowUM.TailUM.Empty, + onItemClick = {}, + onItemLongClick = {}, + ) + + val tokenState: TangemTokenRowUM.Content + get() = TangemTokenRowUM.Content( + id = UUID.randomUUID().toString(), + iconState = tokenIconState, + titleUM = titleUM, + subtitleUM = subtitleUM, + topEndContentUM = topEndContentUM, + bottomEndContentUM = bottomEndContentUM, + promoBannerUM = TangemTokenRowUM.PromoBannerUM.Empty, + tailUM = TangemTokenRowUM.TailUM.Empty, + onItemClick = {}, + onItemLongClick = {}, + ) + + val customTokenState: TangemTokenRowUM.Content + get() = TangemTokenRowUM.Content( + id = UUID.randomUUID().toString(), + iconState = customTokenIconState, + titleUM = titleUM, + subtitleUM = subtitleUM, + topEndContentUM = topEndContentUM, + bottomEndContentUM = bottomEndContentUM, + promoBannerUM = TangemTokenRowUM.PromoBannerUM.Empty, + tailUM = TangemTokenRowUM.TailUM.Empty, + onItemClick = {}, + onItemLongClick = {}, + ) + + val draggableState: TangemTokenRowUM.Actionable + get() = TangemTokenRowUM.Actionable( + id = UUID.randomUUID().toString(), + iconState = coinIconState, + titleUM = titleUM, + subtitleUM = subtitleUM, + tailUM = TangemTokenRowUM.TailUM.Draggable, + onItemClick = {}, + onItemLongClick = {}, + ) + + val draggableStateV2: TangemTokenRowUM.Actionable + get() = TangemTokenRowUM.Actionable( + id = UUID.randomUUID().toString(), + iconState = coinIconState, + titleUM = titleUM, + subtitleUM = subtitleUM, + topEndContentUM = topEndContentUM, + bottomEndContentUM = bottomEndContentUM, + tailUM = TangemTokenRowUM.TailUM.Draggable, + onItemClick = {}, + onItemLongClick = {}, + ) + + val loadingState: TangemTokenRowUM.Loading + get() = TangemTokenRowUM.Loading( + id = UUID.randomUUID().toString(), + iconState = coinIconState, + titleUM = TangemTokenRowUM.TitleUM.Loading, + subtitleUM = TangemTokenRowUM.SubtitleUM.Loading, + ) + + val unreachableState: TangemTokenRowUM.Content + get() = defaultState.copy( + topEndContentUM = TangemTokenRowUM.EndContentUM.Content( + text = stringReference(StringsSigns.DASH_SIGN), + ), + bottomEndContentUM = TangemTokenRowUM.EndContentUM.Content( + text = styledResourceReference( + id = R.string.common_unreachable, + spanStyleReference = { SpanStyle(color = TangemTheme.colors2.text.status.attention) }, + ), + ), + ) + + val accountState: TangemTokenRowUM.Content + get() = TangemTokenRowUM.Content( + id = UUID.randomUUID().toString(), + iconState = accountResIcon, + titleUM = TangemTokenRowUM.TitleUM.Content( + text = stringReference(value = "Portfolio"), + ), + subtitleUM = TangemTokenRowUM.SubtitleUM.Content( + text = stringReference("24 tokens"), + ), + topEndContentUM = TangemTokenRowUM.EndContentUM.Content( + text = stringReference("22,129.65 $"), + ), + bottomEndContentUM = TangemTokenRowUM.EndContentUM.Content( + text = stringReference("+ $ 1,245.32"), + priceChangeUM = priceChangeState, + ), + onItemClick = {}, + onItemLongClick = {}, + ) + + val accountLetterState: TangemTokenRowUM.Content + get() = accountState.copy( + iconState = accountLetterIcon, + ) + + val accountEllipsisState: TangemTokenRowUM.Content + get() = accountState.copy( + titleUM = TangemTokenRowUM.TitleUM.Content( + text = stringReference(value = "Portfolio Portfolio Portfolio Portfolio"), + ), + subtitleUM = TangemTokenRowUM.SubtitleUM.Content( + text = stringReference("24 tokens 24 tokens 24 tokens 24 tokens"), + ), + topEndContentUM = TangemTokenRowUM.EndContentUM.Content( + text = stringReference("22,129.6129387147653025 $"), + ), + bottomEndContentUM = TangemTokenRowUM.EndContentUM.Content( + text = stringReference("+ $ 1,245.31093284302752"), + priceChangeUM = priceChangeState, + ), + ) + + val promoBannerState: TangemTokenRowUM.Content + get() = defaultState.copy( + promoBannerUM = promoBannerUM, + ) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowEndBottomContent.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowEndBottomContent.kt new file mode 100644 index 0000000000..a4bdecca54 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowEndBottomContent.kt @@ -0,0 +1,100 @@ +package com.tangem.core.ui.ds.row.token.internal + +import android.content.res.Configuration +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +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.PreviewParameterProvider +import com.tangem.core.ui.components.TextShimmer +import com.tangem.core.ui.components.marketprice.PriceChangeState +import com.tangem.core.ui.components.text.applyBladeBrush +import com.tangem.core.ui.ds.row.token.TangemTokenRowUM +import com.tangem.core.ui.extensions.orMaskWithStars +import com.tangem.core.ui.extensions.resolveAnnotatedReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreviewRedesign + +@Composable +internal fun TokenRowEndBottomContent( + endContentUM: TangemTokenRowUM.EndContentUM, + isBalanceHidden: Boolean, + modifier: Modifier = Modifier, +) { + when (endContentUM) { + is TangemTokenRowUM.EndContentUM.Content -> Content( + modifier = modifier, + endContentUM = endContentUM, + isBalanceHidden = isBalanceHidden, + ) + TangemTokenRowUM.EndContentUM.Empty -> Unit + TangemTokenRowUM.EndContentUM.Loading -> TextShimmer( + style = TangemTheme.typography2.captionSemibold12, + modifier = modifier.width(TangemTheme.dimens2.x10), + radius = TangemTheme.dimens2.x25, + ) + } +} + +@Composable +private fun Content( + endContentUM: TangemTokenRowUM.EndContentUM.Content, + isBalanceHidden: Boolean, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = endContentUM.text.orMaskWithStars(isBalanceHidden).resolveAnnotatedReference(), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = TangemTheme.typography2.captionSemibold12.applyBladeBrush( + isEnabled = endContentUM.isFlickering, + textColor = if (endContentUM.isAvailable) { + TangemTheme.colors2.text.neutral.secondary + } else { + TangemTheme.colors2.text.status.disabled + }, + ), + ) + + when (endContentUM.priceChangeUM) { + is PriceChangeState.Content -> TokenRowPriceChangeContent( + priceChangeState = endContentUM.priceChangeUM, + isFlickering = endContentUM.isFlickering, + isAvailable = endContentUM.isAvailable, + ) + PriceChangeState.Unknown -> Unit + } + } +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun TokenRowEndBottomContent_Preview( + @PreviewParameter(TokenRowEndBottomContentPreviewProvider::class) params: TangemTokenRowUM.EndContentUM, +) { + TangemThemePreviewRedesign { + TokenRowEndBottomContent( + endContentUM = params, + isBalanceHidden = false, + ) + } +} + +private class TokenRowEndBottomContentPreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + TangemTokenRowPreviewData.bottomEndContentUM, + ) +} +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowEndTopContent.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowEndTopContent.kt new file mode 100644 index 0000000000..131497424f --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowEndTopContent.kt @@ -0,0 +1,115 @@ +package com.tangem.core.ui.ds.row.token.internal + +import android.content.res.Configuration +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.res.vectorResource +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.PreviewParameterProvider +import androidx.compose.ui.util.fastForEach +import com.tangem.core.ui.components.TextShimmer +import com.tangem.core.ui.components.text.applyBladeBrush +import com.tangem.core.ui.ds.row.token.TangemTokenRowUM +import com.tangem.core.ui.extensions.orMaskWithStars +import com.tangem.core.ui.extensions.resolveAnnotatedReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreviewRedesign + +@Composable +internal fun TokenRowEndTopContent( + endContentUM: TangemTokenRowUM.EndContentUM, + isBalanceHidden: Boolean, + modifier: Modifier = Modifier, +) { + when (endContentUM) { + is TangemTokenRowUM.EndContentUM.Content -> Content( + modifier = modifier, + endContentUM = endContentUM, + isBalanceHidden = isBalanceHidden, + ) + TangemTokenRowUM.EndContentUM.Empty -> Unit + TangemTokenRowUM.EndContentUM.Loading -> TextShimmer( + style = TangemTheme.typography2.bodySemibold16, + modifier = modifier.width(TangemTheme.dimens2.x18), + radius = TangemTheme.dimens2.x25, + ) + } +} + +@Composable +private fun Content( + endContentUM: TangemTokenRowUM.EndContentUM.Content, + isBalanceHidden: Boolean, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + ) { + AnimatedVisibility( + visible = endContentUM.icons.isNotEmpty(), + ) { + Row( + modifier = Modifier.padding(horizontal = TangemTheme.dimens2.x1), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1), + ) { + endContentUM.icons.fastForEach { icon -> + Icon( + modifier = Modifier.size(TangemTheme.dimens2.x3), + painter = rememberVectorPainter(image = ImageVector.vectorResource(icon.iconRes)), + tint = icon.tintReference(), + contentDescription = null, + ) + } + } + } + + Text( + modifier = Modifier, + text = endContentUM.text.orMaskWithStars(isBalanceHidden).resolveAnnotatedReference(), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = TangemTheme.typography2.bodySemibold16.applyBladeBrush( + isEnabled = endContentUM.isFlickering, + textColor = if (endContentUM.isAvailable) { + TangemTheme.colors2.text.neutral.primary + } else { + TangemTheme.colors2.text.status.disabled + }, + ), + ) + } +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun TokenRowEndTopContent_Preview( + @PreviewParameter(TokenRowEndContentPreviewProvider::class) params: TangemTokenRowUM.EndContentUM, +) { + TangemThemePreviewRedesign { + TokenRowEndTopContent( + endContentUM = params, + isBalanceHidden = false, + ) + } +} + +private class TokenRowEndContentPreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + TangemTokenRowPreviewData.topEndContentUM, + ) +} +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowPriceChangeContent.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowPriceChangeContent.kt new file mode 100644 index 0000000000..ef635a0a8e --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowPriceChangeContent.kt @@ -0,0 +1,71 @@ +package com.tangem.core.ui.ds.row.token.internal + +import androidx.compose.animation.AnimatedContent +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextOverflow +import com.tangem.core.ui.R +import com.tangem.core.ui.components.marketprice.PriceChangeState +import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.components.text.applyBladeBrush +import com.tangem.core.ui.res.TangemTheme + +@Composable +internal fun RowScope.TokenRowPriceChangeContent( + priceChangeState: PriceChangeState.Content, + isFlickering: Boolean, + isAvailable: Boolean = true, +) { + AnimatedContent( + targetState = priceChangeState.type, + label = "Update the price change's arrow", + modifier = Modifier.padding(start = TangemTheme.dimens2.x1), + ) { animatedType -> + Icon( + painter = rememberVectorPainter( + ImageVector.vectorResource( + when (animatedType) { + PriceChangeType.UP -> R.drawable.ic_up_dynamic_24 + PriceChangeType.DOWN -> R.drawable.ic_down_dynamic_24 + PriceChangeType.NEUTRAL -> R.drawable.ic_static_dynamic_24 + }, + ), + ), + tint = when (animatedType) { + PriceChangeType.UP -> TangemTheme.colors2.graphic.status.accent + PriceChangeType.DOWN -> TangemTheme.colors2.graphic.status.warning + PriceChangeType.NEUTRAL -> TangemTheme.colors2.graphic.neutral.secondary + }, + contentDescription = null, + modifier = Modifier.size(TangemTheme.dimens2.x3), + ) + } + + AnimatedContent( + targetState = priceChangeState.valueInPercent, + label = "Update the price text", + modifier = Modifier.padding(start = TangemTheme.dimens2.x0_5), + ) { animatedText -> + Text( + text = animatedText, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = TangemTheme.typography2.captionSemibold12.applyBladeBrush( + isEnabled = isFlickering, + textColor = if (isAvailable) { + TangemTheme.colors2.text.neutral.secondary + } else { + TangemTheme.colors2.text.status.disabled + }, + ), + ) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowPromoBanner.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowPromoBanner.kt new file mode 100644 index 0000000000..93845dea18 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowPromoBanner.kt @@ -0,0 +1,107 @@ +package com.tangem.core.ui.ds.row.token.internal + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.material3.ripple +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.R +import com.tangem.core.ui.ds.row.token.TangemTokenRowUM +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreviewRedesign + +@Composable +internal fun TokenRowPromoBanner(promoBannerUM: TangemTokenRowUM.PromoBannerUM, modifier: Modifier = Modifier) { + when (promoBannerUM) { + is TangemTokenRowUM.PromoBannerUM.Content -> TokenRowPromoBanner( + promoBannerUM = promoBannerUM, + modifier = modifier, + ) + TangemTokenRowUM.PromoBannerUM.Empty -> Unit + } +} + +@Composable +internal fun TokenRowPromoBanner(promoBannerUM: TangemTokenRowUM.PromoBannerUM.Content, modifier: Modifier = Modifier) { + LaunchedEffect(promoBannerUM) { + promoBannerUM.onPromoShown() + } + val bgColor = TangemTheme.colors.control.default + Column(modifier = modifier) { + Row( + modifier = Modifier + .background(color = bgColor, shape = RoundedCornerShape(TangemTheme.dimens2.x4)) + .clickable(onClick = promoBannerUM.onPromoBannerClick) + .padding(horizontal = TangemTheme.dimens2.x3, vertical = TangemTheme.dimens2.x2) + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_analytics_up_24), + contentDescription = null, + tint = TangemTheme.colors.icon.accent, + modifier = Modifier + .padding(end = TangemTheme.dimens2.x2) + .size(TangemTheme.dimens2.x4), + ) + Text( + text = promoBannerUM.title.resolveReference(), + style = TangemTheme.typography2.captionSemibold12, + color = TangemTheme.colors2.text.neutral.secondary, + modifier = Modifier + .weight(1f) + .padding(end = TangemTheme.dimens2.x2), + ) + Icon( + painter = painterResource(id = R.drawable.ic_close_24), + contentDescription = null, + tint = TangemTheme.colors2.text.neutral.secondary, + modifier = Modifier + .size(TangemTheme.dimens2.x4) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = ripple(bounded = false), + onClick = { promoBannerUM.onCloseClick() }, + ), + ) + } + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center, + ) { + Icon( + painter = painterResource(id = R.drawable.ic_rectangle_bottom), + contentDescription = null, + tint = bgColor, + modifier = Modifier + .size(width = TangemTheme.dimens2.x3, height = TangemTheme.dimens2.x2), + ) + } + } +} + +@Preview(widthDp = 360, showBackground = true) +@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_TokenRowPromoBanner() { + TangemThemePreviewRedesign { + TokenRowPromoBanner( + promoBannerUM = TangemTokenRowPreviewData.promoBannerUM, + modifier = Modifier.fillMaxWidth(), + ) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowSubtitle.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowSubtitle.kt new file mode 100644 index 0000000000..c0d32baca0 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowSubtitle.kt @@ -0,0 +1,100 @@ +package com.tangem.core.ui.ds.row.token.internal + +import android.content.res.Configuration +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.width +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.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.tangem.core.ui.components.TextShimmer +import com.tangem.core.ui.components.marketprice.PriceChangeState +import com.tangem.core.ui.components.text.applyBladeBrush +import com.tangem.core.ui.ds.badge.TangemBadge +import com.tangem.core.ui.ds.row.token.TangemTokenRowUM +import com.tangem.core.ui.extensions.resolveAnnotatedReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreviewRedesign + +@Composable +internal fun TokenRowSubtitle(subtitleUM: TangemTokenRowUM.SubtitleUM, modifier: Modifier = Modifier) { + when (subtitleUM) { + is TangemTokenRowUM.SubtitleUM.Content -> SubtitleContent( + subtitleUM = subtitleUM, + modifier = modifier, + ) + TangemTokenRowUM.SubtitleUM.Loading -> TextShimmer( + style = TangemTheme.typography2.captionSemibold12, + modifier = modifier.width(TangemTheme.dimens2.x8), + radius = TangemTheme.dimens2.x25, + ) + TangemTokenRowUM.SubtitleUM.Empty -> Unit + } +} + +@Composable +private fun SubtitleContent(subtitleUM: TangemTokenRowUM.SubtitleUM.Content, modifier: Modifier = Modifier) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier, + ) { + AnimatedVisibility( + visible = subtitleUM.badge != null, + ) { + val wrappedBadge = remember(this) { requireNotNull(subtitleUM.badge) } + TangemBadge(wrappedBadge) + } + + Text( + text = subtitleUM.text.resolveAnnotatedReference(), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = TangemTheme.typography2.captionSemibold12.applyBladeBrush( + isEnabled = subtitleUM.isFlickering, + textColor = if (subtitleUM.isAvailable) { + TangemTheme.colors2.text.neutral.secondary + } else { + TangemTheme.colors2.text.status.disabled + }, + ), + ) + + when (subtitleUM.priceChangeUM) { + is PriceChangeState.Content -> TokenRowPriceChangeContent( + priceChangeState = subtitleUM.priceChangeUM, + isFlickering = subtitleUM.isFlickering, + isAvailable = subtitleUM.isAvailable, + ) + PriceChangeState.Unknown -> Unit + } + } +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun TokenRowSubtitle_Preview( + @PreviewParameter(TokenRowSubtitlePreviewProvider::class) params: TangemTokenRowUM.SubtitleUM, +) { + TangemThemePreviewRedesign { + TokenRowSubtitle( + subtitleUM = params, + ) + } +} + +private class TokenRowSubtitlePreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + TangemTokenRowPreviewData.subtitleUM, + TangemTokenRowUM.SubtitleUM.Loading, + ) +} +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowTail.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowTail.kt new file mode 100644 index 0000000000..f3416f8a29 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowTail.kt @@ -0,0 +1,81 @@ +package com.tangem.core.ui.ds.row.token.internal + +import androidx.compose.animation.AnimatedContent +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +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.platform.testTag +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow +import com.tangem.core.ui.R +import com.tangem.core.ui.ds.row.token.TangemTokenRowUM +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveAnnotatedReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.test.OrganizeTokensScreenTestTags +import org.burnoutcrew.reorderable.ReorderableLazyListState +import org.burnoutcrew.reorderable.detectReorder + +@Composable +internal fun TokenRowTail( + tailUM: TangemTokenRowUM.TailUM, + reorderableTokenListState: ReorderableLazyListState?, + modifier: Modifier = Modifier, +) { + AnimatedContent( + targetState = tailUM, + label = "Update non content fiat block", + modifier = modifier, + contentKey = { it::class.java }, + ) { animatedState -> + val innerModifier = Modifier.padding(start = TangemTheme.dimens2.x2) + when (animatedState) { + TangemTokenRowUM.TailUM.Empty -> Unit + is TangemTokenRowUM.TailUM.Draggable -> DraggableImage( + reorderableTokenListState = reorderableTokenListState, + modifier = innerModifier, + ) + is TangemTokenRowUM.TailUM.Text -> ContentText(text = animatedState.text, modifier = innerModifier) + } + } +} + +@Composable +private fun DraggableImage(reorderableTokenListState: ReorderableLazyListState?, modifier: Modifier = Modifier) { + Box( + modifier = modifier + .size(size = TangemTheme.dimens2.x6) + .then( + other = if (reorderableTokenListState != null) { + Modifier.detectReorder(reorderableTokenListState) + } else { + Modifier + }, + ) + .testTag(OrganizeTokensScreenTestTags.DRAGGABLE_IMAGE), + contentAlignment = Alignment.Center, + ) { + Icon( + painter = painterResource(id = R.drawable.ic_drag_24), + tint = TangemTheme.colors2.graphic.neutral.tertiaryConstant, + contentDescription = null, + ) + } +} + +@Composable +private fun ContentText(text: TextReference, modifier: Modifier = Modifier) { + Text( + text = text.resolveAnnotatedReference(), + color = TangemTheme.colors2.text.neutral.tertiary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = TangemTheme.typography2.captionRegular12, + modifier = modifier, + ) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowTitle.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowTitle.kt new file mode 100644 index 0000000000..ad8368ab4d --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowTitle.kt @@ -0,0 +1,101 @@ +package com.tangem.core.ui.ds.row.token.internal + +import android.content.res.Configuration +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.width +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.res.painterResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.R +import com.tangem.core.ui.components.TextShimmer +import com.tangem.core.ui.ds.badge.TangemBadge +import com.tangem.core.ui.ds.row.token.TangemTokenRowUM +import com.tangem.core.ui.extensions.conditional +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreviewRedesign + +@Composable +internal fun TokenRowTitle(titleUM: TangemTokenRowUM.TitleUM, modifier: Modifier = Modifier) { + when (titleUM) { + is TangemTokenRowUM.TitleUM.Content -> ContentTitle(titleUM = titleUM, modifier = modifier) + TangemTokenRowUM.TitleUM.Loading -> TextShimmer( + style = TangemTheme.typography2.bodySemibold16, + modifier = modifier.width(TangemTheme.dimens2.x18), + radius = TangemTheme.dimens2.x25, + ) + TangemTokenRowUM.TitleUM.Empty -> Unit + } +} + +@Composable +private fun ContentTitle(titleUM: TangemTokenRowUM.TitleUM.Content, modifier: Modifier = Modifier) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens2.x4), + verticalAlignment = Alignment.CenterVertically, + ) { + /* + * If currency name has a long width, then it will completely displace the image. + * So we need to use [weight] to avoid displacement. + */ + Text( + text = titleUM.text.resolveReference(), + modifier = Modifier.weight(weight = 1f, fill = false), + color = if (titleUM.isAvailable) { + TangemTheme.colors2.text.neutral.primary + } else { + TangemTheme.colors2.text.status.disabled + }, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + style = TangemTheme.typography2.bodySemibold16, + ) + + AnimatedVisibility( + visible = titleUM.hasPending, + modifier = Modifier.align(alignment = Alignment.CenterVertically), + ) { + Image( + painter = painterResource(id = R.drawable.img_loader_15), + contentDescription = null, + ) + } + + AnimatedVisibility( + visible = titleUM.badge != null, + modifier = Modifier.conditional( + condition = titleUM.onBadgeClick != null, + modifier = { clickable(onClick = requireNotNull(titleUM.onBadgeClick)) }, + ), + ) { + val wrappedBadge = remember(this) { requireNotNull(titleUM.badge) } + TangemBadge(wrappedBadge) + } + } +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun TokenRowTitle_Preview() { + TangemThemePreviewRedesign { + TokenRowTitle( + titleUM = TangemTokenRowPreviewData.titleUM, + modifier = Modifier.fillMaxWidth(), + ) + } +} + +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/ColorReference.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/ColorReference.kt index 604a8a16e1..79c329a101 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/ColorReference.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/ColorReference.kt @@ -2,6 +2,8 @@ package com.tangem.core.ui.extensions import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.runtime.Stable import androidx.compose.ui.graphics.Color /** @@ -11,7 +13,7 @@ import androidx.compose.ui.graphics.Color * * @property value color provider from theme */ -@Deprecated("Use TextReference with applied SpanStyleReference for colored text.") +@Deprecated("Use TextReference with applied SpanStyleReference for colored text or ColorReference2 for color.") @Immutable data class ColorReference(val value: @Composable () -> Color) @@ -31,4 +33,17 @@ fun themedColor(value: @Composable () -> Color): ColorReference { @Composable fun ColorReference.resolveReference(): Color { return value() +} + +/** + * Utility functional interface for keeping themed [Color] reference from app theme. + * It is necessary to use [Stable] annotation for runtime stability. + */ +@Stable +@FunctionalInterface +fun interface ColorReference2 { + + @ReadOnlyComposable + @Composable + operator fun invoke(): Color } \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_down_dynamic_24.xml b/core/ui/src/main/res/drawable/ic_down_dynamic_24.xml new file mode 100644 index 0000000000..01b2208a14 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_down_dynamic_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_drag_24.xml b/core/ui/src/main/res/drawable/ic_drag_24.xml index 9246315ec7..3cea032612 100644 --- a/core/ui/src/main/res/drawable/ic_drag_24.xml +++ b/core/ui/src/main/res/drawable/ic_drag_24.xml @@ -4,7 +4,9 @@ android:viewportWidth="24" android:viewportHeight="24"> + android:pathData="M5,8H19M5,16H19" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#000000" + android:strokeLineCap="round"/> diff --git a/core/ui/src/main/res/drawable/ic_static_dynamic_24.xml b/core/ui/src/main/res/drawable/ic_static_dynamic_24.xml new file mode 100644 index 0000000000..ee4903259a --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_static_dynamic_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_up_dynamic_24.xml b/core/ui/src/main/res/drawable/ic_up_dynamic_24.xml new file mode 100644 index 0000000000..4f63f4b68f --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_up_dynamic_24.xml @@ -0,0 +1,9 @@ + + +