Updated on 2026-08-14

This commit is contained in:
Tangem 2024-05-17 14:05:26 +05:00
commit 63849d3c02
155 changed files with 2317 additions and 607 deletions

View 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
}

View file

@ -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

View file

@ -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,
)
}
}

View file

@ -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,
),
) {

View file

@ -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()

View file

@ -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

View file

@ -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!"),
),
)

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}
}

View file

@ -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
}
}
}
}

View file

@ -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
}

View file

@ -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)
}
}

View file

@ -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)
}

View file

@ -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")
}

View file

@ -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