Updated on 2026-08-14
This commit is contained in:
parent
13d5d24cd4
commit
e018eee87b
8 changed files with 289 additions and 204 deletions
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
* 1–2. 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 = {},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue