diff --git a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleBadge.kt b/common/ui/src/main/java/com/tangem/common/ui/news/ArticleBadge.kt deleted file mode 100644 index 157a36e08d..0000000000 --- a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleBadge.kt +++ /dev/null @@ -1,76 +0,0 @@ -package com.tangem.common.ui.news - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.components.currency.icon.CurrencyIcon -import com.tangem.core.ui.components.currency.icon.CurrencyIconState -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 - -@Composable -internal fun ArticleBadge(articleTagUM: ArticleTagUM, modifier: Modifier = Modifier) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp), - modifier = modifier - .heightIn(min = 24.dp) - .background( - color = TangemTheme.colors.icon.informative.copy(alpha = 0.1f), - shape = RoundedCornerShape(8.dp), - ) - .padding(horizontal = 8.dp, vertical = 4.dp), - ) { - when (articleTagUM) { - is ArticleTagUM.Category -> Unit - is ArticleTagUM.Token -> { - CurrencyIcon( - state = articleTagUM.iconState, - shouldDisplayNetwork = false, - iconSize = 16.dp, - ) - } - } - Text( - text = articleTagUM.title.resolveReference(), - style = TangemTheme.typography.caption1, - color = TangemTheme.colors.text.secondary, - ) - } -} - -@Preview(showBackground = true, widthDp = 360) -@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun ArticleBadgePreview() { - TangemThemePreview { - Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) { - ArticleBadge( - articleTagUM = ArticleTagUM.Token( - TextReference.Str("BTC"), - iconState = CurrencyIconState.CoinIcon( - url = "", - fallbackResId = 0, - isGrayscale = false, - shouldShowCustomBadge = false, - ), - ), - ) - ArticleBadge( - articleTagUM = ArticleTagUM.Category(TextReference.Str("Regulation")), - ) - } - } -} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleCard.kt b/common/ui/src/main/java/com/tangem/common/ui/news/ArticleCard.kt index 259c72b98d..a1ee6a610c 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleCard.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/news/ArticleCard.kt @@ -1,7 +1,6 @@ package com.tangem.common.ui.news import android.content.res.Configuration -import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState @@ -12,24 +11,23 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawWithCache -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.core.ui.R import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.block.BlockCard +import com.tangem.core.ui.components.label.Label +import com.tangem.core.ui.components.label.entity.LabelUM import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.utils.StringsSigns import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableSet -import kotlinx.collections.immutable.toPersistentList @Composable fun ArticleCard(articleConfigUM: ArticleConfigUM, onArticleClick: () -> Unit, modifier: Modifier = Modifier) { @@ -121,54 +119,13 @@ private fun DefaultArticle(articleConfigUM: ArticleConfigUM) { } } -@Composable -private fun ArticleInfo(score: Float, createdAt: String, modifier: Modifier = Modifier) { - val dotColor = TangemTheme.colors.text.secondary - Row( - modifier = modifier, - horizontalArrangement = Arrangement.spacedBy(4.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Image( - imageVector = ImageVector.vectorResource(R.drawable.ic_start_circle_12), - contentDescription = null, - ) - - Text( - text = score.toString(), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.secondary, - ) - - Spacer( - modifier = Modifier - .size(4.dp) - .drawWithCache { - val radius = size.minDimension / 2f - onDrawBehind { - drawCircle( - color = dotColor, - radius = radius, - ) - } - }, - ) - - Text( - text = createdAt, - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.secondary, - ) - } -} - @OptIn(ExperimentalLayoutApi::class) @Composable -private fun Tags(tags: ImmutableList, modifier: Modifier = Modifier) { +private fun Tags(tags: ImmutableList, modifier: Modifier = Modifier) { val expandIndicator = remember { ContextualFlowRowOverflow.expandIndicator { val remainingItems = tags.size - shownItemCount - ArticleBadge(articleTagUM = ArticleTagUM.Category(TextReference.Str("${StringsSigns.PLUS}$remainingItems"))) + Label(state = LabelUM(TextReference.Str("${StringsSigns.PLUS}$remainingItems"))) } } ContextualFlowRow( @@ -179,7 +136,7 @@ private fun Tags(tags: ImmutableList, modifier: Modifier = Modifie maxLines = 1, overflow = expandIndicator, ) { index -> - ArticleBadge(articleTagUM = tags[index]) + Label(state = tags[index]) } } @@ -189,14 +146,14 @@ private fun Tags(tags: ImmutableList, modifier: Modifier = Modifie private fun TagsPreview() { TangemThemePreview { Tags( - tags = listOf( - ArticleTagUM.Category(TextReference.Str("Hype")), - ArticleTagUM.Category(TextReference.Str("BTC")), - ArticleTagUM.Category(TextReference.Str("Supply")), - ArticleTagUM.Category(TextReference.Str("Demand")), - ArticleTagUM.Category(TextReference.Str("Best rate")), - ArticleTagUM.Category(TextReference.Str("Breaking news")), - ).toPersistentList(), + tags = persistentListOf( + LabelUM(TextReference.Str("Hype")), + LabelUM(TextReference.Str("BTC")), + LabelUM(TextReference.Str("Supply")), + LabelUM(TextReference.Str("Demand")), + LabelUM(TextReference.Str("Best rate")), + LabelUM(TextReference.Str("Breaking news")), + ), ) } } @@ -206,12 +163,12 @@ private fun TagsPreview() { @Composable private fun ArticleCardsPreview() { val tags = listOf( - ArticleTagUM.Category(TextReference.Str("Hype")), - ArticleTagUM.Category(TextReference.Str("BTC")), - ArticleTagUM.Category(TextReference.Str("Supply")), - ArticleTagUM.Category(TextReference.Str("Demand")), - ArticleTagUM.Category(TextReference.Str("Best rate")), - ArticleTagUM.Category(TextReference.Str("Breaking news")), + LabelUM(TextReference.Str("Hype")), + LabelUM(TextReference.Str("BTC")), + LabelUM(TextReference.Str("Supply")), + LabelUM(TextReference.Str("Demand")), + LabelUM(TextReference.Str("Best rate")), + LabelUM(TextReference.Str("Breaking news")), ).toImmutableSet() val config = ArticleConfigUM( diff --git a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleConfigUM.kt b/common/ui/src/main/java/com/tangem/common/ui/news/ArticleConfigUM.kt index e6e9477790..46e616eaab 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleConfigUM.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/news/ArticleConfigUM.kt @@ -1,5 +1,6 @@ package com.tangem.common.ui.news +import com.tangem.core.ui.components.label.entity.LabelUM import kotlinx.collections.immutable.ImmutableSet data class ArticleConfigUM( @@ -8,6 +9,6 @@ data class ArticleConfigUM( val score: Float, val createdAt: String, val isTrending: Boolean, - val tags: ImmutableSet, + val tags: ImmutableSet, val isViewed: Boolean, ) \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleHeader.kt b/common/ui/src/main/java/com/tangem/common/ui/news/ArticleHeader.kt new file mode 100644 index 0000000000..15bac87bdc --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/news/ArticleHeader.kt @@ -0,0 +1,55 @@ +package com.tangem.common.ui.news + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.label.Label +import com.tangem.core.ui.components.label.entity.LabelUM +import com.tangem.core.ui.res.TangemTheme +import kotlinx.collections.immutable.ImmutableList + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun ArticleHeader( + title: String, + createdAt: String, + score: Float, + tags: ImmutableList, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier) { + ArticleInfo( + score = score, + createdAt = createdAt, + ) + + Spacer(modifier = Modifier.height(12.dp)) + + Text( + text = title, + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + ) + + if (tags.isNotEmpty()) { + Spacer(modifier = Modifier.height(20.dp)) + FlowRow( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + tags.forEach { tag -> + Label( + state = tag, + ) + } + } + } + } +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleInfo.kt b/common/ui/src/main/java/com/tangem/common/ui/news/ArticleInfo.kt new file mode 100644 index 0000000000..f7cc656eae --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/news/ArticleInfo.kt @@ -0,0 +1,55 @@ +package com.tangem.common.ui.news + +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.draw.drawWithCache +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.R +import com.tangem.core.ui.res.TangemTheme + +@Composable +internal fun ArticleInfo(score: Float, createdAt: String, modifier: Modifier = Modifier) { + val dotColor = TangemTheme.colors.text.secondary + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Image( + imageVector = ImageVector.vectorResource(R.drawable.ic_start_circle_12), + contentDescription = null, + ) + + Text( + text = score.toString(), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.secondary, + ) + + Spacer( + modifier = Modifier + .size(4.dp) + .drawWithCache { + val radius = size.minDimension / 2f + onDrawBehind { + drawCircle( + color = dotColor, + radius = radius, + ) + } + }, + ) + + Text( + text = createdAt, + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.secondary, + ) + } +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleTagUM.kt b/common/ui/src/main/java/com/tangem/common/ui/news/ArticleTagUM.kt deleted file mode 100644 index 24eb867ad2..0000000000 --- a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleTagUM.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tangem.common.ui.news - -import androidx.compose.runtime.Immutable -import com.tangem.core.ui.components.currency.icon.CurrencyIconState -import com.tangem.core.ui.extensions.TextReference - -@Immutable -sealed interface ArticleTagUM { - - val title: TextReference - - data class Category(override val title: TextReference) : ArticleTagUM - - data class Token( - override val title: TextReference, - val iconState: CurrencyIconState, - ) : ArticleTagUM -} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt index 7b4e6dbb70..00b71108b4 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt @@ -220,6 +220,8 @@ fun SecondaryButtonIconEnd( modifier: Modifier = Modifier, showProgress: Boolean = false, enabled: Boolean = true, + size: TangemButtonSize = TangemButtonSize.Default, + shape: Shape = size.toShape(), ) { TangemButton( modifier = modifier, @@ -230,6 +232,8 @@ fun SecondaryButtonIconEnd( enabled = enabled, showProgress = showProgress, textStyle = TangemTheme.typography.subtitle1, + size = size, + shape = shape, ) } @@ -244,6 +248,8 @@ fun SecondaryButtonIconStart( modifier: Modifier = Modifier, showProgress: Boolean = false, enabled: Boolean = true, + size: TangemButtonSize = TangemButtonSize.Default, + shape: Shape = size.toShape(), ) { TangemButton( modifier = modifier, @@ -254,6 +260,8 @@ fun SecondaryButtonIconStart( enabled = enabled, showProgress = showProgress, textStyle = TangemTheme.typography.subtitle1, + size = size, + shape = shape, ) } // endregion SecondaryButton diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/label/Label.kt b/core/ui/src/main/java/com/tangem/core/ui/components/label/Label.kt index 2cc6209978..36e1eae057 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/label/Label.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/label/Label.kt @@ -17,11 +17,15 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.core.ui.R +import com.tangem.core.ui.components.label.entity.LabelLeadingContentUM +import com.tangem.core.ui.components.label.entity.LabelSize import com.tangem.core.ui.components.label.entity.LabelStyle import com.tangem.core.ui.components.label.entity.LabelUM import com.tangem.core.ui.extensions.TextReference @@ -37,6 +41,7 @@ import com.tangem.core.ui.res.TangemThemePreview * * @see Figma */ +@Suppress("LongMethod", "CyclomaticComplexMethod") @Composable fun Label(state: LabelUM, modifier: Modifier = Modifier) { val backgroundColor by animateColorAsState( @@ -63,12 +68,28 @@ fun Label(state: LabelUM, modifier: Modifier = Modifier) { }, ) - AnimatedContent(targetState = state.text) { text -> + val horizontalArrangementSize = remember { + when (state.size) { + LabelSize.REGULAR -> 4.dp + LabelSize.BIG -> 8.dp + } + } + + val paddings = remember { + when (state.size) { + LabelSize.REGULAR -> PaddingValues(horizontal = 8.dp, vertical = 4.dp) + LabelSize.BIG -> PaddingValues(horizontal = 16.dp, vertical = 8.dp) + } + } + + AnimatedContent( + modifier = modifier, + targetState = state.text, + ) { text -> Row( verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp), - modifier = modifier - .padding(horizontal = 4.dp) + horizontalArrangement = Arrangement.spacedBy(horizontalArrangementSize), + modifier = Modifier .clip(TangemTheme.shapes.roundedCorners8) .background(color = backgroundColor) .then( @@ -82,8 +103,22 @@ fun Label(state: LabelUM, modifier: Modifier = Modifier) { Modifier }, ) - .padding(horizontal = 8.dp, vertical = 4.dp), + .padding(paddings), ) { + state.leadingContent.let { leadingContentUM -> + when (leadingContentUM) { + is LabelLeadingContentUM.Token -> { + Icon( + modifier = Modifier + .size(16.dp), + painter = painterResource(leadingContentUM.icon), + tint = Color.Unspecified, + contentDescription = null, + ) + } + LabelLeadingContentUM.None -> Unit + } + } Text( modifier = Modifier.weight(1.0f, fill = false), text = text.resolveReference(), @@ -109,6 +144,8 @@ fun Label(state: LabelUM, modifier: Modifier = Modifier) { } } +@Suppress("LongMethod") +@OptIn(ExperimentalLayoutApi::class) @Preview(showBackground = true) @Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable @@ -118,47 +155,93 @@ private fun LabelPreview() { verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.padding(16.dp), ) { - Label( - state = LabelUM( - text = TextReference.Str("Regular Label"), - style = LabelStyle.REGULAR, - ), - ) - Label( - state = LabelUM( - text = TextReference.Str("Accent Label"), - style = LabelStyle.ACCENT, - ), - ) - Label( - state = LabelUM( - text = TextReference.Str("Warning Label"), - style = LabelStyle.WARNING, - ), - ) - Label( - state = LabelUM( - text = TextReference.Str( - "Regular long long long long long long long long long long long long Label", + FlowRow( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Label( + state = LabelUM( + text = TextReference.Str("Regular Label"), + style = LabelStyle.REGULAR, ), - style = LabelStyle.REGULAR, - icon = R.drawable.ic_information_24, - ), - ) - Label( - state = LabelUM( - text = TextReference.Str("Accent Label"), - style = LabelStyle.ACCENT, - icon = R.drawable.ic_information_24, - ), - ) - Label( - state = LabelUM( - text = TextReference.Str("Warning Label"), - style = LabelStyle.WARNING, - icon = R.drawable.ic_information_24, - ), - ) + ) + Label( + state = LabelUM( + leadingContent = LabelLeadingContentUM.Token( + icon = R.drawable.img_bsc_22, + ), + text = TextReference.Str("Regular Label"), + style = LabelStyle.REGULAR, + ), + ) + Label( + state = LabelUM( + text = TextReference.Str("Accent Label"), + style = LabelStyle.ACCENT, + ), + ) + Label( + state = LabelUM( + text = TextReference.Str("Warning Label"), + style = LabelStyle.WARNING, + ), + ) + } + FlowRow( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Label( + state = LabelUM( + text = TextReference.Str( + "Regular long long long long long long long long long long long long Label", + ), + style = LabelStyle.REGULAR, + icon = R.drawable.ic_information_24, + ), + ) + Label( + state = LabelUM( + text = TextReference.Str("Accent Label"), + style = LabelStyle.ACCENT, + icon = R.drawable.ic_information_24, + ), + ) + Label( + state = LabelUM( + text = TextReference.Str("Warning Label"), + style = LabelStyle.WARNING, + icon = R.drawable.ic_information_24, + ), + ) + } + FlowRow( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Label( + state = LabelUM( + text = TextReference.Str("Regular Label"), + style = LabelStyle.REGULAR, + size = LabelSize.BIG, + ), + ) + Label( + state = LabelUM( + text = TextReference.Str("Accent Label"), + style = LabelStyle.ACCENT, + size = LabelSize.BIG, + icon = R.drawable.ic_information_24, + ), + ) + Label( + state = LabelUM( + text = TextReference.Str("Warning Label"), + style = LabelStyle.WARNING, + size = LabelSize.BIG, + ), + ) + } } } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/label/entity/LabelUM.kt b/core/ui/src/main/java/com/tangem/core/ui/components/label/entity/LabelUM.kt index 9982a897b0..afa0e60bae 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/label/entity/LabelUM.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/label/entity/LabelUM.kt @@ -1,16 +1,29 @@ package com.tangem.core.ui.components.label.entity import androidx.annotation.DrawableRes +import androidx.compose.runtime.Immutable import com.tangem.core.ui.extensions.TextReference data class LabelUM( val text: TextReference, - val style: LabelStyle, + val style: LabelStyle = LabelStyle.REGULAR, + val size: LabelSize = LabelSize.REGULAR, + val leadingContent: LabelLeadingContentUM = LabelLeadingContentUM.None, @DrawableRes val icon: Int? = null, val onIconClick: (() -> Unit)? = null, val onClick: (() -> Unit)? = null, ) +@Immutable +sealed class LabelLeadingContentUM { + data object None : LabelLeadingContentUM() + data class Token(val icon: Int) : LabelLeadingContentUM() +} + enum class LabelStyle { REGULAR, ACCENT, WARNING, +} + +enum class LabelSize { + REGULAR, BIG, } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/pager/PagerIndicator.kt b/core/ui/src/main/java/com/tangem/core/ui/components/pager/PagerIndicator.kt new file mode 100644 index 0000000000..8dc43f2d53 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/pager/PagerIndicator.kt @@ -0,0 +1,130 @@ +package com.tangem.core.ui.components.pager + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.pager.PagerState +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +/** + * Horizontal pager indicator + * + * @param pagerState state of pager + * @param indicatorCount counter of visible indicator items + */ +@Composable +fun PagerIndicator(pagerState: PagerState, modifier: Modifier = Modifier, indicatorCount: Int = 5) { + val listState = rememberLazyListState() + + val indicatorColor = TangemTheme.colors.control.key + val overlayColor = TangemTheme.colors.overlay.secondary + val indicatorSize = 8.dp + val spacing = 4.dp + + val totalWidth: Dp = indicatorSize * indicatorCount + spacing * (indicatorCount - 1) + val widthInPx = LocalDensity.current.run { indicatorSize.toPx() } + + val currentItem by remember { + derivedStateOf { + pagerState.currentPage + } + } + + val itemCount = pagerState.pageCount + + LaunchedEffect(key1 = currentItem) { + val viewportSize = listState.layoutInfo.viewportSize + listState.animateScrollToItem( + currentItem, + (widthInPx / 2 - viewportSize.width / 2).toInt(), + ) + } + + Box( + modifier = modifier + .height(32.dp) + .background( + color = overlayColor, + shape = CircleShape, + ) + .padding(horizontal = 16.dp, vertical = 12.dp), + contentAlignment = Alignment.Center, + ) { + LazyRow( + modifier = Modifier + .width(totalWidth), + state = listState, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + userScrollEnabled = false, + ) { + indicatorItems( + itemCount = itemCount, + currentItem = currentItem, + indicatorShape = CircleShape, + activeColor = indicatorColor, + inActiveColor = indicatorColor.copy(alpha = 0.5f), + indicatorSize = indicatorSize, + ) + } + } +} + +@Suppress("LongParameterList") +private fun LazyListScope.indicatorItems( + itemCount: Int, + currentItem: Int, + indicatorShape: Shape, + activeColor: Color, + inActiveColor: Color, + indicatorSize: Dp, +) { + items(itemCount) { index -> + + val isSelected = index == currentItem + + Box( + modifier = Modifier + .clip(indicatorShape) + .size(indicatorSize) + .background( + if (isSelected) activeColor else inActiveColor, + indicatorShape, + ), + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun PagerIndicatorPreviewFirstPage() { + TangemThemePreview { + Box( + modifier = Modifier + .background(TangemTheme.colors.background.primary) + .padding(), + contentAlignment = Alignment.Center, + ) { + val pagerState = rememberPagerState( + initialPage = 0, + pageCount = { 10 }, + ) + PagerIndicator(pagerState = pagerState) + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_explore_16.xml b/core/ui/src/main/res/drawable/ic_explore_16.xml new file mode 100644 index 0000000000..a739a875d4 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_explore_16.xml @@ -0,0 +1,12 @@ + + + + diff --git a/core/ui/src/main/res/drawable/ic_heart_20.xml b/core/ui/src/main/res/drawable/ic_heart_20.xml new file mode 100644 index 0000000000..6e0f036179 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_heart_20.xml @@ -0,0 +1,15 @@ + + + + + + diff --git a/core/ui/src/main/res/drawable/ic_quick_recap_16.xml b/core/ui/src/main/res/drawable/ic_quick_recap_16.xml new file mode 100644 index 0000000000..11a259eed3 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_quick_recap_16.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/ui/CreateWalletSelectionContent.kt b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/ui/CreateWalletSelectionContent.kt index 7fde4ee3ac..07cdb0689e 100644 --- a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/ui/CreateWalletSelectionContent.kt +++ b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/ui/CreateWalletSelectionContent.kt @@ -5,6 +5,7 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -137,11 +138,12 @@ private fun WalletBlock( vertical = 12.dp, ), ) { - Row { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { Text( modifier = Modifier - .weight(1f, fill = false) - .padding(end = 8.dp), + .weight(1f, fill = false), text = title, style = TangemTheme.typography.subtitle1, color = TangemTheme.colors.text.primary1, diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/preview/FeedListPreviewDataProvider.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/preview/FeedListPreviewDataProvider.kt index 734644fa3d..9eaf7e5c4b 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/preview/FeedListPreviewDataProvider.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/preview/FeedListPreviewDataProvider.kt @@ -4,12 +4,13 @@ import com.tangem.common.ui.charts.state.MarketChartRawData import com.tangem.core.ui.components.fields.entity.SearchBarUM import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.common.ui.news.ArticleConfigUM -import com.tangem.common.ui.news.ArticleTagUM -import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.components.label.entity.LabelLeadingContentUM +import com.tangem.core.ui.components.label.entity.LabelUM import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.features.feed.impl.R import com.tangem.features.feed.ui.feed.state.* import com.tangem.features.feed.ui.market.state.MarketsListItemUM import com.tangem.features.feed.ui.market.state.SortByTypeUM @@ -159,21 +160,18 @@ internal object FeedListPreviewDataProvider { ), ) - private fun createArticleTags(): ImmutableSet { + private fun createArticleTags(): ImmutableSet { return persistentSetOf( - ArticleTagUM.Token( - title = TextReference.Str("BTC"), - iconState = CurrencyIconState.CoinIcon( - url = "", - fallbackResId = 0, - isGrayscale = false, - shouldShowCustomBadge = false, + LabelUM( + text = TextReference.Str("BTC"), + leadingContent = LabelLeadingContentUM.Token( + icon = R.drawable.img_btc_22, ), ), - ArticleTagUM.Category(TextReference.Str("Regulation")), - ArticleTagUM.Category(TextReference.Str("BTC")), - ArticleTagUM.Category(TextReference.Str("Supply")), - ArticleTagUM.Category(TextReference.Str("Demand")), + LabelUM(TextReference.Str("Regulation")), + LabelUM(TextReference.Str("BTC")), + LabelUM(TextReference.Str("Supply")), + LabelUM(TextReference.Str("Demand")), ) } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/common/ui/OptionBlock.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/common/ui/OptionBlock.kt index f76ab84370..6f7f584bfb 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/common/ui/OptionBlock.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/common/ui/OptionBlock.kt @@ -1,6 +1,7 @@ package com.tangem.features.hotwallet.common.ui import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth @@ -43,11 +44,12 @@ internal fun OptionBlock( } .padding(16.dp), ) { - Row { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { Text( modifier = Modifier - .weight(1f, fill = false) - .padding(end = 4.dp), + .weight(1f, fill = false), text = title, style = TangemTheme.typography.subtitle1, color = TangemTheme.colors.text.primary1, diff --git a/features/news/news-details/api/build.gradle.kts b/features/news/news-details/api/build.gradle.kts new file mode 100644 index 0000000000..b68f6815c6 --- /dev/null +++ b/features/news/news-details/api/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.news.details.api" +} + +dependencies { + /* Project - Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) + + /* Compose */ + implementation(deps.compose.runtime) +} diff --git a/features/news/news-details/api/src/main/kotlin/com/tangem/features/news/details/api/NewsDetailsComponent.kt b/features/news/news-details/api/src/main/kotlin/com/tangem/features/news/details/api/NewsDetailsComponent.kt new file mode 100644 index 0000000000..167f65b88f --- /dev/null +++ b/features/news/news-details/api/src/main/kotlin/com/tangem/features/news/details/api/NewsDetailsComponent.kt @@ -0,0 +1,11 @@ +package com.tangem.features.news.details.api + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent + +interface NewsDetailsComponent : ComposableContentComponent { + + data class Params(val selectedArticleId: Int = 0) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/news/news-details/impl/build.gradle.kts b/features/news/news-details/impl/build.gradle.kts new file mode 100644 index 0000000000..a9953868af --- /dev/null +++ b/features/news/news-details/impl/build.gradle.kts @@ -0,0 +1,43 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.hilt.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.news.details.impl" +} + +dependencies { + /* AndroidX */ + implementation(deps.lifecycle.compose) + implementation(deps.androidx.activity.compose) + + /** Compose */ + implementation(deps.compose.foundation) + implementation(deps.compose.ui) + implementation(deps.compose.ui.tooling) + implementation(deps.compose.material3) + + /** Core modules */ + implementation(projects.core.ui) + implementation(projects.core.utils) + implementation(projects.core.decompose) + implementation(projects.common.ui) + implementation(projects.common.routing) + + /** Feature modules */ + implementation(projects.features.news.newsDetails.api) + implementation(projects.domain.models) + + /** Other dependencies */ + implementation(deps.kotlin.immutable.collections) + implementation(deps.arrow.core) + implementation(deps.timber) + + /** DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) +} diff --git a/features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/DefaultNewsDetailsComponent.kt b/features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/DefaultNewsDetailsComponent.kt new file mode 100644 index 0000000000..d68562beb6 --- /dev/null +++ b/features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/DefaultNewsDetailsComponent.kt @@ -0,0 +1,39 @@ +package com.tangem.features.news.details.impl + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Modifier +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.news.details.api.NewsDetailsComponent +import com.tangem.features.news.details.impl.ui.NewsDetailsContent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultNewsDetailsComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + @Assisted params: NewsDetailsComponent.Params, +) : NewsDetailsComponent, AppComponentContext by context { + + private val model: NewsDetailsModel = getOrCreateModel(params) + + @Composable + override fun Content(modifier: Modifier) { + val uiState by model.uiState.collectAsState() + NewsDetailsContent( + state = uiState, + onBackClick = model::onBackClick, + modifier = modifier, + ) + } + + @AssistedFactory + interface Factory : NewsDetailsComponent.Factory { + override fun create( + context: AppComponentContext, + params: NewsDetailsComponent.Params, + ): DefaultNewsDetailsComponent + } +} \ No newline at end of file diff --git a/features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/MockArticlesFactory.kt b/features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/MockArticlesFactory.kt new file mode 100644 index 0000000000..ac35bb6ee4 --- /dev/null +++ b/features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/MockArticlesFactory.kt @@ -0,0 +1,182 @@ +package com.tangem.features.news.details.impl + +import com.tangem.core.ui.components.label.entity.LabelSize +import com.tangem.core.ui.components.label.entity.LabelUM +import com.tangem.core.ui.extensions.TextReference +import com.tangem.features.news.details.impl.ui.ArticleUM +import com.tangem.features.news.details.impl.ui.SourceUM +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList + +// TODO [REDACTED_TASK_KEY] remove mock data +@Suppress("MaximumLineLength", "LongMethod") +object MockArticlesFactory { + fun createMockArticles(): ImmutableList = listOf( + ArticleUM( + id = 1, + title = "SEC delays decisions on ETH-staking ETFs and spot XRP/SOL funds", + createdAt = "20 Jun, 21:45", + score = 6.5f, + tags = listOf( + LabelUM(text = TextReference.Str("Regulation"), size = LabelSize.BIG), + ).toPersistentList(), + shortContent = "Bitwise has updated its Solana ETF, adding staking and setting a low management fee of 0.20%.", + content = "Bitwise Asset Management has revised its filing to launch a Solana ETF, renaming it \"Bitwise Solana Staking ETF\" and setting an exceptionally low management fee of just 0.20%.\n\nThe update comes as the SEC prepares to review several Solana ETF applications.", + sources = listOf( + SourceUM( + id = 1, + title = "Deeper liquidity could drive crypto market beyond \$6T", + sourceName = "cointelegraph", + publishedAt = "1h ago", + url = "https://cointelegraph.com", + ), + SourceUM( + id = 2, + title = "Top gainers and losers in crypto this week", + sourceName = "Investing", + publishedAt = "2h ago", + url = "https://investing.com", + ), + ).toPersistentList(), + ), + ArticleUM( + id = 2, + title = "Bitcoin ETFs log 4th straight day of inflows (+\$550M)", + createdAt = "20 Jun, 20:15", + score = 8.2f, + tags = listOf( + LabelUM(text = TextReference.Str("BTC"), size = LabelSize.BIG), + LabelUM(text = TextReference.Str("ETF"), size = LabelSize.BIG), + ).toPersistentList(), + shortContent = "Bitcoin spot ETFs recorded fourth consecutive day of positive inflows.", + content = "Bitcoin spot ETFs continue their impressive streak with \$550 million in net positive inflows.\n\nBlackRock's IBIT led with \$250M.", + sources = listOf( + SourceUM( + id = 3, + title = "Bitcoin ETFs see massive inflows", + sourceName = "Bloomberg", + publishedAt = "3h ago", + url = "https://bloomberg.com", + ), + ).toPersistentList(), + ), + ArticleUM( + id = 3, + title = "Ethereum network upgrade scheduled for Q2 2025", + createdAt = "20 Jun, 18:30", + score = 7.8f, + tags = listOf( + LabelUM(text = TextReference.Str("ETH"), size = LabelSize.BIG), + LabelUM(text = TextReference.Str("Technology"), size = LabelSize.BIG), + ).toPersistentList(), + shortContent = "Ethereum developers announced major network upgrade.", + content = "The Ethereum Foundation announced a significant upgrade for Q2 2025.\n\nKey improvements include EVM enhancements.", + sources = listOf( + SourceUM( + id = 4, + title = "Ethereum core devs announce upgrade", + sourceName = "CoinDesk", + publishedAt = "5h ago", + url = "https://coindesk.com", + ), + ).toPersistentList(), + ), + ArticleUM( + id = 4, + title = "Solana surpasses Ethereum in daily transaction volume", + createdAt = "20 Jun, 16:00", + score = 9.1f, + tags = listOf( + LabelUM(text = TextReference.Str("SOL"), size = LabelSize.BIG), + LabelUM(text = TextReference.Str("Market"), size = LabelSize.BIG), + ).toPersistentList(), + shortContent = "Solana achieved new milestone processing more daily transactions than Ethereum.", + content = "Solana processed over 50 million transactions in a single day.\n\nDriven by DeFi and NFT activity.", + sources = listOf( + SourceUM( + id = 5, + title = "Solana transactions hit record", + sourceName = "The Block", + publishedAt = "7h ago", + url = "https://theblock.co", + ), + ).toPersistentList(), + ), + ArticleUM( + id = 5, + title = "DeFi protocol launches innovative yield farming strategy", + createdAt = "20 Jun, 14:20", + score = 6.9f, + tags = listOf( + LabelUM(text = TextReference.Str("DeFi"), size = LabelSize.BIG), + ).toPersistentList(), + shortContent = "New DeFi protocol introduced innovative yield farming approach.", + content = "A newly launched protocol unveiled innovative yield farming mechanism.\n\nAPY rates range from 15% to 30%.", + sources = persistentListOf(), + ), + ArticleUM( + id = 6, + title = "Crypto regulation bill advances in US Senate", + createdAt = "20 Jun, 12:45", + score = 8.7f, + tags = listOf( + LabelUM(text = TextReference.Str("Regulation"), size = LabelSize.BIG), + LabelUM(text = TextReference.Str("USA"), size = LabelSize.BIG), + ).toPersistentList(), + shortContent = "Comprehensive cryptocurrency regulation bill passed Senate Banking Committee.", + content = "The US Senate Banking Committee advanced landmark crypto regulation bill.\n\nKey provisions include asset definitions.", + sources = persistentListOf(), + ), + ArticleUM( + id = 7, + title = "Major bank announces crypto custody services", + createdAt = "20 Jun, 10:30", + score = 7.3f, + tags = listOf( + LabelUM(text = TextReference.Str("Adoption"), size = LabelSize.BIG), + ).toPersistentList(), + shortContent = "World's largest bank announced cryptocurrency custody services.", + content = "Major financial institution announced comprehensive crypto custody services.\n\nSupporting Bitcoin and Ethereum initially.", + sources = persistentListOf(), + ), + ArticleUM( + id = 8, + title = "NFT marketplace reports 300% increase in trading volume", + createdAt = "20 Jun, 08:15", + score = 5.8f, + tags = listOf( + LabelUM(text = TextReference.Str("NFT"), size = LabelSize.BIG), + ).toPersistentList(), + shortContent = "Leading NFT marketplace experienced dramatic surge in trading activity.", + content = "Prominent NFT marketplace reported 300% increase in trading volume.\n\nNew features include lower fees.", + sources = persistentListOf(), + ), + ArticleUM( + id = 9, + title = "Layer 2 solution achieves 100,000 TPS milestone", + createdAt = "19 Jun, 22:00", + score = 8.5f, + tags = listOf( + LabelUM(text = TextReference.Str("Technology"), size = LabelSize.BIG), + LabelUM(text = TextReference.Str("L2"), size = LabelSize.BIG), + ).toPersistentList(), + shortContent = "New Layer 2 scaling solution achieved 100,000 TPS in testing.", + content = "Layer 2 solution processed 100,000 transactions per second.\n\nUsing zero-knowledge proof technology.", + sources = persistentListOf(), + ), + ArticleUM( + id = 10, + title = "Stablecoin market cap reaches new all-time high", + createdAt = "19 Jun, 19:30", + score = 7.6f, + tags = listOf( + LabelUM(text = TextReference.Str("Stablecoins"), size = LabelSize.BIG), + LabelUM(text = TextReference.Str("Market"), size = LabelSize.BIG), + ).toPersistentList(), + shortContent = "Total stablecoin market capitalization surpassed \$180 billion.", + content = "Stablecoin market cap reached \$180 billion all-time high.\n\nDriven by DeFi activity and institutional adoption.", + sources = persistentListOf(), + ), + ).toPersistentList() +} \ No newline at end of file diff --git a/features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/NewsDetailsModel.kt b/features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/NewsDetailsModel.kt new file mode 100644 index 0000000000..3ff89dde9d --- /dev/null +++ b/features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/NewsDetailsModel.kt @@ -0,0 +1,41 @@ +package com.tangem.features.news.details.impl + +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.navigation.Router +import com.tangem.features.news.details.api.NewsDetailsComponent +import com.tangem.features.news.details.impl.ui.NewsDetailsUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.extensions.indexOfFirstOrNull +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import javax.inject.Inject + +@ModelScoped +internal class NewsDetailsModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, + private val router: Router, +) : Model() { + + private val mockedArticles = MockArticlesFactory.createMockArticles() + + private val params = paramsContainer.require() + + private val _uiState = MutableStateFlow( + NewsDetailsUM( + /* [REDACTED_TODO_COMMENT] */ + articles = mockedArticles, + selectedArticleIndex = mockedArticles.indexOfFirstOrNull { it.id == params.selectedArticleId } ?: 0, + onShareClick = { /* [REDACTED_TODO_COMMENT] */ }, + onLikeClick = { /* [REDACTED_TODO_COMMENT] */ }, + ), + ) + val uiState: StateFlow = _uiState.asStateFlow() + + fun onBackClick() { + router.pop() + } +} \ No newline at end of file diff --git a/features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/di/NewsDetailsModule.kt b/features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/di/NewsDetailsModule.kt new file mode 100644 index 0000000000..f0fcac783e --- /dev/null +++ b/features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/di/NewsDetailsModule.kt @@ -0,0 +1,18 @@ +package com.tangem.features.news.details.impl.di + +import com.tangem.features.news.details.api.NewsDetailsComponent +import com.tangem.features.news.details.impl.DefaultNewsDetailsComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface NewsDetailsModule { + + @Binds + @Singleton + fun bindNewsDetailsComponentFactory(factory: DefaultNewsDetailsComponent.Factory): NewsDetailsComponent.Factory +} \ No newline at end of file diff --git a/features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/ui/NewsDetailsContent.kt b/features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/ui/NewsDetailsContent.kt new file mode 100644 index 0000000000..a149f57bf4 --- /dev/null +++ b/features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/ui/NewsDetailsContent.kt @@ -0,0 +1,278 @@ +package com.tangem.features.news.details.impl.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.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.material3.VerticalDivider +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.common.ui.news.ArticleHeader +import com.tangem.core.ui.R +import com.tangem.core.ui.components.SecondaryButtonIconStart +import com.tangem.core.ui.components.appbar.TangemTopAppBar +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.components.buttons.common.TangemButtonSize +import com.tangem.core.ui.components.pager.PagerIndicator +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.news.details.impl.MockArticlesFactory + +// TODO [REDACTED_TASK_KEY] make internal +@Composable +fun NewsDetailsContent(state: NewsDetailsUM, onBackClick: () -> Unit, modifier: Modifier = Modifier) { + val pagerState = rememberPagerState( + initialPage = state.selectedArticleIndex, + pageCount = { state.articles.size }, + ) + + Column( + modifier = modifier + .fillMaxSize() + .background(TangemTheme.colors.background.secondary) + .systemBarsPadding(), + ) { + Column { + TangemTopAppBar( + title = null, + startButton = TopAppBarButtonUM.Icon( + iconRes = R.drawable.ic_back_24, + onClicked = onBackClick, + ), + endButton = TopAppBarButtonUM.Icon( + iconRes = R.drawable.ic_share_24, + onClicked = state.onShareClick, + ), + ) + Box( + modifier = Modifier.fillMaxSize(), + ) { + if (state.articles.isNotEmpty()) { + HorizontalPager( + state = pagerState, + modifier = Modifier.fillMaxSize(), + ) { page -> + ArticleDetail( + article = state.articles[page], + modifier = Modifier.fillMaxSize(), + onLikeClick = state.onLikeClick, + ) + } + + if (state.articles.size > 1) { + Column( + modifier = Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth(), + ) { + PagerIndicator( + pagerState = pagerState, + modifier = Modifier + .align(Alignment.CenterHorizontally) + .padding(bottom = 16.dp), + ) + } + } + } + } + } + } +} + +@Suppress("LongMethod") +@Composable +private fun ArticleDetail(article: ArticleUM, modifier: Modifier = Modifier, onLikeClick: () -> Unit) { + val density = LocalDensity.current + val pagerHeight = 48.dp + val contentPadding = 56.dp + LazyColumn( + modifier = modifier.padding(horizontal = 16.dp), + contentPadding = PaddingValues( + bottom = contentPadding + pagerHeight + WindowInsets.navigationBars.getBottom(density).dp, + ), + ) { + item { + ArticleHeader( + title = article.title, + createdAt = article.createdAt, + score = article.score, + tags = article.tags, + modifier = Modifier.padding(top = 16.dp), + ) + + if (article.shortContent.isNotEmpty()) { + QuickRecap( + content = article.shortContent, + modifier = Modifier.padding(top = 32.dp), + ) + } + + Text( + text = article.content, + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + modifier = Modifier.padding(top = 16.dp), + ) + + Spacer(modifier = Modifier.height(24.dp)) + + SecondaryButtonIconStart( + iconResId = R.drawable.ic_heart_20, + text = "Like", // TODO [REDACTED_TASK_KEY] export to strings + size = TangemButtonSize.RoundedAction, + onClick = onLikeClick, + ) + + // TODO [REDACTED_TASK_KEY] add related tokens block + + if (article.sources.isNotEmpty()) { + Spacer(modifier = Modifier.height(24.dp)) + Row { + Text( + text = "Sources", // TODO [REDACTED_TASK_KEY] export to strings + style = TangemTheme.typography.h3, + color = TangemTheme.colors.text.primary1, + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = "${article.sources.size}", + style = TangemTheme.typography.h3, + color = TangemTheme.colors.text.tertiary, + ) + } + } + } + + if (article.sources.isNotEmpty()) { + item { + val sourcesPagerState = rememberPagerState( + pageCount = { article.sources.size }, + ) + Spacer(modifier = Modifier.height(12.dp)) + HorizontalPager( + state = sourcesPagerState, + modifier = Modifier + .fillMaxWidth(), + pageSpacing = 12.dp, + contentPadding = PaddingValues(horizontal = 0.dp), + ) { page -> + SourceItem( + source = article.sources[page], + modifier = Modifier.fillMaxWidth(), + ) + } + Spacer(modifier = Modifier.height(12.dp)) + } + } + } +} + +@Composable +private fun QuickRecap(content: String, modifier: Modifier = Modifier) { + Box( + modifier = modifier.height(IntrinsicSize.Min), + ) { + VerticalDivider( + modifier = Modifier.fillMaxHeight(), + thickness = 2.dp, + color = TangemTheme.colors.stroke.primary, + ) + Column( + modifier = Modifier + .padding(start = 16.dp), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + painter = painterResource(id = R.drawable.ic_quick_recap_16), + contentDescription = null, + tint = TangemTheme.colors.icon.accent, + modifier = Modifier.size(20.dp), + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = "Quick recap", // TODO [REDACTED_TASK_KEY] export to strings + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.accent, + ) + } + Spacer(modifier = Modifier.height(12.dp)) + Text( + text = content, + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + ) + } + } +} + +@Composable +private fun SourceItem(source: SourceUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .background( + color = TangemTheme.colors.background.primary, + shape = RoundedCornerShape(12.dp), + ) + .padding(12.dp), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(bottom = 4.dp), + ) { + Icon( + painter = painterResource(id = R.drawable.ic_explore_16), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + modifier = Modifier.size(20.dp), + ) + Spacer(modifier = Modifier.width(4.dp)) + Text( + text = source.sourceName, + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.tertiary, + ) + } + if (source.title.isNotEmpty()) { + Text( + text = source.title, + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.primary1, + modifier = Modifier.padding(bottom = 12.dp), + ) + } + Text( + text = source.publishedAt, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun PreviewNewsDetailsContent() { + TangemThemePreview { + NewsDetailsContent( + state = NewsDetailsUM( + articles = MockArticlesFactory.createMockArticles(), + selectedArticleIndex = 0, + onShareClick = { }, + onLikeClick = { }, + ), + onBackClick = { }, + ) + } +} \ No newline at end of file diff --git a/features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/ui/NewsDetailsUM.kt b/features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/ui/NewsDetailsUM.kt new file mode 100644 index 0000000000..889fcc202a --- /dev/null +++ b/features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/ui/NewsDetailsUM.kt @@ -0,0 +1,33 @@ +package com.tangem.features.news.details.impl.ui + +import com.tangem.core.ui.components.label.entity.LabelUM +import kotlinx.collections.immutable.ImmutableList + +// TODO [REDACTED_TASK_KEY] make internal +data class NewsDetailsUM( + val articles: ImmutableList, + val selectedArticleIndex: Int, + val onShareClick: () -> Unit, + val onLikeClick: () -> Unit, +) + +// TODO [REDACTED_TASK_KEY] make internal +data class ArticleUM( + val id: Int, + val title: String, + val createdAt: String, + val score: Float, + val tags: ImmutableList, + val shortContent: String, + val content: String, + val sources: ImmutableList, +) + +// TODO [REDACTED_TASK_KEY] make internal +data class SourceUM( + val id: Int, + val title: String, + val sourceName: String, + val publishedAt: String, + val url: String, +) \ No newline at end of file diff --git a/features/tester/impl/build.gradle.kts b/features/tester/impl/build.gradle.kts index 57fe70672c..ba40b749c4 100644 --- a/features/tester/impl/build.gradle.kts +++ b/features/tester/impl/build.gradle.kts @@ -59,6 +59,8 @@ dependencies { /** Feature Apis */ implementation(projects.features.tester.api) implementation(projects.features.pushNotifications.api) + implementation(projects.features.news.newsDetails.api) + implementation(projects.features.news.newsDetails.impl) /* SDK */ implementation(tangemDeps.blockchain) diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt index bab7b098d3..393fac33ad 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt @@ -35,6 +35,11 @@ import com.tangem.feature.tester.presentation.providers.ui.BlockchainProvidersSc import com.tangem.feature.tester.presentation.providers.viewmodel.BlockchainProvidersViewModel import com.tangem.feature.tester.presentation.testpush.ui.TestPushScreen import com.tangem.feature.tester.presentation.testpush.viewmodel.TestPushViewModel +import com.tangem.feature.tester.presentation.news.ui.NewsScreen +import com.tangem.feature.tester.presentation.news.viewmodel.NewsViewModel +import com.tangem.features.news.details.impl.MockArticlesFactory +import com.tangem.features.news.details.impl.ui.NewsDetailsContent +import com.tangem.features.news.details.impl.ui.NewsDetailsUM import dagger.hilt.android.AndroidEntryPoint import kotlinx.collections.immutable.persistentSetOf import javax.inject.Inject @@ -82,9 +87,10 @@ internal class TesterActivity : ComposeActivity() { ButtonUM.TESTER_ACTIONS, ButtonUM.TEST_PUSHES, ButtonUM.ACCOUNTS, + ButtonUM.NEWS, ), - onButtonClick = { - val route = when (it) { + onButtonClick = { buttonUM -> + val route = when (buttonUM) { ButtonUM.FEATURE_TOGGLES -> TesterScreen.FEATURE_TOGGLES ButtonUM.EXCLUDED_BLOCKCHAINS -> TesterScreen.EXCLUDED_BLOCKCHAINS ButtonUM.ENVIRONMENT_TOGGLES -> TesterScreen.ENVIRONMENTS_TOGGLES @@ -92,6 +98,7 @@ internal class TesterActivity : ComposeActivity() { ButtonUM.TESTER_ACTIONS -> TesterScreen.TESTER_ACTIONS ButtonUM.TEST_PUSHES -> TesterScreen.TEST_PUSHES ButtonUM.ACCOUNTS -> TesterScreen.ACCOUNTS + ButtonUM.NEWS -> TesterScreen.NEWS } innerTesterRouter.open(route) @@ -162,6 +169,27 @@ internal class TesterActivity : ComposeActivity() { AccountsScreen(state) } + + composable(route = TesterScreen.NEWS.name) { + val viewModel = hiltViewModel().apply { + setupNavigation(innerTesterRouter) + } + val state by viewModel.uiState.collectAsStateWithLifecycle() + + NewsScreen(state) + } + + composable(route = TesterScreen.NEWS_DETAILS.name) { + NewsDetailsContent( + state = NewsDetailsUM( + articles = MockArticlesFactory.createMockArticles(), + selectedArticleIndex = 0, + onLikeClick = { }, + onShareClick = { }, + ), + onBackClick = { innerTesterRouter.back() }, + ) + } } } } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuUM.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuUM.kt index fa30058309..00a019e9c4 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuUM.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuUM.kt @@ -25,5 +25,6 @@ data class TesterMenuUM( TESTER_ACTIONS(R.string.tester_actions), TEST_PUSHES(R.string.test_push), ACCOUNTS(R.string.accounts), + NEWS(R.string.news), } } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/TesterScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/TesterScreen.kt index e87d942c19..854706cec2 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/TesterScreen.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/TesterScreen.kt @@ -14,4 +14,6 @@ internal enum class TesterScreen { BLOCKCHAIN_PROVIDERS, TEST_PUSHES, ACCOUNTS, + NEWS, + NEWS_DETAILS, } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/news/state/NewsUM.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/news/state/NewsUM.kt new file mode 100644 index 0000000000..b832b363d8 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/news/state/NewsUM.kt @@ -0,0 +1,16 @@ +package com.tangem.feature.tester.presentation.news.state + +import androidx.annotation.StringRes +import com.tangem.feature.tester.impl.R +import kotlinx.collections.immutable.ImmutableSet + +data class NewsUM( + val onBackClick: () -> Unit, + val buttons: ImmutableSet, + val onButtonClick: (ButtonUM) -> Unit, +) { + + enum class ButtonUM(@StringRes val textResId: Int) { + NEWS_DETAILS(R.string.news_details), + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/news/ui/NewsScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/news/ui/NewsScreen.kt new file mode 100644 index 0000000000..2f0b9c690c --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/news/ui/NewsScreen.kt @@ -0,0 +1,62 @@ +package com.tangem.feature.tester.presentation.news.ui + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.items +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.appbar.AppBarWithBackButton +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.tester.impl.R +import com.tangem.feature.tester.presentation.news.state.NewsUM +import kotlinx.collections.immutable.ImmutableSet + +@OptIn(ExperimentalFoundationApi::class) +@Composable +internal fun NewsScreen(state: NewsUM, modifier: Modifier = Modifier) { + BackHandler(onBack = state.onBackClick) + + LazyColumn( + modifier = modifier + .fillMaxSize() + .background(TangemTheme.colors.background.primary), + ) { + stickyHeader { AppBar(onBackClick = state.onBackClick) } + + NewsButtons(buttons = state.buttons, onButtonClick = state.onButtonClick) + } +} + +@Composable +private fun AppBar(onBackClick: () -> Unit) { + AppBarWithBackButton( + onBackClick = onBackClick, + text = stringResourceSafe(id = R.string.news), + containerColor = TangemTheme.colors.background.primary, + ) +} + +@Suppress("FunctionNaming") +private fun LazyListScope.NewsButtons( + buttons: ImmutableSet, + onButtonClick: (NewsUM.ButtonUM) -> Unit, +) { + items(buttons.toList()) { button -> + PrimaryButton( + text = stringResourceSafe(button.textResId), + onClick = { onButtonClick(button) }, + modifier = Modifier + .padding(horizontal = 16.dp, vertical = 8.dp) + .fillMaxWidth(), + ) + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/news/viewmodel/NewsViewModel.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/news/viewmodel/NewsViewModel.kt new file mode 100644 index 0000000000..5ed86201e4 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/news/viewmodel/NewsViewModel.kt @@ -0,0 +1,44 @@ +package com.tangem.feature.tester.presentation.news.viewmodel + +import androidx.lifecycle.ViewModel +import com.tangem.feature.tester.presentation.navigation.InnerTesterRouter +import com.tangem.feature.tester.presentation.navigation.TesterScreen +import com.tangem.feature.tester.presentation.news.state.NewsUM +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.persistentSetOf +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject + +@HiltViewModel +internal class NewsViewModel @Inject constructor() : ViewModel() { + + private val _uiState = MutableStateFlow(createInitialState()) + val uiState: StateFlow = _uiState + + private var router: InnerTesterRouter? = null + + fun setupNavigation(router: InnerTesterRouter) { + this.router = router + } + + private fun createInitialState(): NewsUM { + return NewsUM( + onBackClick = ::onBackClick, + buttons = persistentSetOf( + NewsUM.ButtonUM.NEWS_DETAILS, + ), + onButtonClick = ::onButtonClick, + ) + } + + private fun onBackClick() { + router?.back() + } + + private fun onButtonClick(button: NewsUM.ButtonUM) { + when (button) { + NewsUM.ButtonUM.NEWS_DETAILS -> router?.open(TesterScreen.NEWS_DETAILS) + } + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/res/values/strings.xml b/features/tester/impl/src/main/res/values/strings.xml index c47c184c25..15b31a07c6 100644 --- a/features/tester/impl/src/main/res/values/strings.xml +++ b/features/tester/impl/src/main/res/values/strings.xml @@ -18,4 +18,6 @@ Share logs Test push Accounts + News + News details diff --git a/settings.gradle.kts b/settings.gradle.kts index 60632a9860..f6e74fac62 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -300,6 +300,9 @@ include(":features:yield-supply:impl") include(":features:feed:api") include(":features:feed:impl") + +include(":features:news:news-details:api") +include(":features:news:news-details:impl") // endregion Feature modules // region Domain modules