Updated on 2026-08-14

This commit is contained in:
Tangem 2026-05-27 12:00:15 +02:00
parent 13d5d24cd4
commit e018eee87b
8 changed files with 289 additions and 204 deletions

View file

@ -8,12 +8,7 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.defaultAmount
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.formatStyled
import com.tangem.core.ui.format.bigdecimal.percent
import com.tangem.core.ui.format.bigdecimal.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
@ -61,12 +56,12 @@ internal class UpdateStakingNotificationTransformer(
iconUM = EarnBlockUM.IconUM.Plain(iconRes = CoreUiR.drawable.ic_staking_disable_40),
titleUM = EarnBlockUM.TitleUM(
text = resourceReference(CoreResR.string.common_staking),
style = EarnBlockUM.TitleUM.Style.Large,
style = EarnBlockUM.TitleUM.Style.Small,
tone = EarnBlockUM.TitleUM.Tone.Disabled,
),
subtitleUM = EarnBlockUM.SubtitleUM.Text(
text = resourceReference(CoreResR.string.staking_notification_network_error_text),
style = EarnBlockUM.SubtitleUM.Style.Small,
style = EarnBlockUM.SubtitleUM.Style.Large,
tone = EarnBlockUM.SubtitleUM.Tone.Disabled,
),
trailingUM = null,
@ -122,12 +117,12 @@ internal class UpdateStakingNotificationTransformer(
iconUM = EarnBlockUM.IconUM.Glowing(iconRes = CoreUiR.drawable.ic_staking_40),
titleUM = EarnBlockUM.TitleUM(
text = resourceReference(id = CoreResR.string.common_staking),
style = EarnBlockUM.TitleUM.Style.Large,
style = EarnBlockUM.TitleUM.Style.Small,
tone = EarnBlockUM.TitleUM.Tone.Primary,
),
subtitleUM = EarnBlockUM.SubtitleUM.Text(
text = stakeAvailableSubtitle(availability.option.displayRewardInfo),
style = EarnBlockUM.SubtitleUM.Style.Small,
style = EarnBlockUM.SubtitleUM.Style.Large,
tone = EarnBlockUM.SubtitleUM.Tone.Disabled,
),
trailingUM = EarnBlockUM.TrailingUM.Button(
@ -170,7 +165,7 @@ internal class UpdateStakingNotificationTransformer(
iconUM = EarnBlockUM.IconUM.Glowing(iconRes = CoreUiR.drawable.ic_staking_40),
titleUM = EarnBlockUM.TitleUM(
text = resourceReference(CoreResR.string.staking_enabled),
style = EarnBlockUM.TitleUM.Style.Large,
style = EarnBlockUM.TitleUM.Style.Small,
tone = EarnBlockUM.TitleUM.Tone.Primary,
),
subtitleUM = getRewardSubtitle(status, rewardFiatAmount),
@ -257,7 +252,7 @@ internal class UpdateStakingNotificationTransformer(
return EarnBlockUM.SubtitleUM.Text(
text = text,
style = EarnBlockUM.SubtitleUM.Style.Small,
style = EarnBlockUM.SubtitleUM.Style.Large,
tone = if (isAccent) EarnBlockUM.SubtitleUM.Tone.Accent else EarnBlockUM.SubtitleUM.Tone.Disabled,
)
}

View file

@ -2,27 +2,12 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import com.tangem.common.ui.earn.EarnBlock
import com.tangem.common.ui.notifications.notifications
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@ -31,31 +16,30 @@ 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 androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.common.ui.account.AccountIconUM
import com.tangem.common.ui.earn.EarnBlock
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM
import com.tangem.common.ui.expressStatus.state.ExpressTransactionsBlockState
import com.tangem.common.ui.notifications.notifications
import com.tangem.core.ui.components.BottomFade
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig
import com.tangem.core.ui.ds.button.TangemButtonType
import com.tangem.core.ui.ds.button.TangemButtonUM
import com.tangem.core.ui.components.containers.pullToRefresh.TangemPullToRefreshSlidingContainer
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenuItem
import com.tangem.core.ui.components.haze.hazeSourceTangem
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
import com.tangem.core.ui.components.topFade
import com.tangem.core.ui.ds.button.TangemButtonType
import com.tangem.core.ui.ds.button.TangemButtonUM
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.themedColor
import com.tangem.core.ui.res.LocalHazeState
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.domain.models.account.CryptoPortfolioIcon
import com.tangem.feature.tokendetails.presentation.tokendetails.state.AddFundsUM
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBalanceTypeUM
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TransferUM
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockUM
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM
import com.tangem.feature.tokendetails.presentation.tokendetails.state.*
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM.TitleState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsUM
import com.tangem.feature.tokendetails.presentation.tokendetails.state.ZeroBalanceActionsUM
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsBalanceBlock
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.ZeroBalanceActionsBlock
import com.tangem.features.markets.token.block.TokenMarketBlockComponent
@ -64,6 +48,7 @@ import com.tangem.features.txhistory.component.TxHistoryComponent
import com.tangem.features.txhistory.entity.TxHistoryItemsUM
import com.tangem.features.txhistory.entity.TxHistoryUM
import com.tangem.features.yield.supply.api.YieldSupplyComponent
import dev.chrisbanes.haze.rememberHazeState
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.MutableStateFlow
@ -72,6 +57,9 @@ import kotlinx.coroutines.flow.StateFlow
private val TopBarHeight: Dp = 64.dp
private val MarketBlockHorizontalPadding: Dp = 14.dp
private const val TOP_FADE_MID_STOP = 0.8f
private const val TOP_FADE_MID_ALPHA = 0.8f
@Composable
internal fun TokenDetailsScreen(
tokenDetailsUM: TokenDetailsUM,
@ -84,20 +72,17 @@ internal fun TokenDetailsScreen(
val expressState by expressTransactionsComponent.state.collectAsStateWithLifecycle()
val statusBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getTop(this).toDp() }
val topBarTotalHeight = TopBarHeight + statusBarHeight
val hazeState = rememberHazeState()
val rootBackground = TangemTheme.colors2.surface.level2
var marketBlockHeight by remember { mutableStateOf(0.dp) }
val effectiveBottomPadding = marketBlockHeight + TangemTheme.dimens2.x4
Box(
modifier = modifier
.fillMaxSize()
.background(rootBackground),
) {
CompositionLocalProvider(LocalHazeState provides hazeState) {
Box(
modifier = Modifier
modifier = modifier
.fillMaxSize()
.hazeSourceTangem(zIndex = -2f),
.background(rootBackground),
) {
TangemPullToRefreshSlidingContainer(
config = tokenDetailsUM.pullToRefreshConfig,
@ -115,27 +100,17 @@ internal fun TokenDetailsScreen(
modifier = Modifier.fillMaxSize(),
)
}
TokenDetailsTopBar(topAppBarUM = tokenDetailsUM.topAppBarUM)
if (tokenMarketBlockComponent != null) {
TokenDetailsMarketBlockOverlay(
component = tokenMarketBlockComponent,
onHeightChange = { marketBlockHeight = it },
)
}
expressState.bottomSheetSlot?.content(null)
}
TokenDetailsTopBarOverlay(topAppBarUM = tokenDetailsUM.topAppBarUM)
if (tokenMarketBlockComponent != null) {
TokenDetailsMarketBlockOverlay(
component = tokenMarketBlockComponent,
onHeightChange = { marketBlockHeight = it },
)
}
expressState.bottomSheetSlot?.content(null)
}
}
@Composable
private fun TokenDetailsTopBarOverlay(topAppBarUM: TokenDetailsTopAppBarUM) {
Box(
modifier = Modifier.background(TangemTheme.colors2.surface.level2),
) {
TokenDetailsTopBar(topAppBarUM = topAppBarUM)
}
}
@ -189,7 +164,14 @@ private fun TokenDetailsBody(
.padding(start = TangemTheme.dimens2.x4, end = TangemTheme.dimens2.x4, top = TangemTheme.dimens2.x4)
LazyColumn(
modifier = modifier,
modifier = modifier
.hazeSourceTangem(state = LocalHazeState.current)
.topFade(
height = topContentPadding,
0f to rootBackground,
TOP_FADE_MID_STOP to rootBackground.copy(alpha = TOP_FADE_MID_ALPHA),
1f to Color.Transparent,
),
state = listState,
contentPadding = PaddingValues(top = topContentPadding, bottom = bottomContentPadding),
) {
@ -201,14 +183,6 @@ private fun TokenDetailsBody(
)
}
val balance = tokenDetailsUM.balanceBlockUM
if (balance is TokenDetailsBalanceBlockUM.Content && balance.isBalanceZero) {
item(key = "zero_balance_actions") {
ZeroBalanceActionsBlock(
state = tokenDetailsUM.zeroBalanceActionsUM,
modifier = itemModifier,
)
}
}
notifications(
notifications = tokenDetailsUM.notifications,
contentColor = rootBackground,
@ -218,12 +192,20 @@ private fun TokenDetailsBody(
item(key = "staking_block") {
EarnBlock(
state = earnBlock,
modifier = itemModifier,
modifier = itemModifier.padding(vertical = TangemTheme.dimens2.x3),
)
}
}
item(key = "yield_supply_block") {
yieldSupplyComponent.Content(modifier = itemModifier.padding(vertical = TangemTheme.dimens2.x2))
yieldSupplyComponent.Content(modifier = itemModifier.padding(vertical = TangemTheme.dimens2.x3))
}
if (balance is TokenDetailsBalanceBlockUM.Content && balance.isBalanceZero) {
item(key = "zero_balance_actions") {
ZeroBalanceActionsBlock(
state = tokenDetailsUM.zeroBalanceActionsUM,
modifier = itemModifier.padding(vertical = TangemTheme.dimens2.x2),
)
}
}
with(expressTransactionsComponent) {
expressTransactionsContent(

View file

@ -1,8 +1,10 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui
import android.content.res.Configuration
import androidx.annotation.StringRes
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.InlineTextContent
import androidx.compose.foundation.text.TextAutoSize
import androidx.compose.foundation.text.appendInlineContent
@ -11,6 +13,7 @@ import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
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.platform.LocalDensity
import androidx.compose.ui.text.*
@ -30,6 +33,8 @@ import com.tangem.core.ui.components.account.AccountIconSize
import com.tangem.core.ui.components.dropdownmenu.TangemDropdownItem
import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenu
import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenuItem
import com.tangem.core.ui.components.haze.ProvideHaze
import com.tangem.core.ui.components.haze.hazeEffectTangem
import com.tangem.core.ui.ds.image.DeviceIconUM
import com.tangem.core.ui.ds.image.TangemDeviceIcon
import com.tangem.core.ui.ds.topbar.TangemTopBar
@ -44,19 +49,22 @@ import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.domain.models.account.CryptoPortfolioIcon
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM.TitleState
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.*
import com.tangem.core.ui.R as CoreUiR
@Composable
internal fun TokenDetailsTopBar(topAppBarUM: TokenDetailsTopAppBarUM, modifier: Modifier = Modifier) {
val actionModifier = Modifier
.clip(CircleShape)
.hazeEffectTangem { blurRadius = ACTION_BLUR_RADIUS }
TangemTopBar(
modifier = modifier.statusBarsPadding(),
startContent = {
TangemTopBarActionContent(
modifier = actionModifier,
actionUM = TangemTopBarActionUM(
iconRes = R.drawable.ic_arrow_back_28,
onClick = topAppBarUM.onBackClick,
ghostModeProgress = 1f,
),
)
},
@ -65,10 +73,10 @@ internal fun TokenDetailsTopBar(topAppBarUM: TokenDetailsTopAppBarUM, modifier:
var isDropdownMenuShown by rememberSaveable { mutableStateOf(false) }
Box {
TangemTopBarActionContent(
modifier = actionModifier,
actionUM = TangemTopBarActionUM(
iconRes = CoreUiR.drawable.ic_more_default_24,
onClick = { isDropdownMenuShown = true },
ghostModeProgress = 1f,
),
)
TangemDropdownMenu(
@ -100,8 +108,8 @@ internal fun TokenDetailsTopBar(topAppBarUM: TokenDetailsTopAppBarUM, modifier:
TokenDetailsTitle(titleState = topAppBarUM.titleState)
Text(
text = topAppBarUM.subtitle.resolveAnnotatedReference(),
color = TangemTheme.colors2.text.neutral.secondary,
style = TangemTheme.typography2.captionMedium12,
color = TangemTheme.colors2.text.neutral.tertiary,
style = TangemTheme.typography2.captionSemibold12,
textAlign = TextAlign.Center,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
@ -138,11 +146,7 @@ private fun TokenDetailsTitle(titleState: TitleState) {
AdaptiveTokenWithSecondaryRow(
tokenName = titleState.tokenName,
secondaryName = titleState.walletName,
template = stringResourceSafe(
id = CoreUiR.string.token_details_toolbar_title_token_in_wallet,
titleState.tokenName,
titleState.walletName,
),
template = formattedTitleTemplate(CoreUiR.string.token_details_toolbar_title_token_in_wallet),
appearance = appearance,
icon = {
TangemDeviceIcon(
@ -153,21 +157,16 @@ private fun TokenDetailsTitle(titleState: TitleState) {
)
}
is TitleState.WithAccount -> {
val accountNameStr = titleState.accountName.resolveAnnotatedReference().toString()
AdaptiveTokenWithSecondaryRow(
tokenName = titleState.tokenName,
secondaryName = accountNameStr,
template = stringResourceSafe(
id = CoreUiR.string.token_details_toolbar_title_token_in_account,
titleState.tokenName,
accountNameStr,
),
secondaryName = titleState.accountName.resolveAnnotatedReference().toString(),
template = formattedTitleTemplate(CoreUiR.string.token_details_toolbar_title_token_in_account),
appearance = appearance,
icon = {
AccountIcon(
name = titleState.accountName,
icon = titleState.accountIconUM,
size = AccountIconSize.ExtraSmall,
size = AccountIconSize.RedesignExtraSmall,
)
},
)
@ -178,9 +177,12 @@ private fun TokenDetailsTitle(titleState: TitleState) {
/**
* Adaptive title for [TitleState.WithAccount] / [TitleState.WithWallet].
*
* Phrase template carries the [IMAGE_PLACEHOLDER] marker translator decides where
* the icon sits (e.g. "Tether in [⭐] Portfolio" or "Tether in My Wallet [⭐]").
* RTL is handled by BiDi inside the single [Text].
* Raw template carries [TOKEN_MARKER] / [SECONDARY_MARKER] for the names and
* [IMAGE_PLACEHOLDER] for the icon translator decides where each sits
* (e.g. "Tether in [⭐] Portfolio" or "Tether in My Wallet [⭐]"). Anything outside
* the markers (connecting words, punctuation) is painted as tertiary text no
* locale-specific substring matching required. RTL is handled by BiDi inside the
* single [Text].
*
* Width-driven cascade:
* 12. Full phrase as single [Text] with inline icon; [TextOverflow.Ellipsis]
@ -201,7 +203,7 @@ private fun AdaptiveTokenWithSecondaryRow(
appearance: TitleAppearance,
icon: @Composable () -> Unit,
) {
val (beforeIcon, afterIcon) = remember(template) { splitTemplate(template) }
val segments = remember(template) { parseTemplate(template) }
val inlineContent = rememberIconInlineContent(appearance.iconSize, icon)
BoxWithConstraints(
@ -209,16 +211,16 @@ private fun AdaptiveTokenWithSecondaryRow(
contentAlignment = Alignment.Center,
) {
val isFullTextShown = rememberShouldShowFullText(
beforeIcon = beforeIcon,
afterIcon = afterIcon,
secondaryName = secondaryName,
segments = segments,
tokenName = tokenName,
appearance = appearance,
maxWidthPx = constraints.maxWidth,
)
if (isFullTextShown) {
FullPhraseTitle(
beforeIcon = beforeIcon,
afterIcon = afterIcon,
segments = segments,
tokenName = tokenName,
secondaryName = secondaryName,
style = appearance.style,
inlineContent = inlineContent,
)
@ -232,17 +234,25 @@ private fun AdaptiveTokenWithSecondaryRow(
}
}
private fun splitTemplate(template: String): Pair<String, String> {
val parts = template.split(IMAGE_PLACEHOLDER, limit = 2)
return if (parts.size == 2) parts[0] to parts[1] else template to ""
}
/**
* Returns the title template with its `%1$s` / `%2$s` placeholders replaced by [TOKEN_MARKER] /
* [SECONDARY_MARKER] sentinels (and `%%image%%` collapsed to [IMAGE_PLACEHOLDER]).
*
* Feeding the markers in as format args lets [String.format] resolve all escaping for us, so
* [parseTemplate] sees a clean string and the real names stay un-substituted until render time.
*/
@Composable
private fun formattedTitleTemplate(@StringRes id: Int): String = stringResourceSafe(id, TOKEN_MARKER, SECONDARY_MARKER)
@Composable
private fun rememberIconInlineContent(iconSize: Dp, icon: @Composable () -> Unit): Map<String, InlineTextContent> {
private fun rememberIconInlineContent(
iconSize: Dp,
icon: @Composable () -> Unit,
): ImmutableMap<String, InlineTextContent> {
val iconSizeSp = with(LocalDensity.current) { iconSize.toSp() }
val currentIcon by rememberUpdatedState(icon)
return remember(iconSizeSp) {
mapOf(
persistentMapOf(
ICON_INLINE_ID to InlineTextContent(
placeholder = Placeholder(
width = iconSizeSp,
@ -261,23 +271,29 @@ private fun rememberIconInlineContent(iconSize: Dp, icon: @Composable () -> Unit
@Composable
private fun rememberShouldShowFullText(
beforeIcon: String,
afterIcon: String,
secondaryName: String,
segments: ImmutableList<TitleSegment>,
tokenName: String,
appearance: TitleAppearance,
maxWidthPx: Int,
): Boolean {
val measurer = rememberTextMeasurer()
val density = LocalDensity.current
return remember(beforeIcon, afterIcon, secondaryName, maxWidthPx, appearance, density) {
return remember(segments, tokenName, maxWidthPx, appearance, density) {
if (maxWidthPx <= 0) return@remember true
val fullTextWidthPx = measurer
.measure(text = beforeIcon + afterIcon, style = appearance.style, softWrap = false)
.size.width
val secondaryWidthPx = measurer
.measure(text = secondaryName, style = appearance.style, softWrap = false)
.size.width
val staticWidthPx = (fullTextWidthPx - secondaryWidthPx).coerceAtLeast(0)
val staticText = buildString {
for (segment in segments) {
when (segment) {
TitleSegment.Token -> append(tokenName)
is TitleSegment.Plain -> append(segment.text)
TitleSegment.Secondary, TitleSegment.Image -> Unit
}
}
}
val staticWidthPx = if (staticText.isEmpty()) {
0
} else {
measurer.measure(text = staticText, style = appearance.style, softWrap = false).size.width
}
val iconReservePx = with(density) {
(appearance.iconSize + appearance.spacing * 2).toPx()
}.toInt()
@ -288,16 +304,25 @@ private fun rememberShouldShowFullText(
@Composable
private fun FullPhraseTitle(
beforeIcon: String,
afterIcon: String,
segments: ImmutableList<TitleSegment>,
tokenName: String,
secondaryName: String,
style: TextStyle,
inlineContent: Map<String, InlineTextContent>,
inlineContent: ImmutableMap<String, InlineTextContent>,
) {
val fullText = remember(beforeIcon, afterIcon) {
val tertiaryColor = TangemTheme.colors2.text.neutral.tertiary
val fullText = remember(segments, tokenName, secondaryName, tertiaryColor) {
buildAnnotatedString {
append(beforeIcon)
appendInlineContent(ICON_INLINE_ID, IMAGE_PLACEHOLDER)
append(afterIcon)
for (segment in segments) {
when (segment) {
TitleSegment.Token -> append(tokenName)
TitleSegment.Secondary -> append(secondaryName)
TitleSegment.Image -> appendInlineContent(ICON_INLINE_ID, IMAGE_PLACEHOLDER)
is TitleSegment.Plain -> withStyle(SpanStyle(color = tertiaryColor)) {
append(segment.text)
}
}
}
}
}
Text(
@ -311,6 +336,47 @@ private fun FullPhraseTitle(
)
}
private sealed interface TitleSegment {
data object Token : TitleSegment
data object Secondary : TitleSegment
data object Image : TitleSegment
data class Plain(val text: String) : TitleSegment
}
/**
* Splits the [String.format]ed template (see [formattedTitleTemplate]) into ordered segments.
*
* Markers appear in their resolved form: [TOKEN_MARKER] / [SECONDARY_MARKER] for the names and
* [IMAGE_PLACEHOLDER] for the icon (escaping was already collapsed by [String.format]). Anything
* else is plain connecting text painted as tertiary.
*/
private fun parseTemplate(template: String): ImmutableList<TitleSegment> {
val segments = mutableListOf<TitleSegment>()
val plain = StringBuilder()
fun flushPlain() {
if (plain.isNotEmpty()) {
segments += TitleSegment.Plain(plain.toString())
plain.clear()
}
}
var i = 0
while (i < template.length) {
val marker = MARKERS.firstOrNull { (text, _) -> template.startsWith(text, i) }
if (marker != null) {
flushPlain()
segments += marker.second
i += marker.first.length
} else {
plain.append(template[i])
i++
}
}
flushPlain()
return segments.toImmutableList()
}
@Composable
private fun FallbackTokenWithIconTitle(tokenName: String, appearance: TitleAppearance, icon: @Composable () -> Unit) {
Row(
@ -342,7 +408,17 @@ private data class TitleAppearance(
private const val ICON_INLINE_ID = "account_icon"
private const val IMAGE_PLACEHOLDER = "%image%"
private const val TOKEN_MARKER = "%1\$s"
private const val SECONDARY_MARKER = "%2\$s"
// Order matters: longer/overlapping markers must be tried first by [parseTemplate].
private val MARKERS: List<Pair<String, TitleSegment>> = listOf(
IMAGE_PLACEHOLDER to TitleSegment.Image,
TOKEN_MARKER to TitleSegment.Token,
SECONDARY_MARKER to TitleSegment.Secondary,
)
private val ACTION_BLUR_RADIUS = 8.dp
private val MIN_TITLE_FONT_SIZE = 12.sp
private val MAX_TITLE_FONT_SIZE = 16.sp
private val MIN_SECONDARY_NAME_WIDTH = 48.dp
@ -355,20 +431,22 @@ private fun TokenDetailsTopBar_Preview(
@PreviewParameter(TokenDetailsTopBarPreviewProvider::class) titleState: TitleState,
) {
TangemThemePreviewRedesign {
TokenDetailsTopBar(
topAppBarUM = TokenDetailsTopAppBarUM(
titleState = titleState,
subtitle = stringReference("ERC-20 in Ethereum network"),
onBackClick = {},
menuItems = persistentListOf(
TangemDropdownMenuItem(
title = stringReference("Hide Token"),
textColor = themedColor { TangemTheme.colors.text.warning },
onClick = {},
ProvideHaze {
TokenDetailsTopBar(
topAppBarUM = TokenDetailsTopAppBarUM(
titleState = titleState,
subtitle = stringReference("ERC-20 in Ethereum network"),
onBackClick = {},
menuItems = persistentListOf(
TangemDropdownMenuItem(
title = stringReference("Hide Token"),
textColor = themedColor { TangemTheme.colors.text.warning },
onClick = {},
),
),
),
),
)
)
}
}
}

View file

@ -4,16 +4,10 @@ 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.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -102,8 +96,7 @@ private fun ContentBody(state: TokenDetailsBalanceBlockUM.Content, isBalanceHidd
targetState = state.tokenBalanceTypeUM.type,
label = "Token balance type",
) { currentType ->
val tokenBalanceTypeUM = state.tokenBalanceTypeUM
when (tokenBalanceTypeUM) {
when (val tokenBalanceTypeUM = state.tokenBalanceTypeUM) {
is TokenBalanceTypeUM.Multiple -> Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1),
@ -111,20 +104,20 @@ private fun ContentBody(state: TokenDetailsBalanceBlockUM.Content, isBalanceHidd
) {
Text(
text = currentType.text.resolveReference(),
style = TangemTheme.typography2.calloutSemibold15,
color = TangemTheme.colors2.text.neutral.secondary,
style = TangemTheme.typography2.subheadlineMedium14,
color = TangemTheme.colors2.text.neutral.primary,
)
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_sort_24),
contentDescription = null,
tint = TangemTheme.colors2.graphic.neutral.secondary,
modifier = Modifier.size(TangemTheme.dimens2.x4),
modifier = Modifier.size(TangemTheme.dimens2.x5),
)
}
TokenBalanceTypeUM.Single -> Text(
text = currentType.text.resolveReference(),
style = TangemTheme.typography2.calloutSemibold15,
color = TangemTheme.colors2.text.neutral.secondary,
style = TangemTheme.typography2.subheadlineMedium14,
color = TangemTheme.colors2.text.neutral.primary,
)
}
}

View file

@ -1,11 +1,7 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components
import androidx.annotation.DrawableRes
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.*
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@ -26,7 +22,7 @@ internal fun ZeroBalanceActionsBlock(state: ZeroBalanceActionsUM, modifier: Modi
Column(
modifier = modifier
.fillMaxWidth()
.padding(bottom = TangemTheme.dimens2.x10),
.padding(bottom = TangemTheme.dimens2.x6),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2),
) {
ActionRow(