Updated on 2026-08-14
This commit is contained in:
commit
63849d3c02
155 changed files with 2317 additions and 607 deletions
11
core/ui/src/main/java/com/tangem/core/ui/UiDependencies.kt
Normal file
11
core/ui/src/main/java/com/tangem/core/ui/UiDependencies.kt
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.core.ui
|
||||
|
||||
import com.tangem.core.ui.haptic.HapticManager
|
||||
import com.tangem.core.ui.theme.AppThemeModeHolder
|
||||
|
||||
interface UiDependencies {
|
||||
|
||||
val hapticManager: HapticManager
|
||||
|
||||
val appThemeModeHolder: AppThemeModeHolder
|
||||
}
|
||||
|
|
@ -95,10 +95,14 @@ fun AmountTextField(
|
|||
SimpleTextField(
|
||||
value = value,
|
||||
onValueChange = { newText ->
|
||||
if (decimalFormat.isValidSymbols(newText)) {
|
||||
val trimmed = decimalFormat.getValidatedNumberWithFixedDecimals(newText, decimals)
|
||||
onValueChange(trimmed)
|
||||
}
|
||||
onValueChange(
|
||||
prepareEnter(
|
||||
oldValue = value,
|
||||
newValue = newText,
|
||||
decimalFormat = decimalFormat,
|
||||
decimals = decimals,
|
||||
),
|
||||
)
|
||||
},
|
||||
textStyle = textStyle.copy(
|
||||
fontSize = fontSize,
|
||||
|
|
@ -117,8 +121,37 @@ fun AmountTextField(
|
|||
}
|
||||
}
|
||||
|
||||
private fun prepareEnter(oldValue: String, newValue: String, decimalFormat: DecimalFormat, decimals: Int): String {
|
||||
val decimalSymbol = decimalFormat.decimalFormatSymbols.decimalSeparator
|
||||
return if (decimalFormat.isValidSymbols(newValue)) {
|
||||
val parsedValue = newValue.parseBigDecimalOrNull()?.toPlainString()
|
||||
?: if (newValue.isBlank()) "" else oldValue
|
||||
val replacedWithSymbol = if (parsedValue.findLast { it != decimalSymbol } != null) {
|
||||
when {
|
||||
parsedValue.findLast { it == COMMA_SEPARATOR } != null -> {
|
||||
parsedValue.replace(COMMA_SEPARATOR, decimalSymbol)
|
||||
}
|
||||
parsedValue.findLast { it == POINT_SEPARATOR } != null -> {
|
||||
parsedValue.replace(POINT_SEPARATOR, decimalSymbol)
|
||||
}
|
||||
else -> parsedValue
|
||||
}
|
||||
} else {
|
||||
parsedValue
|
||||
}
|
||||
val joinedSymbol = if (newValue.endsWith(COMMA_SEPARATOR) || newValue.endsWith(POINT_SEPARATOR)) {
|
||||
replacedWithSymbol.plus(decimalSymbol)
|
||||
} else {
|
||||
replacedWithSymbol
|
||||
}
|
||||
decimalFormat.getValidatedNumberWithFixedDecimals(joinedSymbol, decimals)
|
||||
} else {
|
||||
oldValue
|
||||
}
|
||||
}
|
||||
|
||||
private fun DecimalFormat.isValidSymbols(text: String): Boolean {
|
||||
return checkDecimalSeparatorDuplicate(text) && checkGroupingSeparator(text)
|
||||
return checkDecimalSeparatorDuplicate(text)
|
||||
}
|
||||
|
||||
// region preview
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ fun SimpleTextField(
|
|||
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
||||
color: Color = TangemTheme.colors.text.primary1,
|
||||
textStyle: TextStyle = TangemTheme.typography.body2.copy(color = color),
|
||||
placeholderColor: Color = TangemTheme.colors.text.disabled,
|
||||
readOnly: Boolean = false,
|
||||
isValuePasted: Boolean = false,
|
||||
onValuePastedTriggerDismiss: () -> Unit = {},
|
||||
|
|
@ -108,6 +109,7 @@ fun SimpleTextField(
|
|||
value = value,
|
||||
textStyle = textStyle,
|
||||
textValue = textValue,
|
||||
color = placeholderColor,
|
||||
)
|
||||
},
|
||||
modifier = modifier
|
||||
|
|
@ -122,6 +124,7 @@ private fun SimpleTextPlaceholder(
|
|||
value: String,
|
||||
textStyle: TextStyle,
|
||||
textValue: @Composable () -> Unit,
|
||||
color: Color = TangemTheme.colors.text.disabled,
|
||||
) {
|
||||
Box {
|
||||
if (value.isBlank() && placeholder != null) {
|
||||
|
|
@ -132,7 +135,7 @@ private fun SimpleTextPlaceholder(
|
|||
Text(
|
||||
text = it.resolveReference(),
|
||||
style = textStyle,
|
||||
color = TangemTheme.colors.text.disabled,
|
||||
color = color,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,12 +51,14 @@ fun Notification(
|
|||
modifier: Modifier = Modifier,
|
||||
containerColor: Color? = null,
|
||||
iconTint: Color? = null,
|
||||
isEnabled: Boolean = true,
|
||||
) {
|
||||
BaseContainer(
|
||||
buttonsState = config.buttonsState,
|
||||
onClick = config.onClick,
|
||||
modifier = modifier,
|
||||
containerColor = containerColor,
|
||||
isEnabled = isEnabled,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(all = TangemTheme.dimens.spacing12),
|
||||
|
|
@ -67,15 +69,16 @@ fun Notification(
|
|||
iconTint = iconTint,
|
||||
title = config.title,
|
||||
subtitle = config.subtitle,
|
||||
isClickableComponent = config.onClick != null,
|
||||
isClickableComponent = isEnabled && config.onClick != null,
|
||||
)
|
||||
|
||||
Buttons(state = config.buttonsState)
|
||||
Buttons(state = config.buttonsState, isEnabled = isEnabled)
|
||||
}
|
||||
|
||||
CloseableIconButton(
|
||||
onClick = config.onCloseClick,
|
||||
modifier = Modifier.align(alignment = Alignment.TopEnd),
|
||||
isEnabled = isEnabled,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -85,6 +88,7 @@ private fun BaseContainer(
|
|||
buttonsState: NotificationConfig.ButtonsState?,
|
||||
onClick: (() -> Unit)?,
|
||||
modifier: Modifier = Modifier,
|
||||
isEnabled: Boolean = true,
|
||||
containerColor: Color? = null,
|
||||
content: @Composable BoxScope.() -> Unit,
|
||||
) {
|
||||
|
|
@ -101,7 +105,7 @@ private fun BaseContainer(
|
|||
modifier = modifier
|
||||
.defaultMinSize(minHeight = TangemTheme.dimens.size62)
|
||||
.fillMaxWidth(),
|
||||
enabled = onClick != null,
|
||||
enabled = onClick != null && isEnabled,
|
||||
shape = TangemTheme.shapes.roundedCornersXMedium,
|
||||
color = containerColor ?: tempContainerColor,
|
||||
) {
|
||||
|
|
@ -179,27 +183,31 @@ private fun TextsBlock(title: TextReference, subtitle: TextReference) {
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun Buttons(state: NotificationButtonsState?) {
|
||||
private fun Buttons(state: NotificationButtonsState?, isEnabled: Boolean = true) {
|
||||
when (state) {
|
||||
is NotificationButtonsState.SecondaryButtonConfig -> SingleSecondaryButton(config = state)
|
||||
is NotificationButtonsState.PrimaryButtonConfig -> SinglePrimaryButton(config = state)
|
||||
is NotificationButtonsState.PairButtonsConfig -> PairButtons(config = state)
|
||||
is NotificationButtonsState.SecondaryButtonConfig -> SingleSecondaryButton(
|
||||
config = state,
|
||||
isEnabled = isEnabled,
|
||||
)
|
||||
is NotificationButtonsState.PrimaryButtonConfig -> SinglePrimaryButton(config = state, isEnabled = isEnabled)
|
||||
is NotificationButtonsState.PairButtonsConfig -> PairButtons(config = state, isEnabled = isEnabled)
|
||||
null -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SingleSecondaryButton(config: NotificationButtonsState.SecondaryButtonConfig) {
|
||||
private fun SingleSecondaryButton(config: NotificationButtonsState.SecondaryButtonConfig, isEnabled: Boolean = true) {
|
||||
SecondaryButton(
|
||||
text = config.text.resolveReference(),
|
||||
onClick = config.onClick,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
size = TangemButtonSize.WideAction,
|
||||
enabled = isEnabled,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SinglePrimaryButton(config: NotificationButtonsState.PrimaryButtonConfig) {
|
||||
private fun SinglePrimaryButton(config: NotificationButtonsState.PrimaryButtonConfig, isEnabled: Boolean = true) {
|
||||
if (config.iconResId != null) {
|
||||
PrimaryButtonIconEnd(
|
||||
text = config.text.resolveReference(),
|
||||
|
|
@ -207,6 +215,7 @@ private fun SinglePrimaryButton(config: NotificationButtonsState.PrimaryButtonCo
|
|||
onClick = config.onClick,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
size = TangemButtonSize.WideAction,
|
||||
enabled = isEnabled,
|
||||
)
|
||||
} else {
|
||||
PrimaryButton(
|
||||
|
|
@ -214,18 +223,20 @@ private fun SinglePrimaryButton(config: NotificationButtonsState.PrimaryButtonCo
|
|||
onClick = config.onClick,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
size = TangemButtonSize.WideAction,
|
||||
enabled = isEnabled,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PairButtons(config: NotificationButtonsState.PairButtonsConfig) {
|
||||
private fun PairButtons(config: NotificationButtonsState.PairButtonsConfig, isEnabled: Boolean = true) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing8)) {
|
||||
SecondaryButton(
|
||||
text = config.secondaryText.resolveReference(),
|
||||
onClick = config.onSecondaryClick,
|
||||
modifier = Modifier.weight(weight = 1f),
|
||||
size = TangemButtonSize.WideAction,
|
||||
enabled = isEnabled,
|
||||
)
|
||||
|
||||
PrimaryButton(
|
||||
|
|
@ -233,12 +244,13 @@ private fun PairButtons(config: NotificationButtonsState.PairButtonsConfig) {
|
|||
onClick = config.onPrimaryClick,
|
||||
modifier = Modifier.weight(weight = 1f),
|
||||
size = TangemButtonSize.WideAction,
|
||||
enabled = isEnabled,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CloseableIconButton(onClick: (() -> Unit)?, modifier: Modifier = Modifier) {
|
||||
private fun CloseableIconButton(onClick: (() -> Unit)?, modifier: Modifier = Modifier, isEnabled: Boolean = true) {
|
||||
AnimatedVisibility(visible = onClick != null, modifier = modifier) {
|
||||
onClick ?: return@AnimatedVisibility
|
||||
|
||||
|
|
@ -255,6 +267,7 @@ private fun CloseableIconButton(onClick: (() -> Unit)?, modifier: Modifier = Mod
|
|||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = LocalIndication.current,
|
||||
role = Role.Button,
|
||||
enabled = isEnabled,
|
||||
onClick = onClick,
|
||||
),
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -49,7 +49,11 @@ fun NotificationWithBackground(config: NotificationConfig, modifier: Modifier =
|
|||
modifier = modifier
|
||||
.defaultMinSize(minHeight = TangemTheme.dimens.size62)
|
||||
.fillMaxWidth()
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium),
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.clickable(
|
||||
enabled = config.onClick != null,
|
||||
onClick = config.onClick ?: {},
|
||||
),
|
||||
) {
|
||||
val (iconRef, titleRef, subtitleRef, closeIconRef, buttonRef, backgroundRef) = createRefs()
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,215 @@
|
|||
package com.tangem.core.ui.components.notifications
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.layout.ScaleFactor
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.style.LineBreak
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.SpacerH8
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButton
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonColors
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.res.TangemColorPalette.White
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
/**
|
||||
* Travala notification with image background
|
||||
* @see <a href="https://www.figma.com/file/Vs6SkVsFnUPsSCNwlnVf5U/Android-%E2%80%93-UI?type=design&node-id=11690-12057&mode=design&t=eFnsA9sNytcQIoQ4-4">Travala Promo</a>
|
||||
*/
|
||||
@Suppress("LongMethod", "DestructuringDeclarationWithTooManyEntries")
|
||||
@Composable
|
||||
fun TravalaNotificationWithBackground(config: NotificationConfig, modifier: Modifier = Modifier) {
|
||||
val button = config.buttonsState as? NotificationConfig.ButtonsState.SecondaryButtonConfig
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.defaultMinSize(minHeight = TangemTheme.dimens.size62)
|
||||
.fillMaxWidth()
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(Color.Black)
|
||||
.clickable(
|
||||
enabled = config.onClick != null,
|
||||
onClick = config.onClick ?: {},
|
||||
),
|
||||
propagateMinConstraints = true,
|
||||
contentAlignment = Alignment.TopStart,
|
||||
) {
|
||||
val density = LocalDensity.current
|
||||
Image(
|
||||
painter = painterResource(R.drawable.img_travala_banner_promo_background),
|
||||
contentDescription = null,
|
||||
contentScale = TravalaBackgroundScale(density),
|
||||
alignment = Alignment.TopStart,
|
||||
modifier = Modifier
|
||||
.matchParentSize()
|
||||
.wrapContentSize(unbounded = true, align = Alignment.TopStart)
|
||||
.align(Alignment.TopStart),
|
||||
)
|
||||
Image(
|
||||
painter = painterResource(R.drawable.img_travala_banner_promo_background_2),
|
||||
contentDescription = null,
|
||||
contentScale = TravalaBackgroundScale(density),
|
||||
alignment = Alignment.TopStart,
|
||||
modifier = Modifier
|
||||
.matchParentSize()
|
||||
.wrapContentSize(unbounded = true, align = Alignment.TopEnd)
|
||||
.align(Alignment.TopEnd),
|
||||
)
|
||||
Column {
|
||||
Row {
|
||||
Box(modifier = Modifier.size(87.dp))
|
||||
Column(
|
||||
Modifier
|
||||
.weight(1f)
|
||||
.padding(top = TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
Text(
|
||||
text = config.title.resolveReference(),
|
||||
style = TangemTheme.typography.button.copy(
|
||||
lineBreak = LineBreak.Heading,
|
||||
),
|
||||
color = TangemTheme.colors.text.constantWhite,
|
||||
)
|
||||
SpacerH8()
|
||||
Text(
|
||||
text = formatSubtitle(config.subtitle.resolveReference()),
|
||||
style = TangemTheme.typography.caption2.copy(
|
||||
lineBreak = LineBreak.Heading,
|
||||
),
|
||||
color = TangemTheme.colors.text.constantWhite,
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_close_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.text.constantWhite,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
top = TangemTheme.dimens.spacing12,
|
||||
end = TangemTheme.dimens.spacing12,
|
||||
start = TangemTheme.dimens.spacing2,
|
||||
)
|
||||
.size(TangemTheme.dimens.size16)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(bounded = false),
|
||||
) {
|
||||
config.onCloseClick?.invoke()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
TangemButton(
|
||||
text = button?.text?.resolveReference().orEmpty(),
|
||||
icon = TangemButtonIconPosition.None,
|
||||
onClick = button?.onClick ?: {},
|
||||
colors = TangemButtonColors(
|
||||
backgroundColor = White.copy(alpha = 0.3f),
|
||||
contentColor = White,
|
||||
disabledBackgroundColor = TangemTheme.colors.button.disabled,
|
||||
disabledContentColor = TangemTheme.colors.text.disabled,
|
||||
),
|
||||
enabled = true,
|
||||
showProgress = false,
|
||||
modifier = Modifier
|
||||
.padding(TangemTheme.dimens.spacing12)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val TRAVALA_BACKGROUND_SRC_IMG_SCALE = 4
|
||||
|
||||
private class TravalaBackgroundScale(
|
||||
val density: Density,
|
||||
) : ContentScale {
|
||||
override fun computeScaleFactor(srcSize: Size, dstSize: Size): ScaleFactor {
|
||||
with(density) {
|
||||
val originalWidth = (srcSize.width / TRAVALA_BACKGROUND_SRC_IMG_SCALE).dp.toPx()
|
||||
val widthScale = originalWidth / srcSize.width
|
||||
val originalHeight = (srcSize.height / TRAVALA_BACKGROUND_SRC_IMG_SCALE).dp.toPx()
|
||||
val heightScale = originalHeight / srcSize.height
|
||||
return ScaleFactor(widthScale, heightScale)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun formatSubtitle(subtitle: String): AnnotatedString {
|
||||
val pattern = Regex("\\*\\*(.*?)\\*\\*")
|
||||
var startIndex = 0
|
||||
val annotatedString = buildAnnotatedString {
|
||||
pattern.findAll(subtitle).forEach { matchResult ->
|
||||
val index = matchResult.range.first
|
||||
val matchedValue = matchResult.groups[1]?.value ?: ""
|
||||
|
||||
// appends unformatted part
|
||||
append(subtitle.substring(startIndex, index))
|
||||
|
||||
// applies style on ^^-wrapped parts
|
||||
withStyle(SpanStyle(fontWeight = TangemTheme.typography.caption1.fontWeight)) {
|
||||
append(matchedValue)
|
||||
}
|
||||
|
||||
// goes to next part
|
||||
startIndex = matchResult.range.last + 1
|
||||
}
|
||||
|
||||
// appends remaining ending if exists
|
||||
append(subtitle.substring(startIndex))
|
||||
}
|
||||
|
||||
return annotatedString
|
||||
}
|
||||
|
||||
//region preview
|
||||
@Preview
|
||||
@Composable
|
||||
private fun TravalaNotificationWithBackgroundPreview() {
|
||||
TangemTheme {
|
||||
TravalaNotificationWithBackground(
|
||||
config = NotificationConfig(
|
||||
title = resourceReference(
|
||||
id = R.string.main_travala_promotion_title,
|
||||
),
|
||||
subtitle = resourceReference(
|
||||
id = R.string.main_travala_promotion_description,
|
||||
formatArgs = wrappedList("May 13", "June 12"),
|
||||
),
|
||||
iconResId = R.drawable.img_swap_promo,
|
||||
backgroundResId = R.drawable.img_travala_banner_promo_background,
|
||||
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
|
||||
text = resourceReference(id = R.string.token_swap_promotion_button),
|
||||
onClick = {},
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
//endregion
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
package com.tangem.core.ui.components.snackbar
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.SnackbarData
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
/**
|
||||
* Snackbar to inform the user about copying text to the clipboard
|
||||
*
|
||||
* @param message message
|
||||
* @param modifier modifier
|
||||
*
|
||||
* @see <a href = https://www.figma.com/design/14ISV23YB1yVW1uNVwqrKv/Android?node-id=2001-728&t=kDzSZDx0m0sk4iYz-4
|
||||
* >Figma</a>
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
fun CopiedTextSnackbar(message: TextReference, modifier: Modifier = Modifier) {
|
||||
BaseSnackbar(message = message, modifier = modifier)
|
||||
}
|
||||
|
||||
/**
|
||||
* Snackbar to inform the user about copying text to the clipboard.
|
||||
*
|
||||
* @param snackbarData this is needed to better support Material3.SnackbarHost, but only supports the message field
|
||||
* @param modifier modifier
|
||||
*
|
||||
* @see <a href = https://www.figma.com/design/14ISV23YB1yVW1uNVwqrKv/Android?node-id=2001-728&t=kDzSZDx0m0sk4iYz-4
|
||||
* >Figma</a>
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
fun CopiedTextSnackbar(snackbarData: SnackbarData, modifier: Modifier = Modifier) {
|
||||
BaseSnackbar(message = stringReference(snackbarData.visuals.message), modifier = modifier)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BaseSnackbar(message: TextReference, modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.background(color = TangemTheme.colors.icon.secondary, shape = TangemTheme.shapes.roundedCorners8)
|
||||
.heightIn(min = TangemTheme.dimens.size48)
|
||||
.padding(
|
||||
horizontal = TangemTheme.dimens.spacing16,
|
||||
vertical = TangemTheme.dimens.spacing14,
|
||||
),
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
MarkIcon()
|
||||
|
||||
MessageText(text = message, modifier = Modifier.weight(weight = 1f, fill = false))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MarkIcon() {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_check_24),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(TangemTheme.dimens.size20),
|
||||
tint = TangemTheme.colors.icon.accent,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MessageText(text: TextReference, modifier: Modifier = Modifier) {
|
||||
Text(
|
||||
text = text.resolveReference(),
|
||||
modifier = modifier,
|
||||
color = TangemTheme.colors.text.disabled,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
style = TangemTheme.typography.body2,
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(widthDp = 344, showBackground = true, fontScale = 1f)
|
||||
@Preview(widthDp = 344, showBackground = true, fontScale = 1f, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Preview(widthDp = 344, showBackground = true, fontScale = 2f)
|
||||
@Composable
|
||||
private fun Preview_CopiedTextSnackbar(
|
||||
@PreviewParameter(CopiedTextSnackbarDataProvider::class) message: TextReference,
|
||||
) {
|
||||
TangemTheme(isDark = false) {
|
||||
CopiedTextSnackbar(message = message)
|
||||
}
|
||||
}
|
||||
|
||||
private class CopiedTextSnackbarDataProvider : CollectionPreviewParameterProvider<TextReference>(
|
||||
collection = listOf(
|
||||
stringReference(value = "Copied!"),
|
||||
stringReference(value = "Contract address copied!"),
|
||||
stringReference(value = "Coooooooooooontract addreeeeeeeeeeeeeeeess coooooooooooooooopied!"),
|
||||
),
|
||||
)
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package com.tangem.core.ui.components.snackbar
|
||||
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
/**
|
||||
* SnackbarHost to inform the user about copying text to the clipboard
|
||||
* Based on Material3 component. It's best way to show [CopiedTextSnackbar].
|
||||
*
|
||||
* @param hostState snackbar host state
|
||||
* @param modifier modifier
|
||||
*
|
||||
* @see <a href = https://www.figma.com/design/14ISV23YB1yVW1uNVwqrKv/Android?node-id=2001-728&t=kDzSZDx0m0sk4iYz-4
|
||||
* >Figma</a>
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
fun CopiedTextSnackbarHost(hostState: SnackbarHostState, modifier: Modifier = Modifier) {
|
||||
SnackbarHost(hostState = hostState, modifier = modifier) {
|
||||
CopiedTextSnackbar(snackbarData = it)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
package com.tangem.core.ui.components.snackbar
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
|
||||
/**
|
||||
* Tangem snackbar.
|
||||
* Based on Material3 component. It can be presented as one line or multi lines snackbar – depends on text length.
|
||||
*
|
||||
* @param data snackbar data
|
||||
* @param modifier modifier
|
||||
* @param actionOnNewLine flag that indicates if the action should be displayed on a new line (default: false)
|
||||
*
|
||||
* @see <a href = https://www.figma.com/design/14ISV23YB1yVW1uNVwqrKv/Android?node-id=682-761&t=kDzSZDx0m0sk4iYz-4
|
||||
* >Figma</a>
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
fun TangemSnackbar(data: SnackbarData, modifier: Modifier = Modifier, actionOnNewLine: Boolean = false) {
|
||||
Snackbar(
|
||||
modifier = modifier,
|
||||
action = {
|
||||
ActionButton(label = data.visuals.actionLabel, onClick = data::performAction)
|
||||
},
|
||||
actionOnNewLine = actionOnNewLine,
|
||||
shape = TangemTheme.shapes.roundedCorners8,
|
||||
containerColor = TangemTheme.colors.icon.secondary,
|
||||
) {
|
||||
MessageText(text = data.visuals.message)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ActionButton(label: String?, onClick: () -> Unit) {
|
||||
if (!label.isNullOrBlank()) {
|
||||
TextButton(
|
||||
onClick = onClick,
|
||||
colors = ButtonDefaults.textButtonColors(
|
||||
contentColor = TangemTheme.colors.text.primary2,
|
||||
),
|
||||
content = {
|
||||
Text(
|
||||
text = label,
|
||||
maxLines = 1,
|
||||
style = TangemTheme.typography.button,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MessageText(text: String) {
|
||||
Text(
|
||||
text = text,
|
||||
color = TangemTheme.colors.text.disabled,
|
||||
textAlign = TextAlign.Start,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = TangemTheme.typography.body2,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* IMPORTANT!
|
||||
* Preview doesn't work correctly, check on device or start [TangemSnackbarHost]'s preview in interactive mode *
|
||||
*/
|
||||
@Preview(widthDp = 344, showBackground = true, fontScale = 1f)
|
||||
@Preview(widthDp = 344, showBackground = true, fontScale = 1f, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Preview(widthDp = 344, showBackground = true, fontScale = 2f)
|
||||
@Composable
|
||||
private fun Preview_TangemSnackbar(@PreviewParameter(TangemSnackbarModelProvider::class) model: TangemSnackbarModel) {
|
||||
TangemThemePreview {
|
||||
TangemSnackbar(data = model.snackbarData, actionOnNewLine = model.actionOnNewLine)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
package com.tangem.core.ui.components.snackbar
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
|
||||
/**
|
||||
* Tangem snackbar host.
|
||||
* Based on Material3 component. It's best way to show [TangemSnackbar].
|
||||
*
|
||||
* @param hostState snackbar host state
|
||||
* @param modifier modifier
|
||||
* @param actionOnNewLine flag that indicates if the action should be displayed on a new line (default: false)
|
||||
*
|
||||
* @see <a href = https://www.figma.com/design/14ISV23YB1yVW1uNVwqrKv/Android?node-id=682-761&t=kDzSZDx0m0sk4iYz-4
|
||||
* >Figma</a>
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
fun TangemSnackbarHost(hostState: SnackbarHostState, modifier: Modifier = Modifier, actionOnNewLine: Boolean = false) {
|
||||
SnackbarHost(hostState = hostState, modifier = modifier) { data ->
|
||||
TangemSnackbar(data = data, actionOnNewLine = actionOnNewLine)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(widthDp = 344, showBackground = true, fontScale = 1f)
|
||||
@Preview(widthDp = 344, showBackground = true, fontScale = 1f, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Preview(widthDp = 344, showBackground = true, fontScale = 2f)
|
||||
@Composable
|
||||
private fun Preview_TangemSnackbar(@PreviewParameter(TangemSnackbarModelProvider::class) model: TangemSnackbarModel) {
|
||||
TangemThemePreview {
|
||||
val snackbarHostState = remember(::SnackbarHostState)
|
||||
|
||||
TangemSnackbarHost(hostState = snackbarHostState, actionOnNewLine = model.actionOnNewLine)
|
||||
|
||||
LaunchedEffect(key1 = null) {
|
||||
snackbarHostState.showSnackbar(visuals = model.snackbarData.visuals)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
package com.tangem.core.ui.components.snackbar
|
||||
|
||||
import androidx.compose.material3.SnackbarData
|
||||
import androidx.compose.material3.SnackbarDuration
|
||||
import androidx.compose.material3.SnackbarVisuals
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
|
||||
internal data class TangemSnackbarModel(val snackbarData: SnackbarData, val actionOnNewLine: Boolean)
|
||||
|
||||
internal class TangemSnackbarModelProvider : CollectionPreviewParameterProvider<TangemSnackbarModel>(
|
||||
listOf(
|
||||
createTangemSnackbarModel(
|
||||
message = "Single-line description.",
|
||||
actionLabel = "Button",
|
||||
actionOnNewLine = false,
|
||||
),
|
||||
createTangemSnackbarModel(
|
||||
message = "Very loooooooooooong single-line description.",
|
||||
actionLabel = "Button",
|
||||
actionOnNewLine = false,
|
||||
),
|
||||
createTangemSnackbarModel(
|
||||
message = "Single-line description.",
|
||||
actionLabel = "Very looooooong button name",
|
||||
actionOnNewLine = false,
|
||||
),
|
||||
createTangemSnackbarModel(
|
||||
message = "Single-line description.",
|
||||
actionLabel = "Button",
|
||||
actionOnNewLine = true,
|
||||
),
|
||||
createTangemSnackbarModel(
|
||||
message = "Very loooooooooooong single-line description.",
|
||||
actionLabel = "Button",
|
||||
actionOnNewLine = true,
|
||||
),
|
||||
createTangemSnackbarModel(
|
||||
message = "Single-line description.",
|
||||
actionLabel = "Very looooooong button name",
|
||||
actionOnNewLine = true,
|
||||
),
|
||||
),
|
||||
) {
|
||||
|
||||
companion object {
|
||||
|
||||
fun createTangemSnackbarModel(
|
||||
message: String,
|
||||
actionLabel: String,
|
||||
actionOnNewLine: Boolean,
|
||||
): TangemSnackbarModel {
|
||||
return TangemSnackbarModel(
|
||||
snackbarData = createSnackbarData(message, actionLabel),
|
||||
actionOnNewLine = actionOnNewLine,
|
||||
)
|
||||
}
|
||||
|
||||
private fun createSnackbarData(message: String, actionLabel: String): SnackbarData {
|
||||
return object : SnackbarData {
|
||||
|
||||
override val visuals: SnackbarVisuals
|
||||
get() = object : SnackbarVisuals {
|
||||
override val message: String = message
|
||||
override val actionLabel: String = actionLabel
|
||||
override val duration: SnackbarDuration = SnackbarDuration.Short // Never-mind
|
||||
override val withDismissAction: Boolean = false // Never-mind
|
||||
}
|
||||
|
||||
override fun dismiss() = Unit
|
||||
override fun performAction() = Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
package com.tangem.core.ui.extensions
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import org.intellij.markdown.MarkdownElementTypes
|
||||
import org.intellij.markdown.ast.ASTNode
|
||||
import org.intellij.markdown.ast.getTextInNode
|
||||
import org.intellij.markdown.flavours.commonmark.CommonMarkFlavourDescriptor
|
||||
import org.intellij.markdown.parser.MarkdownParser
|
||||
|
||||
/** Markdown parser */
|
||||
@Composable
|
||||
fun rememberMarkdownParser() = remember {
|
||||
MarkdownParser(CommonMarkFlavourDescriptor())
|
||||
}
|
||||
|
||||
/**
|
||||
* Styling markdown tree recursively
|
||||
*
|
||||
* @param markdownText original text
|
||||
* @param node current processed node
|
||||
*/
|
||||
@Composable
|
||||
fun AnnotatedString.Builder.appendMarkdown(markdownText: String, node: ASTNode): AnnotatedString.Builder {
|
||||
when (node.type) {
|
||||
MarkdownElementTypes.MARKDOWN_FILE, MarkdownElementTypes.PARAGRAPH -> {
|
||||
node.children.forEach { childNode ->
|
||||
appendMarkdown(
|
||||
markdownText = markdownText,
|
||||
node = childNode,
|
||||
)
|
||||
}
|
||||
}
|
||||
MarkdownElementTypes.STRONG -> {
|
||||
withStyle(SpanStyle(fontWeight = FontWeight.Medium)) {
|
||||
node.children
|
||||
.drop(2)
|
||||
.dropLast(2)
|
||||
.forEach { childNode ->
|
||||
appendMarkdown(
|
||||
markdownText = markdownText,
|
||||
node = childNode,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
append(node.getTextInNode(markdownText).toString())
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
|
@ -8,6 +8,9 @@ import androidx.compose.runtime.Immutable
|
|||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import org.intellij.markdown.MarkdownElementTypes
|
||||
|
||||
/**
|
||||
* Utility class for creating text as [String] or [StringRes].
|
||||
|
|
@ -160,6 +163,29 @@ fun TextReference.resolveReference(resources: Resources): String {
|
|||
}
|
||||
}
|
||||
|
||||
/** Resolve [TextReference] as [AnnotatedString] */
|
||||
@Composable
|
||||
fun TextReference.resolveAnnotatedReference(): AnnotatedString {
|
||||
return when (this) {
|
||||
is TextReference.Res -> {
|
||||
val args = formatArgs
|
||||
.map { if (it is TextReference) it.resolveReference() else it }
|
||||
.toTypedArray()
|
||||
|
||||
formatAnnotated(stringResource(id = id, *args))
|
||||
}
|
||||
is TextReference.PluralRes -> formatAnnotated(
|
||||
pluralStringResource(id, count, *formatArgs.toTypedArray()),
|
||||
)
|
||||
is TextReference.Str -> formatAnnotated(value)
|
||||
is TextReference.Combined -> buildAnnotatedString {
|
||||
refs.forEach {
|
||||
append(formatAnnotated(it.resolveReference()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Concatenate [this] reference with [ref] */
|
||||
operator fun TextReference.plus(ref: TextReference): TextReference {
|
||||
return when (this) {
|
||||
|
|
@ -169,4 +195,14 @@ operator fun TextReference.plus(ref: TextReference): TextReference {
|
|||
is TextReference.Str,
|
||||
-> TextReference.Combined(refs = wrappedList(this, ref))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun formatAnnotated(rawString: String): AnnotatedString {
|
||||
val markdownDescriptor = rememberMarkdownParser()
|
||||
val parsedTree = markdownDescriptor.parse(MarkdownElementTypes.MARKDOWN_FILE, rawString, true)
|
||||
|
||||
return buildAnnotatedString {
|
||||
appendMarkdown(markdownText = rawString, node = parsedTree)
|
||||
}
|
||||
}
|
||||
|
|
@ -8,9 +8,8 @@ import androidx.compose.runtime.ReadOnlyComposable
|
|||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import com.tangem.core.ui.haptic.HapticManager
|
||||
import com.tangem.core.ui.UiDependencies
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.theme.AppThemeModeHolder
|
||||
import com.tangem.domain.apptheme.model.AppThemeMode
|
||||
|
||||
/**
|
||||
|
|
@ -23,14 +22,9 @@ import com.tangem.domain.apptheme.model.AppThemeMode
|
|||
internal interface ComposeScreen {
|
||||
|
||||
/**
|
||||
* The holder for managing the current application theme mode.
|
||||
* The holder for ui dependencies.
|
||||
*/
|
||||
val appThemeModeHolder: AppThemeModeHolder
|
||||
|
||||
/**
|
||||
* Haptic manager.
|
||||
*/
|
||||
val hapticManager: HapticManager
|
||||
val uiDependencies: UiDependencies
|
||||
|
||||
/**
|
||||
* The screen modifier.
|
||||
|
|
@ -59,11 +53,11 @@ internal interface ComposeScreen {
|
|||
internal fun ComposeScreen.createComposeView(context: Context): ComposeView {
|
||||
return ComposeView(context).apply {
|
||||
setContent {
|
||||
val appThemeMode by appThemeModeHolder.appThemeMode
|
||||
val appThemeMode by uiDependencies.appThemeModeHolder.appThemeMode
|
||||
|
||||
TangemTheme(
|
||||
isDark = shouldUseDarkTheme(appThemeMode),
|
||||
hapticManager = hapticManager,
|
||||
hapticManager = uiDependencies.hapticManager,
|
||||
) {
|
||||
ScreenContent(modifier = screenModifier)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.core.ui.utils
|
||||
|
||||
import android.text.format.DateFormat
|
||||
import org.joda.time.DateTime
|
||||
import org.joda.time.format.DateTimeFormat
|
||||
import org.joda.time.format.DateTimeFormatter
|
||||
|
|
@ -55,6 +56,16 @@ object DateTimeFormatters {
|
|||
.withLocale(Locale.getDefault())
|
||||
}
|
||||
|
||||
/**
|
||||
* In API version < 24, there may be some problems with getting the best date and time format pattern.
|
||||
*/
|
||||
val dateMMMMd: DateTimeFormatter by lazy {
|
||||
DateTimeFormatterBuilder()
|
||||
.appendPattern(DateFormat.getBestDateTimePattern(Locale.getDefault(), "MMMM d"))
|
||||
.toFormatter()
|
||||
.withLocale(Locale.getDefault())
|
||||
}
|
||||
|
||||
val dateTimeFormatter: DateTimeFormatter by lazy {
|
||||
DateTimeFormat.forPattern("dd.MM.yyyy HH:mm")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,9 +10,9 @@ import java.text.DecimalFormatSymbols
|
|||
import java.util.Locale
|
||||
|
||||
private const val TEXT_CHUNK_THOUSAND = 3
|
||||
private const val POINT_SEPARATOR = '.'
|
||||
private const val COMMA_SEPARATOR = ','
|
||||
private const val SCIENTIFIC_NOTATION = 'e'
|
||||
const val POINT_SEPARATOR = '.'
|
||||
const val COMMA_SEPARATOR = ','
|
||||
const val DECIMAL_SEPARATOR_LIMIT = 1
|
||||
|
||||
@Composable
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 441 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 409 KiB |
Loading…
Add table
Add a link
Reference in a new issue