Updated on 2026-08-14
This commit is contained in:
parent
0d4fd5874e
commit
8cd088188f
20 changed files with 1119 additions and 489 deletions
|
|
@ -851,6 +851,7 @@
|
|||
<string name="news_related_tokens">Related tokens</string>
|
||||
<string name="news_sources">Related news</string>
|
||||
<string name="news_stay_in_the_loop">Stay in the loop</string>
|
||||
<string name="news_trending_score">Trending score</string>
|
||||
<string name="nfc_error_unavailable">NFC is not available on your device</string>
|
||||
<string name="nft_about_title">About NFT</string>
|
||||
<string name="nft_asset">NFT asset</string>
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ fun BottomFadeWithBlur(backgroundColor: Color, modifier: Modifier = Modifier) {
|
|||
),
|
||||
),
|
||||
),
|
||||
backgroundColor = Color.Transparent,
|
||||
),
|
||||
) {
|
||||
progressive =
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ fun Chip(state: ChipUM, modifier: Modifier = Modifier) {
|
|||
@Preview(showBackground = true)
|
||||
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun ChipPreviewV() {
|
||||
private fun ChipPreview() {
|
||||
TangemThemePreview {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package com.tangem.core.ui.components.pager
|
||||
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
|
|
@ -22,132 +21,147 @@ import androidx.compose.ui.unit.Dp
|
|||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.haze.hazeEffectTangem
|
||||
import com.tangem.core.ui.res.LocalRedesignEnabled
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||
import dev.chrisbanes.haze.HazeStyle
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
private const val ANIMATION_DURATION = 300
|
||||
private const val MAX_VISIBLE_DOTS = 5
|
||||
internal const val ANIMATION_DURATION = 300
|
||||
internal const val MAX_VISIBLE_DOTS = 5
|
||||
internal val SPACING = 4.dp
|
||||
internal val HINT_DOT_SIZE = DpSize(6.dp, 6.dp)
|
||||
private const val MIN_HIDDEN_FOR_SMALL_DOT = 2
|
||||
private const val MIN_DISTANCE_FOR_SMALL_DOT = 3
|
||||
private const val MIN_DISTANCE_FOR_HINT_DOT = 2
|
||||
|
||||
private val SPACING = 4.dp
|
||||
private val BACKGROUND_SIZE = DpSize(92.dp, 32.dp)
|
||||
|
||||
private val CURRENT_DOT_SIZE = DpSize(16.dp, 8.dp)
|
||||
private val NORMAL_DOT_SIZE = DpSize(8.dp, 8.dp)
|
||||
private val HINT_DOT_SIZE = DpSize(6.dp, 6.dp)
|
||||
private val SMALL_DOT_SIZE = DpSize(4.dp, 4.dp)
|
||||
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
@Composable
|
||||
fun PagerIndicator(pagerState: PagerState, modifier: Modifier = Modifier) {
|
||||
val totalPages = pagerState.pageCount
|
||||
val currentIndex = pagerState.currentPage
|
||||
if (LocalRedesignEnabled.current) {
|
||||
PagerIndicatorV2(pagerState, modifier)
|
||||
} else {
|
||||
PagerIndicatorV1(pagerState, modifier)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PagerIndicatorV1(pagerState: PagerState, modifier: Modifier = Modifier) {
|
||||
val colors = PagerIndicatorColors(
|
||||
active = TangemTheme.colors.control.key,
|
||||
inactive = TangemTheme.colors.text.tertiary,
|
||||
overlay = TangemTheme.colors.overlay.secondary,
|
||||
)
|
||||
PagerIndicatorContent(
|
||||
pagerState = pagerState,
|
||||
colors = colors,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PagerIndicatorV2(pagerState: PagerState, modifier: Modifier = Modifier) {
|
||||
val colors = PagerIndicatorColors(
|
||||
active = TangemTheme.colors2.graphic.neutral.primary,
|
||||
inactive = TangemTheme.colors2.graphic.neutral.tertiary,
|
||||
overlay = TangemTheme.colors2.tabs.backgroundSecondary.copy(alpha = .1f),
|
||||
)
|
||||
PagerIndicatorContent(
|
||||
pagerState = pagerState,
|
||||
colors = colors,
|
||||
modifier = modifier,
|
||||
boxModifier = Modifier.hazeEffectTangem(style = HazeStyle(blurRadius = 22.dp, tint = null)),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun rememberPagerIndicatorAnimationState(pagerState: PagerState): PagerIndicatorAnimationState {
|
||||
val density = LocalDensity.current
|
||||
return remember(pagerState.pageCount, density) {
|
||||
PagerIndicatorAnimationState(pagerState.pageCount, pagerState.currentPage, density)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
private fun calculateDotAlpha(
|
||||
isSliding: Boolean,
|
||||
slideDirection: Int,
|
||||
index: Int,
|
||||
displayLower: Int,
|
||||
displayUpper: Int,
|
||||
fadeProgress: Float,
|
||||
): Float {
|
||||
return when {
|
||||
!isSliding -> 1f
|
||||
slideDirection > 0 && index == displayLower -> 1f - fadeProgress
|
||||
slideDirection > 0 && index == displayUpper - 1 -> fadeProgress
|
||||
slideDirection < 0 && index == displayUpper - 1 -> 1f - fadeProgress
|
||||
slideDirection < 0 && index == displayLower -> fadeProgress
|
||||
else -> 1f
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PagerIndicatorContent(
|
||||
pagerState: PagerState,
|
||||
colors: PagerIndicatorColors,
|
||||
modifier: Modifier = Modifier,
|
||||
boxModifier: Modifier = Modifier,
|
||||
) {
|
||||
val totalPages = pagerState.pageCount
|
||||
if (totalPages == 0) return
|
||||
|
||||
val indicatorColor = TangemTheme.colors.control.key
|
||||
val overlayColor = TangemTheme.colors.overlay.secondary
|
||||
val inactiveIndicatorColor = TangemTheme.colors.text.tertiary
|
||||
|
||||
val density = LocalDensity.current
|
||||
|
||||
val (targetLower, targetUpper) = getWindowBounds(totalPages, currentIndex)
|
||||
|
||||
var displayLower by remember { mutableIntStateOf(targetLower) }
|
||||
var displayUpper by remember { mutableIntStateOf(targetUpper) }
|
||||
var prevTargetLower by remember { mutableIntStateOf(targetLower) }
|
||||
|
||||
val slideOffset = remember { Animatable(0f) }
|
||||
var isSliding by remember { mutableStateOf(false) }
|
||||
var slideDirection by remember { mutableIntStateOf(0) }
|
||||
val fadeProgress = remember { Animatable(0f) }
|
||||
var fadeJob by remember { mutableStateOf<Job?>(null) }
|
||||
val animState = rememberPagerIndicatorAnimationState(pagerState)
|
||||
val (targetLower, targetUpper) = getWindowBounds(pagerState.pageCount, pagerState.currentPage)
|
||||
|
||||
LaunchedEffect(targetLower) {
|
||||
if (targetLower != prevTargetLower && totalPages > MAX_VISIBLE_DOTS) {
|
||||
fadeJob?.cancel()
|
||||
slideOffset.stop()
|
||||
fadeProgress.stop()
|
||||
|
||||
val dir = if (targetLower > prevTargetLower) 1 else -1
|
||||
val edgeDotSize = with(density) { (HINT_DOT_SIZE.width + SPACING).toPx() }
|
||||
val halfEdge = edgeDotSize / 2
|
||||
|
||||
isSliding = true
|
||||
slideDirection = dir
|
||||
fadeProgress.snapTo(0f)
|
||||
|
||||
if (dir > 0) {
|
||||
displayLower = prevTargetLower
|
||||
displayUpper = targetUpper
|
||||
slideOffset.snapTo(halfEdge)
|
||||
} else {
|
||||
displayLower = targetLower
|
||||
displayUpper = prevTargetLower + MAX_VISIBLE_DOTS
|
||||
slideOffset.snapTo(-halfEdge)
|
||||
}
|
||||
|
||||
prevTargetLower = targetLower
|
||||
|
||||
fadeJob = launch {
|
||||
fadeProgress.animateTo(1f, tween(ANIMATION_DURATION))
|
||||
}
|
||||
slideOffset.animateTo(
|
||||
if (dir > 0) -halfEdge else halfEdge,
|
||||
tween(ANIMATION_DURATION),
|
||||
)
|
||||
|
||||
displayLower = targetLower
|
||||
displayUpper = targetUpper
|
||||
slideOffset.snapTo(0f)
|
||||
isSliding = false
|
||||
slideDirection = 0
|
||||
}
|
||||
animState.onBoundsChange(this, targetLower, targetUpper)
|
||||
}
|
||||
val visibleIndices = (displayLower until displayUpper).toList()
|
||||
|
||||
val visibleIndices = (animState.displayLower until animState.displayUpper).toList()
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.width(BACKGROUND_SIZE.width)
|
||||
.height(BACKGROUND_SIZE.height)
|
||||
.background(
|
||||
color = overlayColor,
|
||||
color = colors.overlay,
|
||||
shape = CircleShape,
|
||||
)
|
||||
.clip(CircleShape),
|
||||
.clip(CircleShape)
|
||||
.then(boxModifier),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.offset {
|
||||
IntOffset(slideOffset.value.roundToInt(), 0)
|
||||
IntOffset(animState.slideOffset.value.roundToInt(), 0)
|
||||
},
|
||||
horizontalArrangement = Arrangement.spacedBy(SPACING),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
visibleIndices.forEach { index ->
|
||||
val dotAlpha = when {
|
||||
!isSliding -> 1f
|
||||
slideDirection > 0 && index == displayLower -> 1f - fadeProgress.value
|
||||
slideDirection > 0 && index == displayUpper - 1 -> fadeProgress.value
|
||||
slideDirection < 0 && index == displayUpper - 1 -> 1f - fadeProgress.value
|
||||
slideDirection < 0 && index == displayLower -> fadeProgress.value
|
||||
else -> 1f
|
||||
}
|
||||
val dotAlpha = calculateDotAlpha(
|
||||
isSliding = animState.isSliding,
|
||||
slideDirection = animState.slideDirection,
|
||||
index = index,
|
||||
displayLower = animState.displayLower,
|
||||
displayUpper = animState.displayUpper,
|
||||
fadeProgress = animState.fadeProgress.value,
|
||||
)
|
||||
|
||||
key(index) {
|
||||
Dot(
|
||||
index = index,
|
||||
currentIndex = currentIndex,
|
||||
currentIndex = pagerState.currentPage,
|
||||
totalPages = totalPages,
|
||||
activeColor = indicatorColor,
|
||||
inactiveColor = inactiveIndicatorColor,
|
||||
activeColor = colors.active,
|
||||
inactiveColor = colors.inactive,
|
||||
modifier = Modifier.graphicsLayer { alpha = dotAlpha },
|
||||
)
|
||||
}
|
||||
|
|
@ -156,19 +170,6 @@ fun PagerIndicator(pagerState: PagerState, modifier: Modifier = Modifier) {
|
|||
}
|
||||
}
|
||||
|
||||
private fun getWindowBounds(totalPages: Int, currentIndex: Int): Pair<Int, Int> {
|
||||
if (totalPages <= MAX_VISIBLE_DOTS) {
|
||||
return 0 to totalPages
|
||||
}
|
||||
val lowerBound = when {
|
||||
currentIndex <= 1 -> 0
|
||||
currentIndex >= totalPages - 2 -> totalPages - MAX_VISIBLE_DOTS
|
||||
else -> currentIndex - 2
|
||||
}
|
||||
val upperBound = min(lowerBound + MAX_VISIBLE_DOTS, totalPages)
|
||||
return lowerBound to upperBound
|
||||
}
|
||||
|
||||
private fun getDotSize(index: Int, currentIndex: Int, totalPages: Int): DpSize {
|
||||
if (index == currentIndex) {
|
||||
return CURRENT_DOT_SIZE
|
||||
|
|
@ -180,6 +181,46 @@ private fun getDotSize(index: Int, currentIndex: Int, totalPages: Int): DpSize {
|
|||
return params.calculateSize()
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Dot(
|
||||
index: Int,
|
||||
currentIndex: Int,
|
||||
totalPages: Int,
|
||||
activeColor: Color,
|
||||
inactiveColor: Color,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val isActive = index == currentIndex
|
||||
val size = getDotSize(index, currentIndex, totalPages)
|
||||
|
||||
val animSpec = tween<Dp>(ANIMATION_DURATION)
|
||||
val colorSpec = tween<Color>(ANIMATION_DURATION)
|
||||
|
||||
val animatedWidth by animateDpAsState(size.width, animSpec, label = "w$index")
|
||||
val animatedHeight by animateDpAsState(size.height, animSpec, label = "h$index")
|
||||
val animatedColor by animateColorAsState(
|
||||
targetValue = if (isActive) activeColor else inactiveColor,
|
||||
animationSpec = colorSpec,
|
||||
label = "c$index",
|
||||
)
|
||||
|
||||
val shape = RoundedCornerShape(animatedHeight / 2)
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.width(animatedWidth)
|
||||
.height(animatedHeight)
|
||||
.background(animatedColor, shape),
|
||||
)
|
||||
}
|
||||
|
||||
@Immutable
|
||||
private data class PagerIndicatorColors(
|
||||
val active: Color,
|
||||
val inactive: Color,
|
||||
val overlay: Color,
|
||||
)
|
||||
|
||||
private class DotSizeParams private constructor(
|
||||
val posInWindow: Int,
|
||||
val currentPosInWindow: Int,
|
||||
|
|
@ -248,43 +289,27 @@ private class DotSizeParams private constructor(
|
|||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun Dot(
|
||||
index: Int,
|
||||
currentIndex: Int,
|
||||
totalPages: Int,
|
||||
activeColor: Color,
|
||||
inactiveColor: Color,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val isActive = index == currentIndex
|
||||
val size = getDotSize(index, currentIndex, totalPages)
|
||||
|
||||
val animSpec = tween<Dp>(ANIMATION_DURATION)
|
||||
val colorSpec = tween<Color>(ANIMATION_DURATION)
|
||||
|
||||
val animatedWidth by animateDpAsState(size.width, animSpec, label = "w$index")
|
||||
val animatedHeight by animateDpAsState(size.height, animSpec, label = "h$index")
|
||||
val animatedColor by animateColorAsState(
|
||||
targetValue = if (isActive) activeColor else inactiveColor,
|
||||
animationSpec = colorSpec,
|
||||
label = "c$index",
|
||||
)
|
||||
|
||||
val shape = RoundedCornerShape(animatedHeight / 2)
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.width(animatedWidth)
|
||||
.height(animatedHeight)
|
||||
.background(animatedColor, shape),
|
||||
)
|
||||
private fun PagerIndicatorPreviewV1() {
|
||||
TangemThemePreview {
|
||||
Column(
|
||||
Modifier
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.padding(20.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
listOf(0, 1, 2, 3, 4).forEach { page ->
|
||||
PagerIndicator(rememberPagerState(page) { 5 })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun PagerIndicatorPreview() {
|
||||
TangemThemePreview {
|
||||
private fun PagerIndicatorPreviewV2() {
|
||||
TangemThemePreviewRedesign {
|
||||
Column(
|
||||
Modifier
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,95 @@
|
|||
package com.tangem.core.ui.components.pager
|
||||
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.unit.Density
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.min
|
||||
|
||||
@Stable
|
||||
internal class PagerIndicatorAnimationState(
|
||||
private val totalPages: Int,
|
||||
initialCurrentPage: Int,
|
||||
private val density: Density,
|
||||
) {
|
||||
var displayLower by mutableIntStateOf(0)
|
||||
private set
|
||||
var displayUpper by mutableIntStateOf(0)
|
||||
private set
|
||||
|
||||
val slideOffset = Animatable(0f)
|
||||
var isSliding by mutableStateOf(false)
|
||||
private set
|
||||
var slideDirection by mutableIntStateOf(0)
|
||||
private set
|
||||
val fadeProgress = Animatable(0f)
|
||||
private var fadeJob by mutableStateOf<Job?>(null)
|
||||
|
||||
private var prevTargetLower by mutableIntStateOf(0)
|
||||
|
||||
init {
|
||||
val (lower, upper) = getWindowBounds(totalPages, initialCurrentPage)
|
||||
displayLower = lower
|
||||
displayUpper = upper
|
||||
prevTargetLower = lower
|
||||
}
|
||||
|
||||
suspend fun onBoundsChange(scope: CoroutineScope, targetLower: Int, targetUpper: Int) {
|
||||
if (targetLower == prevTargetLower || totalPages <= MAX_VISIBLE_DOTS) {
|
||||
return
|
||||
}
|
||||
|
||||
fadeJob?.cancel()
|
||||
slideOffset.stop()
|
||||
fadeProgress.stop()
|
||||
|
||||
val dir = if (targetLower > prevTargetLower) 1 else -1
|
||||
val edgeDotSize = with(density) { (HINT_DOT_SIZE.width + SPACING).toPx() }
|
||||
val halfEdge = edgeDotSize / 2
|
||||
|
||||
isSliding = true
|
||||
slideDirection = dir
|
||||
fadeProgress.snapTo(0f)
|
||||
|
||||
if (dir > 0) {
|
||||
displayUpper = targetUpper
|
||||
slideOffset.snapTo(halfEdge)
|
||||
} else {
|
||||
displayLower = targetLower
|
||||
displayUpper = prevTargetLower + MAX_VISIBLE_DOTS
|
||||
slideOffset.snapTo(-halfEdge)
|
||||
}
|
||||
|
||||
prevTargetLower = targetLower
|
||||
|
||||
fadeJob = scope.launch {
|
||||
fadeProgress.animateTo(1f, tween(ANIMATION_DURATION))
|
||||
}
|
||||
slideOffset.animateTo(
|
||||
if (dir > 0) -halfEdge else halfEdge,
|
||||
tween(ANIMATION_DURATION),
|
||||
)
|
||||
|
||||
displayLower = targetLower
|
||||
displayUpper = targetUpper
|
||||
slideOffset.snapTo(0f)
|
||||
isSliding = false
|
||||
slideDirection = 0
|
||||
}
|
||||
}
|
||||
|
||||
internal fun getWindowBounds(totalPages: Int, currentIndex: Int): Pair<Int, Int> {
|
||||
if (totalPages <= MAX_VISIBLE_DOTS) {
|
||||
return 0 to totalPages
|
||||
}
|
||||
val lowerBound = when {
|
||||
currentIndex <= 1 -> 0
|
||||
currentIndex >= totalPages - 2 -> totalPages - MAX_VISIBLE_DOTS
|
||||
else -> currentIndex - 2
|
||||
}
|
||||
val upperBound = min(lowerBound + MAX_VISIBLE_DOTS, totalPages)
|
||||
return lowerBound to upperBound
|
||||
}
|
||||
|
|
@ -80,7 +80,7 @@ internal fun TangemButtonInternal(
|
|||
visible = iconRes != null && iconPosition == TangemButtonIconPosition.Start,
|
||||
modifier = Modifier.size(size = size.toContentSize()),
|
||||
) {
|
||||
val wrappedIconRes = remember(this) { requireNotNull(iconRes) }
|
||||
val wrappedIconRes = remember(this, iconRes) { requireNotNull(iconRes) }
|
||||
TangemButtonIcon(iconRes = wrappedIconRes, state = state, iconColor = contentColor, size = size)
|
||||
}
|
||||
|
||||
|
|
|
|||
9
core/ui/src/main/res/drawable/ic_like_20.xml
Normal file
9
core/ui/src/main/res/drawable/ic_like_20.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="20dp"
|
||||
android:height="20dp"
|
||||
android:viewportWidth="20"
|
||||
android:viewportHeight="20">
|
||||
<path
|
||||
android:pathData="M13.013,2.167C16.34,2.167 18.666,4.886 18.666,8.23C18.666,10.929 17.218,13.156 15.803,14.715C14.361,16.302 12.716,17.457 11.829,18.033L11.785,18.062L11.74,18.088C11.685,18.12 11.46,18.256 11.252,18.357C11.082,18.441 10.608,18.667 10.019,18.667C9.455,18.667 8.985,18.458 8.816,18.381C8.583,18.275 8.371,18.153 8.259,18.088L8.214,18.062L8.171,18.033C7.284,17.457 5.639,16.303 4.196,14.715C2.781,13.156 1.333,10.929 1.333,8.23C1.333,4.886 3.659,2.167 6.986,2.167C8.182,2.167 9.19,2.546 10,3.118C10.81,2.545 11.819,2.167 13.013,2.167Z"
|
||||
android:fillColor="#ffffff"/>
|
||||
</vector>
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
package com.tangem.features.feed.model.converter
|
||||
|
||||
import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM
|
||||
import com.tangem.core.ui.components.label.entity.LabelLeadingContentUM
|
||||
import com.tangem.core.ui.components.label.entity.LabelUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.data.common.currency.getTokenIconUrlFromDefaultHost
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.news.ShortArticle
|
||||
import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM
|
||||
import com.tangem.features.feed.ui.utils.mapFormattedDate
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
|
@ -16,7 +16,7 @@ import kotlinx.collections.immutable.toPersistentList
|
|||
import kotlinx.collections.immutable.toPersistentSet
|
||||
|
||||
internal class ShortArticleToArticleConfigUMConverter(
|
||||
private val isTrending: Provider<Boolean>,
|
||||
private val isTrending: Provider<Boolean>?,
|
||||
) : Converter<List<ShortArticle>, ImmutableList<ArticleConfigUM>> {
|
||||
|
||||
override fun convert(value: List<ShortArticle>): ImmutableList<ArticleConfigUM> {
|
||||
|
|
@ -25,7 +25,7 @@ internal class ShortArticleToArticleConfigUMConverter(
|
|||
id = shortArticle.id,
|
||||
title = shortArticle.title,
|
||||
score = shortArticle.score,
|
||||
isTrending = isTrending(),
|
||||
isTrending = isTrending?.invoke() ?: shortArticle.isTrending,
|
||||
tags = buildArticleTags(shortArticle),
|
||||
createdAt = mapFormattedDate(shortArticle.createdAt),
|
||||
isViewed = shortArticle.viewed,
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ internal class NewsDetailsConverter(
|
|||
newsUrl = value.newsUrl,
|
||||
relatedTokens = value.relatedTokens.toImmutableList(),
|
||||
isLiked = value.isLiked,
|
||||
isTrending = value.isTrending,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
package com.tangem.features.feed.model.news.list.statemanager
|
||||
|
||||
import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM
|
||||
import com.tangem.domain.models.news.ShortArticle
|
||||
import com.tangem.domain.news.model.NewsListBatchingContext
|
||||
import com.tangem.domain.news.model.NewsListConfig
|
||||
import com.tangem.domain.news.usecase.GetNewsListBatchFlowUseCase
|
||||
import com.tangem.features.feed.model.converter.ShortArticleToArticleConfigUMConverter
|
||||
import com.tangem.features.feed.model.converter.distinctBatchesContent
|
||||
import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM
|
||||
import com.tangem.pagination.Batch
|
||||
import com.tangem.pagination.BatchAction
|
||||
import com.tangem.pagination.PaginationStatus
|
||||
|
|
@ -30,7 +30,7 @@ internal open class NewsListBatchFlowManager(
|
|||
) {
|
||||
private val actionsFlow = MutableSharedFlow<BatchAction<Int, NewsListConfig, Nothing>>()
|
||||
private val converter by lazy {
|
||||
ShortArticleToArticleConfigUMConverter(isTrending = Provider { false })
|
||||
ShortArticleToArticleConfigUMConverter(null)
|
||||
}
|
||||
|
||||
private val batchFlow = getNewsListBatchFlowUseCase(
|
||||
|
|
|
|||
|
|
@ -10,12 +10,8 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.onFirstVisible
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.HorizontalFadeWithBlur
|
||||
import com.tangem.core.ui.components.block.TangemBlockCardColors
|
||||
import com.tangem.core.ui.components.haze.hazeSourceTangem
|
||||
import com.tangem.core.ui.extensions.conditionalCompose
|
||||
import com.tangem.core.ui.res.LocalMainBottomSheetColor
|
||||
import com.tangem.core.ui.res.LocalRedesignEnabled
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.feed.ui.feed.components.articles.ArticleCard
|
||||
import com.tangem.features.feed.ui.feed.components.articles.ShowMoreArticlesCard
|
||||
|
|
@ -25,72 +21,50 @@ import com.tangem.features.feed.ui.feed.state.NewsSliderConfig
|
|||
@Composable
|
||||
internal fun NewsSlider(newsSliderConfig: NewsSliderConfig) {
|
||||
val background = LocalMainBottomSheetColor.current.value
|
||||
val isRedesignEnabled = LocalRedesignEnabled.current
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
LazyRow(
|
||||
modifier = Modifier.background(color = background),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
state = rememberLazyListState(),
|
||||
) {
|
||||
LazyRow(
|
||||
modifier = Modifier
|
||||
.conditionalCompose(
|
||||
condition = isRedesignEnabled,
|
||||
modifier = {
|
||||
hazeSourceTangem(zIndex = -1f)
|
||||
},
|
||||
itemsIndexed(
|
||||
items = newsSliderConfig.content,
|
||||
key = { index, _ -> index },
|
||||
contentType = { _, _ -> "article" },
|
||||
) { index, article ->
|
||||
val articleModifier = if (index == FOURTH_ITEM_INDEX) {
|
||||
Modifier.onFirstVisible(
|
||||
minFractionVisible = 0.5f,
|
||||
callback = newsSliderConfig.callbacks.onSliderScroll,
|
||||
)
|
||||
.background(color = background),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
state = rememberLazyListState(),
|
||||
) {
|
||||
itemsIndexed(
|
||||
items = newsSliderConfig.content,
|
||||
key = { index, _ -> index },
|
||||
contentType = { _, _ -> "article" },
|
||||
) { index, article ->
|
||||
val articleModifier = if (index == FOURTH_ITEM_INDEX) {
|
||||
Modifier.onFirstVisible(
|
||||
minFractionVisible = 0.5f,
|
||||
callback = newsSliderConfig.callbacks.onSliderScroll,
|
||||
)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
ArticleCard(
|
||||
articleConfigUM = article,
|
||||
onArticleClick = { newsSliderConfig.callbacks.onArticleClick(article.id) },
|
||||
modifier = articleModifier
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
ArticleCard(
|
||||
articleConfigUM = article,
|
||||
onArticleClick = { newsSliderConfig.callbacks.onArticleClick(article.id) },
|
||||
modifier = articleModifier
|
||||
.width(228.dp)
|
||||
.heightIn(min = 172.dp)
|
||||
.fillMaxHeight(),
|
||||
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
|
||||
)
|
||||
}
|
||||
|
||||
if (newsSliderConfig.shouldShowSeeAllNewsItem) {
|
||||
item(contentType = "show_more") {
|
||||
ShowMoreArticlesCard(
|
||||
modifier = Modifier
|
||||
.width(228.dp)
|
||||
.heightIn(min = 172.dp)
|
||||
.fillMaxHeight(),
|
||||
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
|
||||
.onFirstVisible(
|
||||
minFractionVisible = 0.5f,
|
||||
callback = newsSliderConfig.callbacks.onSliderEndReached,
|
||||
),
|
||||
onClick = newsSliderConfig.callbacks.onOpenAllNews,
|
||||
)
|
||||
}
|
||||
|
||||
if (newsSliderConfig.shouldShowSeeAllNewsItem) {
|
||||
item(contentType = "show_more") {
|
||||
ShowMoreArticlesCard(
|
||||
modifier = Modifier
|
||||
.width(228.dp)
|
||||
.heightIn(min = 172.dp)
|
||||
.onFirstVisible(
|
||||
minFractionVisible = 0.5f,
|
||||
callback = newsSliderConfig.callbacks.onSliderEndReached,
|
||||
),
|
||||
onClick = newsSliderConfig.callbacks.onOpenAllNews,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isRedesignEnabled) {
|
||||
HorizontalFadeWithBlur(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.heightIn(min = 172.dp)
|
||||
.fillMaxHeight()
|
||||
.width(100.dp),
|
||||
backgroundColor = background,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +1,69 @@
|
|||
package com.tangem.features.feed.ui.feed.components.articles
|
||||
|
||||
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.foundation.layout.*
|
||||
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.graphics.vector.ImageVector
|
||||
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.SpacerW
|
||||
import com.tangem.core.ui.components.label.Label
|
||||
import com.tangem.core.ui.components.label.entity.LabelLeadingContentUM
|
||||
import com.tangem.core.ui.components.label.entity.LabelUM
|
||||
import com.tangem.core.ui.ds.badge.TangemBadge
|
||||
import com.tangem.core.ui.ds.badge.TangemBadgeColor
|
||||
import com.tangem.core.ui.ds.badge.TangemBadgeIconPosition
|
||||
import com.tangem.core.ui.ds.badge.TangemBadgeShape
|
||||
import com.tangem.core.ui.ds.badge.TangemBadgeSize
|
||||
import com.tangem.core.ui.ds.badge.TangemBadgeType
|
||||
import com.tangem.core.ui.ds.image.TangemIconUM
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.LocalRedesignEnabled
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun ArticleHeader(
|
||||
isTrending: Boolean,
|
||||
title: String,
|
||||
createdAt: String,
|
||||
score: Float,
|
||||
tags: ImmutableList<LabelUM>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (LocalRedesignEnabled.current) {
|
||||
ArticleHeaderV2(
|
||||
isTrending = isTrending,
|
||||
title = title,
|
||||
createdAt = createdAt,
|
||||
score = score,
|
||||
tags = tags,
|
||||
modifier = modifier,
|
||||
)
|
||||
} else {
|
||||
ArticleHeaderV1(
|
||||
title = title,
|
||||
createdAt = createdAt,
|
||||
score = score,
|
||||
tags = tags,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
private fun ArticleHeaderV1(
|
||||
title: String,
|
||||
createdAt: String,
|
||||
score: Float,
|
||||
|
|
@ -52,4 +98,161 @@ fun ArticleHeader(
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
private fun ArticleHeaderV2(
|
||||
isTrending: Boolean,
|
||||
title: String,
|
||||
createdAt: String,
|
||||
score: Float,
|
||||
tags: ImmutableList<LabelUM>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.heightIn(min = 66.dp)
|
||||
.padding(top = 16.dp),
|
||||
verticalAlignment = Alignment.Bottom,
|
||||
) {
|
||||
DateBlock(
|
||||
modifier = Modifier.weight(1f),
|
||||
createdAt = createdAt,
|
||||
)
|
||||
SpacerW(30.dp)
|
||||
VerticalDivider(
|
||||
modifier = Modifier
|
||||
.height(46.dp)
|
||||
.padding(bottom = 4.dp),
|
||||
color = TangemTheme.colors2.border.neutral.primary,
|
||||
)
|
||||
SpacerW(30.dp)
|
||||
ScoreBlock(
|
||||
modifier = Modifier.weight(1f),
|
||||
score = score,
|
||||
isTrending = isTrending,
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
modifier = Modifier.padding(vertical = 36.dp),
|
||||
text = title,
|
||||
style = TangemTheme.typography2.headingBold34,
|
||||
color = TangemTheme.colors2.text.neutral.primary,
|
||||
)
|
||||
|
||||
if (tags.isNotEmpty()) {
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
tags.forEach { tag ->
|
||||
TangemBadge(
|
||||
text = tag.text,
|
||||
tangemIconUM = when (val content = tag.leadingContent) {
|
||||
LabelLeadingContentUM.None -> null
|
||||
is LabelLeadingContentUM.Token -> TangemIconUM.Url(content.iconUrl)
|
||||
},
|
||||
shape = TangemBadgeShape.Rounded,
|
||||
size = TangemBadgeSize.X9,
|
||||
type = TangemBadgeType.Tinted,
|
||||
color = TangemBadgeColor.Gray,
|
||||
iconPosition = when (tag.leadingContent) {
|
||||
LabelLeadingContentUM.None -> TangemBadgeIconPosition.None
|
||||
is LabelLeadingContentUM.Token -> TangemBadgeIconPosition.Start
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ScoreBlock(score: Float, isTrending: Boolean, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(20.dp),
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_wrapped_circle_star_16),
|
||||
tint = if (isTrending) {
|
||||
TangemTheme.colors2.fill.status.attention
|
||||
} else {
|
||||
TangemTheme.colors2.graphic.neutral.primary
|
||||
},
|
||||
contentDescription = null,
|
||||
)
|
||||
Text(
|
||||
text = score.toString(),
|
||||
style = TangemTheme.typography2.bodyRegular16,
|
||||
color = if (isTrending) {
|
||||
TangemTheme.colors2.text.status.attention
|
||||
} else {
|
||||
TangemTheme.colors2.text.neutral.primary
|
||||
},
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.news_trending_score),
|
||||
style = TangemTheme.typography2.captionSemibold13,
|
||||
color = TangemTheme.colors2.text.neutral.tertiary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DateBlock(createdAt: String, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(20.dp),
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_calendar_20),
|
||||
tint = TangemTheme.colors2.fill.neutral.primary,
|
||||
contentDescription = null,
|
||||
)
|
||||
Text(
|
||||
text = createdAt,
|
||||
style = TangemTheme.typography2.captionSemibold13,
|
||||
color = TangemTheme.colors2.text.neutral.tertiary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Composable
|
||||
private fun ArticleHeaderPreviewV1() {
|
||||
TangemThemePreview {
|
||||
ArticleHeader(
|
||||
title = "Something going good!",
|
||||
createdAt = "1 hour ago",
|
||||
score = 5.5f,
|
||||
tags = persistentListOf(),
|
||||
isTrending = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Composable
|
||||
private fun ArticleHeaderPreviewV2() {
|
||||
TangemThemePreviewRedesign {
|
||||
ArticleHeader(
|
||||
title = "Something going good!",
|
||||
createdAt = "1 hour ago",
|
||||
score = 5.5f,
|
||||
tags = persistentListOf(),
|
||||
isTrending = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -3,53 +3,25 @@ package com.tangem.features.feed.ui.news.details
|
|||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
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.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
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.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.SubcomposeAsyncImage
|
||||
import coil.request.CachePolicy
|
||||
import coil.request.ImageRequest
|
||||
import com.tangem.features.feed.ui.feed.components.articles.ArticleHeader
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.*
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonSize
|
||||
import com.tangem.core.ui.components.UnableToLoadData
|
||||
import com.tangem.core.ui.components.haze.hazeSourceTangem
|
||||
import com.tangem.core.ui.components.pager.PagerIndicator
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.LocalMainBottomSheetColor
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.extensions.conditionalCompose
|
||||
import com.tangem.core.ui.res.*
|
||||
import com.tangem.features.feed.ui.news.details.components.ArticleDetail
|
||||
import com.tangem.features.feed.ui.news.details.components.NewsDetailsPlaceholder
|
||||
import com.tangem.features.feed.ui.news.details.components.RelatedTokensBlock
|
||||
import com.tangem.features.feed.ui.news.details.state.*
|
||||
import com.tangem.features.feed.ui.news.details.state.ArticlesStateUM
|
||||
import com.tangem.features.feed.ui.news.details.state.MockArticlesFactory
|
||||
import com.tangem.features.feed.ui.news.details.state.NewsDetailsUM
|
||||
|
||||
@Composable
|
||||
internal fun NewsDetailsContent(state: NewsDetailsUM, modifier: Modifier = Modifier) {
|
||||
|
|
@ -86,6 +58,7 @@ internal fun NewsDetailsContent(state: NewsDetailsUM, modifier: Modifier = Modif
|
|||
|
||||
@Composable
|
||||
private fun Content(state: NewsDetailsUM, background: Color) {
|
||||
val isRedesignEnabled = LocalRedesignEnabled.current
|
||||
val pagerState = rememberPagerState(
|
||||
initialPage = state.selectedArticleIndex,
|
||||
pageCount = { state.articles.size },
|
||||
|
|
@ -109,6 +82,12 @@ private fun Content(state: NewsDetailsUM, background: Color) {
|
|||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.conditionalCompose(
|
||||
condition = isRedesignEnabled,
|
||||
modifier = {
|
||||
hazeSourceTangem(zIndex = 1f)
|
||||
},
|
||||
)
|
||||
.background(background),
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
|
|
@ -138,242 +117,6 @@ private fun Content(state: NewsDetailsUM, background: Color) {
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
private fun ArticleDetail(
|
||||
article: ArticleUM,
|
||||
onLikeClick: () -> Unit,
|
||||
relatedTokensUM: RelatedTokensUM,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val hapticFeedback = LocalHapticFeedback.current
|
||||
val density = LocalDensity.current
|
||||
val background = LocalMainBottomSheetColor.current.value
|
||||
val pagerHeight = 32.dp
|
||||
val contentPadding = pagerHeight + 56.dp + with(density) {
|
||||
WindowInsets.navigationBars.getBottom(this).div(this.density)
|
||||
}.dp
|
||||
|
||||
Box(modifier = modifier) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(bottom = contentPadding),
|
||||
) {
|
||||
item("content") {
|
||||
ArticleHeader(
|
||||
title = article.title,
|
||||
createdAt = article.createdAt.resolveReference(),
|
||||
score = article.score,
|
||||
tags = article.tags,
|
||||
modifier = Modifier
|
||||
.padding(top = 16.dp)
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
|
||||
if (article.shortContent.isNotEmpty()) {
|
||||
QuickRecap(
|
||||
content = article.shortContent,
|
||||
modifier = Modifier
|
||||
.padding(top = 32.dp)
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = article.content,
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
modifier = Modifier
|
||||
.padding(top = 16.dp)
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
|
||||
SpacerH(24.dp)
|
||||
|
||||
SecondaryButtonIconStart(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
iconResId = if (article.isLiked) {
|
||||
R.drawable.ic_heart_filled_20
|
||||
} else {
|
||||
R.drawable.ic_heart_20
|
||||
},
|
||||
iconTint = Color.Unspecified,
|
||||
text = stringResourceSafe(R.string.news_like),
|
||||
size = TangemButtonSize.RoundedAction,
|
||||
onClick = {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
onLikeClick()
|
||||
},
|
||||
)
|
||||
|
||||
RelatedTokensBlock(
|
||||
relatedTokensUM = relatedTokensUM,
|
||||
onItemClick = when (relatedTokensUM) {
|
||||
is RelatedTokensUM.Content -> relatedTokensUM.onTokenClick
|
||||
else -> null
|
||||
},
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
)
|
||||
|
||||
if (article.relatedArticles.isNotEmpty()) {
|
||||
SpacerH(24.dp)
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.news_sources),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
Text(
|
||||
text = "${article.relatedArticles.size}",
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (article.relatedArticles.isNotEmpty()) {
|
||||
item("relatedArticles") {
|
||||
LazyRow(
|
||||
modifier = Modifier.padding(vertical = 12.dp),
|
||||
state = rememberLazyListState(),
|
||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
items(
|
||||
items = article.relatedArticles,
|
||||
key = RelatedArticleUM::id,
|
||||
) { article ->
|
||||
RelatedNewsItem(
|
||||
relatedArticle = article,
|
||||
modifier = Modifier.fillParentMaxHeight(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
BottomFade(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter),
|
||||
backgroundColor = background,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@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 = stringResourceSafe(R.string.news_quick_recap),
|
||||
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 RelatedNewsItem(relatedArticle: RelatedArticleUM, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.sizeIn(maxWidth = 256.dp, minHeight = 132.dp)
|
||||
.background(color = TangemTheme.colors.background.action, shape = RoundedCornerShape(12.dp))
|
||||
.clickable(onClick = relatedArticle.onClick)
|
||||
.padding(12.dp),
|
||||
) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
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(16.dp),
|
||||
)
|
||||
SpacerW(4.dp)
|
||||
Text(
|
||||
text = relatedArticle.media.name,
|
||||
style = TangemTheme.typography.caption1,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
if (relatedArticle.title.isNotEmpty()) {
|
||||
SpacerH(4.dp)
|
||||
Text(
|
||||
text = relatedArticle.title,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
maxLines = 3,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (relatedArticle.imageUrl != null) {
|
||||
SubcomposeAsyncImage(
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.clip(RoundedCornerShape(4.dp)),
|
||||
contentScale = ContentScale.Crop,
|
||||
model = ImageRequest.Builder(context = LocalContext.current)
|
||||
.data(relatedArticle.imageUrl)
|
||||
.crossfade(enable = false)
|
||||
.allowHardware(true)
|
||||
.memoryCachePolicy(CachePolicy.DISABLED)
|
||||
.build(),
|
||||
loading = {
|
||||
RectangleShimmer(
|
||||
modifier = Modifier.size(40.dp),
|
||||
radius = 4.dp,
|
||||
)
|
||||
},
|
||||
error = {},
|
||||
contentDescription = relatedArticle.media.name,
|
||||
)
|
||||
}
|
||||
}
|
||||
SpacerHMax()
|
||||
Text(
|
||||
text = relatedArticle.publishedAt.resolveReference(),
|
||||
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
|
||||
|
|
@ -396,4 +139,28 @@ private fun PreviewNewsDetailsContent() {
|
|||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun PreviewNewsDetailsContentV2() {
|
||||
TangemThemePreviewRedesign {
|
||||
val background = TangemTheme.colors.background.tertiary
|
||||
CompositionLocalProvider(
|
||||
LocalMainBottomSheetColor provides remember { mutableStateOf(background) },
|
||||
) {
|
||||
NewsDetailsContent(
|
||||
state = NewsDetailsUM(
|
||||
articlesStateUM = ArticlesStateUM.Content,
|
||||
articles = MockArticlesFactory.createMockArticles(),
|
||||
selectedArticleIndex = 0,
|
||||
onShareClick = {},
|
||||
onLikeClick = {},
|
||||
onBackClick = {},
|
||||
onArticleIndexChanged = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,324 @@
|
|||
package com.tangem.features.feed.ui.news.details.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
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.Color
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.BottomFade
|
||||
import com.tangem.core.ui.components.BottomFadeWithBlur
|
||||
import com.tangem.core.ui.components.SecondaryButtonIconStart
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonSize
|
||||
import com.tangem.core.ui.components.haze.hazeSourceTangem
|
||||
import com.tangem.core.ui.ds.button.SecondaryTangemButton
|
||||
import com.tangem.core.ui.ds.button.TangemButtonShape
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.LocalMainBottomSheetColor
|
||||
import com.tangem.core.ui.res.LocalRedesignEnabled
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.feed.ui.feed.components.articles.ArticleHeader
|
||||
import com.tangem.features.feed.ui.news.details.state.ArticleUM
|
||||
import com.tangem.features.feed.ui.news.details.state.RelatedArticleUM
|
||||
import com.tangem.features.feed.ui.news.details.state.RelatedTokensUM
|
||||
|
||||
@Composable
|
||||
internal fun ArticleDetail(
|
||||
article: ArticleUM,
|
||||
onLikeClick: () -> Unit,
|
||||
relatedTokensUM: RelatedTokensUM,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (LocalRedesignEnabled.current) {
|
||||
ArticleDetailV2(
|
||||
article = article,
|
||||
onLikeClick = onLikeClick,
|
||||
relatedTokensUM = relatedTokensUM,
|
||||
modifier = modifier,
|
||||
)
|
||||
} else {
|
||||
ArticleDetailV1(
|
||||
article = article,
|
||||
onLikeClick = onLikeClick,
|
||||
relatedTokensUM = relatedTokensUM,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
private fun ArticleDetailV1(
|
||||
article: ArticleUM,
|
||||
onLikeClick: () -> Unit,
|
||||
relatedTokensUM: RelatedTokensUM,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val hapticFeedback = LocalHapticFeedback.current
|
||||
val density = LocalDensity.current
|
||||
val background = LocalMainBottomSheetColor.current.value
|
||||
val pagerHeight = 32.dp
|
||||
val contentPadding = pagerHeight + 56.dp + with(density) {
|
||||
WindowInsets.navigationBars.getBottom(this).div(this.density)
|
||||
}.dp
|
||||
|
||||
Box(modifier = modifier) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(bottom = contentPadding),
|
||||
) {
|
||||
item("content") {
|
||||
ArticleHeader(
|
||||
title = article.title,
|
||||
createdAt = article.createdAt.resolveReference(),
|
||||
score = article.score,
|
||||
tags = article.tags,
|
||||
isTrending = article.isTrending,
|
||||
modifier = Modifier
|
||||
.padding(top = 16.dp)
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
|
||||
if (article.shortContent.isNotEmpty()) {
|
||||
QuickRecap(
|
||||
content = article.shortContent,
|
||||
modifier = Modifier
|
||||
.padding(top = 32.dp)
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = article.content,
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
modifier = Modifier
|
||||
.padding(top = 16.dp)
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
|
||||
SpacerH(24.dp)
|
||||
|
||||
SecondaryButtonIconStart(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
iconResId = if (article.isLiked) {
|
||||
R.drawable.ic_heart_filled_20
|
||||
} else {
|
||||
R.drawable.ic_heart_20
|
||||
},
|
||||
iconTint = Color.Unspecified,
|
||||
text = stringResourceSafe(R.string.news_like),
|
||||
size = TangemButtonSize.RoundedAction,
|
||||
onClick = {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
onLikeClick()
|
||||
},
|
||||
)
|
||||
|
||||
RelatedTokensBlock(
|
||||
relatedTokensUM = relatedTokensUM,
|
||||
onItemClick = when (relatedTokensUM) {
|
||||
is RelatedTokensUM.Content -> relatedTokensUM.onTokenClick
|
||||
else -> null
|
||||
},
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
)
|
||||
|
||||
if (article.relatedArticles.isNotEmpty()) {
|
||||
SpacerH(24.dp)
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.news_sources),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
Text(
|
||||
text = "${article.relatedArticles.size}",
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (article.relatedArticles.isNotEmpty()) {
|
||||
item("relatedArticles") {
|
||||
LazyRow(
|
||||
modifier = Modifier.padding(vertical = 12.dp),
|
||||
state = rememberLazyListState(),
|
||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
items(
|
||||
items = article.relatedArticles,
|
||||
key = RelatedArticleUM::id,
|
||||
) { article ->
|
||||
RelatedNewsItem(
|
||||
relatedArticle = article,
|
||||
modifier = Modifier.fillParentMaxHeight(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
BottomFade(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter),
|
||||
backgroundColor = background,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
internal fun ArticleDetailV2(
|
||||
article: ArticleUM,
|
||||
onLikeClick: () -> Unit,
|
||||
relatedTokensUM: RelatedTokensUM,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val hapticFeedback = LocalHapticFeedback.current
|
||||
val density = LocalDensity.current
|
||||
val background = LocalMainBottomSheetColor.current.value
|
||||
val pagerHeight = 32.dp
|
||||
val contentPadding = pagerHeight + 56.dp + with(density) {
|
||||
WindowInsets.navigationBars.getBottom(this).div(this.density)
|
||||
}.dp
|
||||
|
||||
Box(modifier = modifier) {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.hazeSourceTangem(zIndex = 0f)
|
||||
.background(background),
|
||||
contentPadding = PaddingValues(bottom = contentPadding),
|
||||
) {
|
||||
item("content") {
|
||||
ArticleHeader(
|
||||
title = article.title,
|
||||
createdAt = article.createdAt.resolveReference(),
|
||||
score = article.score,
|
||||
tags = article.tags,
|
||||
isTrending = article.isTrending,
|
||||
modifier = Modifier
|
||||
.padding(top = 16.dp)
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
|
||||
if (article.shortContent.isNotEmpty()) {
|
||||
QuickRecap(
|
||||
content = article.shortContent,
|
||||
modifier = Modifier
|
||||
.padding(top = 32.dp)
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = article.content,
|
||||
style = TangemTheme.typography2.bodyRegular16,
|
||||
color = TangemTheme.colors2.text.neutral.primary,
|
||||
modifier = Modifier
|
||||
.padding(top = 12.dp)
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
|
||||
SpacerH(24.dp)
|
||||
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(horizontal = 24.dp),
|
||||
color = TangemTheme.colors2.border.neutral.primary,
|
||||
)
|
||||
|
||||
SpacerH(20.dp)
|
||||
|
||||
SecondaryTangemButton(
|
||||
modifier = Modifier.padding(horizontal = 24.dp),
|
||||
text = resourceReference(R.string.news_like),
|
||||
size = com.tangem.core.ui.ds.button.TangemButtonSize.X9,
|
||||
iconRes = if (article.isLiked) {
|
||||
R.drawable.ic_like_20
|
||||
} else {
|
||||
R.drawable.ic_heart_20
|
||||
},
|
||||
onClick = {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
onLikeClick()
|
||||
},
|
||||
shape = TangemButtonShape.Rounded,
|
||||
)
|
||||
|
||||
RelatedTokensBlock(
|
||||
relatedTokensUM = relatedTokensUM,
|
||||
onItemClick = when (relatedTokensUM) {
|
||||
is RelatedTokensUM.Content -> relatedTokensUM.onTokenClick
|
||||
else -> null
|
||||
},
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
)
|
||||
|
||||
if (article.relatedArticles.isNotEmpty()) {
|
||||
SpacerH(24.dp)
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.news_sources),
|
||||
style = TangemTheme.typography2.headingSemibold20,
|
||||
color = TangemTheme.colors2.text.neutral.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (article.relatedArticles.isNotEmpty()) {
|
||||
item("relatedArticles") {
|
||||
LazyRow(
|
||||
modifier = Modifier
|
||||
.padding(vertical = 12.dp),
|
||||
state = rememberLazyListState(),
|
||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
items(
|
||||
items = article.relatedArticles,
|
||||
key = RelatedArticleUM::id,
|
||||
) { article ->
|
||||
RelatedNewsItem(
|
||||
relatedArticle = article,
|
||||
modifier = Modifier.fillParentMaxHeight(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BottomFadeWithBlur(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.height(80.dp)
|
||||
.fillMaxWidth(),
|
||||
backgroundColor = background,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ import androidx.compose.ui.text.SpanStyle
|
|||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.text.withStyle
|
||||
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
|
||||
|
|
@ -24,6 +25,7 @@ import com.tangem.core.ui.components.SpacerW
|
|||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.LocalRedesignEnabled
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||
|
||||
@Composable
|
||||
internal fun QuickRecap(content: String, modifier: Modifier = Modifier) {
|
||||
|
|
@ -105,20 +107,36 @@ private fun QuickRecapV2(content: String, modifier: Modifier = Modifier) {
|
|||
|
||||
Box {
|
||||
VerticalDivider(
|
||||
modifier = Modifier.fillMaxHeight(),
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.padding(start = 10.dp),
|
||||
thickness = 2.dp,
|
||||
color = Color(QUICK_RECAP_DIVIDER_COLOR),
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
modifier = Modifier.padding(start = 20.dp),
|
||||
text = content,
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography2.bodyRegular16,
|
||||
color = TangemTheme.colors2.text.neutral.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Composable
|
||||
private fun QuickRecapPreview() {
|
||||
TangemThemePreviewRedesign {
|
||||
QuickRecapV2(
|
||||
content = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut" +
|
||||
" labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris " +
|
||||
"nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit " +
|
||||
"esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt " +
|
||||
"in culpa qui officia deserunt mollit anim id est laborum.",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private const val QUICK_RECAP_DIVIDER_COLOR = 0xFFA99FFF
|
||||
private const val LINEAR_GRADIENT_FIRST_PART = 0xFFA3A0FF
|
||||
private const val LINEAR_GRADIENT_SECOND_PART = 0xFFF79DFF
|
||||
|
|
|
|||
|
|
@ -0,0 +1,188 @@
|
|||
package com.tangem.features.feed.ui.news.details.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.SubcomposeAsyncImage
|
||||
import coil.request.CachePolicy
|
||||
import coil.request.ImageRequest
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.SpacerHMax
|
||||
import com.tangem.core.ui.components.SpacerW
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.LocalRedesignEnabled
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.feed.ui.news.details.state.RelatedArticleUM
|
||||
|
||||
@Composable
|
||||
internal fun RelatedNewsItem(relatedArticle: RelatedArticleUM, modifier: Modifier = Modifier) {
|
||||
if (LocalRedesignEnabled.current) {
|
||||
RelatedNewsItemV2(relatedArticle, modifier)
|
||||
} else {
|
||||
RelatedNewsItemV1(relatedArticle, modifier)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RelatedNewsItemV1(relatedArticle: RelatedArticleUM, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.sizeIn(maxWidth = 256.dp, minHeight = 132.dp)
|
||||
.background(color = TangemTheme.colors.background.action, shape = RoundedCornerShape(12.dp))
|
||||
.clickable(onClick = relatedArticle.onClick)
|
||||
.padding(12.dp),
|
||||
) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
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(16.dp),
|
||||
)
|
||||
SpacerW(4.dp)
|
||||
Text(
|
||||
text = relatedArticle.media.name,
|
||||
style = TangemTheme.typography.caption1,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
if (relatedArticle.title.isNotEmpty()) {
|
||||
SpacerH(4.dp)
|
||||
Text(
|
||||
text = relatedArticle.title,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
maxLines = 3,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (relatedArticle.imageUrl != null) {
|
||||
SubcomposeAsyncImage(
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.clip(RoundedCornerShape(4.dp)),
|
||||
contentScale = ContentScale.Crop,
|
||||
model = ImageRequest.Builder(context = LocalContext.current)
|
||||
.data(relatedArticle.imageUrl)
|
||||
.crossfade(enable = false)
|
||||
.allowHardware(true)
|
||||
.memoryCachePolicy(CachePolicy.DISABLED)
|
||||
.build(),
|
||||
loading = {
|
||||
RectangleShimmer(
|
||||
modifier = Modifier.size(40.dp),
|
||||
radius = 4.dp,
|
||||
)
|
||||
},
|
||||
error = {},
|
||||
contentDescription = relatedArticle.media.name,
|
||||
)
|
||||
}
|
||||
}
|
||||
SpacerHMax()
|
||||
Text(
|
||||
text = relatedArticle.publishedAt.resolveReference(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
private fun RelatedNewsItemV2(relatedArticle: RelatedArticleUM, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.sizeIn(maxWidth = 228.dp, minHeight = 160.dp)
|
||||
.background(
|
||||
color = TangemTheme.colors2.surface.level3,
|
||||
shape = RoundedCornerShape(TangemTheme.dimens2.x5),
|
||||
)
|
||||
.clickable(onClick = relatedArticle.onClick)
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = TangemTheme.colors2.border.neutral.primary,
|
||||
shape = RoundedCornerShape(TangemTheme.dimens2.x5),
|
||||
)
|
||||
.padding(16.dp),
|
||||
) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(bottom = 4.dp)) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_explore_16),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors2.markers.iconGray,
|
||||
modifier = Modifier.size(16.dp),
|
||||
)
|
||||
SpacerW(2.dp)
|
||||
Text(
|
||||
text = relatedArticle.media.name,
|
||||
style = TangemTheme.typography2.captionSemibold12,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = TangemTheme.colors2.text.neutral.secondary,
|
||||
)
|
||||
}
|
||||
if (relatedArticle.title.isNotEmpty()) {
|
||||
SpacerH(8.dp)
|
||||
Text(
|
||||
text = relatedArticle.title,
|
||||
style = TangemTheme.typography2.bodyRegular16,
|
||||
color = TangemTheme.colors2.text.neutral.primary,
|
||||
maxLines = 3,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (relatedArticle.imageUrl != null) {
|
||||
SubcomposeAsyncImage(
|
||||
modifier = Modifier
|
||||
.size(44.dp)
|
||||
.clip(RoundedCornerShape(12.dp)),
|
||||
contentScale = ContentScale.Crop,
|
||||
model = ImageRequest.Builder(context = LocalContext.current)
|
||||
.data(relatedArticle.imageUrl)
|
||||
.crossfade(enable = false)
|
||||
.allowHardware(true)
|
||||
.memoryCachePolicy(CachePolicy.DISABLED)
|
||||
.build(),
|
||||
loading = {
|
||||
RectangleShimmer(
|
||||
modifier = Modifier.size(44.dp),
|
||||
radius = 12.dp,
|
||||
)
|
||||
},
|
||||
error = {},
|
||||
contentDescription = relatedArticle.media.name,
|
||||
)
|
||||
}
|
||||
}
|
||||
SpacerHMax()
|
||||
Text(
|
||||
text = relatedArticle.publishedAt.resolveReference(),
|
||||
style = TangemTheme.typography2.captionSemibold12,
|
||||
color = TangemTheme.colors2.text.neutral.secondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.tangem.features.feed.ui.news.details.components
|
|||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
|
|
@ -16,6 +17,7 @@ import com.tangem.core.ui.components.SpacerH
|
|||
import com.tangem.core.ui.components.block.BlockCard
|
||||
import com.tangem.core.ui.components.block.TangemBlockCardColors
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.LocalRedesignEnabled
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.feed.model.news.details.NewsDetailsModel.Companion.RELATED_TOKEN_MAX_COUNT
|
||||
import com.tangem.features.feed.ui.news.details.state.RelatedTokensUM
|
||||
|
|
@ -38,11 +40,20 @@ internal fun RelatedTokensBlock(
|
|||
|
||||
Column(modifier = modifier) {
|
||||
SpacerH(40.dp)
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.news_related_tokens),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
if (LocalRedesignEnabled.current) {
|
||||
Text(
|
||||
modifier = Modifier.padding(horizontal = 8.dp),
|
||||
text = stringResourceSafe(R.string.news_related_tokens),
|
||||
style = TangemTheme.typography2.headingSemibold20,
|
||||
color = TangemTheme.colors2.text.neutral.primary,
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.news_related_tokens),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
}
|
||||
SpacerH(12.dp)
|
||||
|
||||
BlockCard(
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import kotlinx.collections.immutable.toPersistentList
|
|||
internal object MockArticlesFactory {
|
||||
fun createMockArticles(): ImmutableList<ArticleUM> = listOf(
|
||||
ArticleUM(
|
||||
isTrending = false,
|
||||
id = 1,
|
||||
title = "SEC delays decisions on ETH-staking ETFs and spot XRP/SOL funds",
|
||||
createdAt = TextReference.Str("20 Jun, 21:45"),
|
||||
|
|
@ -78,6 +79,7 @@ internal object MockArticlesFactory {
|
|||
newsUrl = "",
|
||||
relatedTokens = persistentListOf(),
|
||||
isLiked = false,
|
||||
isTrending = false,
|
||||
),
|
||||
ArticleUM(
|
||||
id = 3,
|
||||
|
|
@ -107,6 +109,7 @@ internal object MockArticlesFactory {
|
|||
newsUrl = "",
|
||||
relatedTokens = persistentListOf(),
|
||||
isLiked = false,
|
||||
isTrending = false,
|
||||
),
|
||||
ArticleUM(
|
||||
id = 4,
|
||||
|
|
@ -136,6 +139,7 @@ internal object MockArticlesFactory {
|
|||
newsUrl = "",
|
||||
relatedTokens = persistentListOf(),
|
||||
isLiked = false,
|
||||
isTrending = false,
|
||||
),
|
||||
ArticleUM(
|
||||
id = 5,
|
||||
|
|
@ -151,6 +155,7 @@ internal object MockArticlesFactory {
|
|||
newsUrl = "",
|
||||
relatedTokens = persistentListOf(),
|
||||
isLiked = false,
|
||||
isTrending = false,
|
||||
),
|
||||
ArticleUM(
|
||||
id = 6,
|
||||
|
|
@ -167,6 +172,7 @@ internal object MockArticlesFactory {
|
|||
newsUrl = "",
|
||||
relatedTokens = persistentListOf(),
|
||||
isLiked = false,
|
||||
isTrending = false,
|
||||
),
|
||||
ArticleUM(
|
||||
id = 7,
|
||||
|
|
@ -182,6 +188,7 @@ internal object MockArticlesFactory {
|
|||
newsUrl = "",
|
||||
relatedTokens = persistentListOf(),
|
||||
isLiked = false,
|
||||
isTrending = false,
|
||||
),
|
||||
ArticleUM(
|
||||
id = 8,
|
||||
|
|
@ -197,6 +204,7 @@ internal object MockArticlesFactory {
|
|||
newsUrl = "",
|
||||
relatedTokens = persistentListOf(),
|
||||
isLiked = false,
|
||||
isTrending = false,
|
||||
),
|
||||
ArticleUM(
|
||||
id = 9,
|
||||
|
|
@ -213,6 +221,7 @@ internal object MockArticlesFactory {
|
|||
newsUrl = "",
|
||||
relatedTokens = persistentListOf(),
|
||||
isLiked = false,
|
||||
isTrending = false,
|
||||
),
|
||||
ArticleUM(
|
||||
id = 10,
|
||||
|
|
@ -229,6 +238,7 @@ internal object MockArticlesFactory {
|
|||
newsUrl = "",
|
||||
relatedTokens = persistentListOf(),
|
||||
isLiked = false,
|
||||
isTrending = false,
|
||||
),
|
||||
).toPersistentList()
|
||||
}
|
||||
|
|
@ -25,6 +25,7 @@ internal sealed interface ArticlesStateUM {
|
|||
data class LoadingError(val onRetryClicked: () -> Unit) : ArticlesStateUM
|
||||
}
|
||||
|
||||
@Immutable
|
||||
internal data class ArticleUM(
|
||||
val id: Int,
|
||||
val title: String,
|
||||
|
|
@ -37,6 +38,7 @@ internal data class ArticleUM(
|
|||
val newsUrl: String,
|
||||
val relatedTokens: ImmutableList<RelatedToken>,
|
||||
val isLiked: Boolean,
|
||||
val isTrending: Boolean,
|
||||
)
|
||||
|
||||
internal data class RelatedArticleUM(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
@file:Suppress("MagicNumber", "LongMethod")
|
||||
|
||||
package com.tangem.feature.tester.presentation.storybook.page.badge
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
|
|
@ -186,7 +187,7 @@ private fun BadgeTypeRow(
|
|||
) {
|
||||
TangemBadge(
|
||||
text = stringReference("New"),
|
||||
tangemIconUM = TangemIconUM.Icon(R.drawable.ic_information_24),
|
||||
tangemIconUM = TangemIconUM.Icon(iconRes = R.drawable.ic_information_24),
|
||||
size = size,
|
||||
shape = shape,
|
||||
color = color,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue