Updated on 2026-08-14

This commit is contained in:
Tangem 2024-08-07 15:08:26 +03:00
commit 2a5e3fa7eb
220 changed files with 5932 additions and 1815 deletions

View file

@ -3,6 +3,7 @@ package com.tangem.core.ui.components
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.ime
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.platform.LocalDensity
@ -14,11 +15,15 @@ sealed interface Keyboard {
data class Opened(override val height: Dp) : Keyboard
object Closed : Keyboard {
data object Closed : Keyboard {
override val height: Dp = 0.dp
}
}
val Keyboard.isOpened: Boolean
@Stable
get() = this is Keyboard.Opened
/**
* Allows to subscribe to a soft keyboard to detect when it's open/closed
*/

View file

@ -10,6 +10,7 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.res.LocalBottomSheetAlwaysVisible
@ -114,6 +115,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> PreviewBottomSheet(
crossinline content: @Composable (ColumnScope.(T) -> Unit),
) {
BasicBottomSheet<T>(
modifier = Modifier.width(360.dp),
config = config,
sheetState = SheetState(
skipPartiallyExpanded = true,
@ -137,6 +139,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicBottomSheet(
addBottomInsets: Boolean,
crossinline title: @Composable (BoxScope.(T) -> Unit),
crossinline content: @Composable (ColumnScope.(T) -> Unit),
modifier: Modifier = Modifier,
) {
val model = config.content as? T ?: return
@ -145,7 +148,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicBottomSheet(
ModalBottomSheet(
// FIXME temporary solution to fix height of the bottom sheet
modifier = Modifier.sizeIn(maxHeight = LocalWindowSize.current.height - statusBarHeight),
modifier = modifier.heightIn(max = LocalWindowSize.current.height - statusBarHeight),
onDismissRequest = config.onDismissRequest,
sheetState = sheetState,
containerColor = containerColor,

View file

@ -11,7 +11,7 @@ sealed interface TangemButtonIconPosition {
data class End(@DrawableRes override val iconResId: Int) : TangemButtonIconPosition
object None : TangemButtonIconPosition {
data object None : TangemButtonIconPosition {
@DrawableRes
override val iconResId: Int? = null
}

View file

@ -1,12 +1,11 @@
package com.tangem.core.ui.components.fields
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
import androidx.compose.foundation.text.selection.TextSelectionColors
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
@ -40,6 +39,7 @@ fun SimpleTextField(
textStyle: TextStyle = TangemTheme.typography.body2.copy(color = color),
placeholderColor: Color = TangemTheme.colors.text.disabled,
readOnly: Boolean = false,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
isValuePasted: Boolean = false,
onValuePastedTriggerDismiss: () -> Unit = {},
decorationBox: (@Composable (innerTextField: @Composable () -> Unit) -> Unit)? = null,
@ -54,10 +54,6 @@ fun SimpleTextField(
)
}
val focusRequester = remember { FocusRequester.Default }
val customTextSelectionColors = TextSelectionColors(
handleColor = TangemTheme.colors.text.accent,
backgroundColor = TangemTheme.colors.text.accent.copy(alpha = 0.3f),
)
val textFieldValue = textFieldValueState.copy(text = value)
var lastTextValue by remember(proxyValue, isValuePasted) {
textFieldValueState = textFieldValueState.copy(
@ -85,37 +81,36 @@ fun SimpleTextField(
}
}
CompositionLocalProvider(LocalTextSelectionColors provides customTextSelectionColors) {
BasicTextField(
value = textFieldValue,
onValueChange = { newTextFieldValueState ->
textFieldValueState = newTextFieldValueState
BasicTextField(
value = textFieldValue,
onValueChange = { newTextFieldValueState ->
textFieldValueState = newTextFieldValueState
val stringChangedSinceLastInvocation = lastTextValue != newTextFieldValueState.text
lastTextValue = newTextFieldValueState.text
val stringChangedSinceLastInvocation = lastTextValue != newTextFieldValueState.text
lastTextValue = newTextFieldValueState.text
if (stringChangedSinceLastInvocation) onValueChange(newTextFieldValueState.text)
},
textStyle = textStyle.copy(color = color),
cursorBrush = SolidColor(TangemTheme.colors.text.primary1),
singleLine = singleLine,
readOnly = readOnly,
visualTransformation = visualTransformation,
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
decorationBox = decorationBox ?: { textValue ->
SimpleTextPlaceholder(
placeholder = placeholder,
value = value,
textStyle = textStyle,
textValue = textValue,
color = placeholderColor,
)
},
modifier = modifier
.focusRequester(focusRequester),
)
}
if (stringChangedSinceLastInvocation) onValueChange(newTextFieldValueState.text)
},
textStyle = textStyle.copy(color = color),
cursorBrush = SolidColor(TangemTheme.colors.text.primary1),
singleLine = singleLine,
readOnly = readOnly,
visualTransformation = visualTransformation,
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
interactionSource = interactionSource,
decorationBox = decorationBox ?: { textValue ->
SimpleTextPlaceholder(
placeholder = placeholder,
value = value,
textStyle = textStyle,
textValue = textValue,
color = placeholderColor,
)
},
modifier = modifier
.focusRequester(focusRequester),
)
}
@Composable

View file

@ -72,7 +72,7 @@ fun InputRowEnter(
Column(modifier = Modifier.weight(1f)) {
Text(
text = title.resolveReference(),
style = TangemTheme.typography.caption2,
style = TangemTheme.typography.subtitle2,
color = titleColor,
)
SimpleTextField(

View file

@ -72,7 +72,7 @@ fun InputRowEnterAmount(
Column(modifier = Modifier.weight(1f)) {
Text(
text = title.resolveReference(),
style = TangemTheme.typography.caption2,
style = TangemTheme.typography.subtitle2,
color = titleColor,
)
AmountTextField(

View file

@ -62,7 +62,7 @@ fun InputRowEnterInfo(
) {
Text(
text = title.resolveReference(),
style = TangemTheme.typography.caption2,
style = TangemTheme.typography.subtitle2,
color = titleColor,
)
Row {

View file

@ -70,7 +70,7 @@ fun InputRowEnterInfoAmount(
) {
Text(
text = title.resolveReference(),
style = TangemTheme.typography.caption2,
style = TangemTheme.typography.subtitle2,
color = titleColor,
)
Row {

View file

@ -70,7 +70,7 @@ fun InputRowImage(
) {
Text(
text = title.resolveReference(),
style = TangemTheme.typography.caption2,
style = TangemTheme.typography.subtitle2,
color = titleColor,
)
Row(

View file

@ -15,7 +15,7 @@ import com.tangem.core.ui.res.TangemTheme
@Composable
internal fun InputRowImageBase(
subtitle: TextReference,
caption: TextReference,
caption: TextReference?,
imageUrl: String,
modifier: Modifier = Modifier,
subtitleColor: Color = TangemTheme.colors.text.primary1,
@ -40,12 +40,14 @@ internal fun InputRowImageBase(
style = TangemTheme.typography.subtitle2,
color = subtitleColor,
)
Text(
text = caption.resolveAnnotatedReference(),
style = TangemTheme.typography.caption2,
color = captionColor,
modifier = Modifier.padding(top = TangemTheme.dimens.spacing2),
)
if (caption != null) {
Text(
text = caption.resolveAnnotatedReference(),
style = TangemTheme.typography.caption2,
color = captionColor,
modifier = Modifier.padding(top = TangemTheme.dimens.spacing2),
)
}
}
extraContent()
}

View file

@ -75,7 +75,7 @@ fun InputRowRecipient(
AnimatedContent(targetState = titleText, label = "Title Change") {
Text(
text = it.resolveReference(),
style = TangemTheme.typography.caption2,
style = TangemTheme.typography.subtitle2,
color = color,
)
}

View file

@ -50,7 +50,7 @@ fun InputRowRecipientDefault(
) {
Text(
text = title.resolveReference(),
style = TangemTheme.typography.caption2,
style = TangemTheme.typography.subtitle2,
color = titleColor,
)
Row(

View file

@ -4,6 +4,7 @@ import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@ -15,24 +16,57 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
private const val ROUNDED_LIST_WITH_DIVIDERS_HEADER_KEY = "ROUNDED_LIST_WITH_DIVIDERS_HEADER_KEY"
private const val ROUNDED_LIST_WITH_DIVIDERS_FOOTER_KEY = "ROUNDED_LIST_WITH_DIVIDERS_FOOTER_KEY"
@Composable
fun RoundedListWithDividers(rows: List<RoundedListWithDividersItemData>, modifier: Modifier = Modifier) {
fun RoundedListWithDividers(
rows: ImmutableList<RoundedListWithDividersItemData>,
modifier: Modifier = Modifier,
headerContent: (@Composable () -> Unit)? = null,
footerContent: (@Composable () -> Unit)? = null,
) {
LazyColumn(modifier = modifier) {
itemsIndexed(
items = rows,
key = { _, item -> item.id },
) { index, row ->
InitialInfoContentRow(
startText = row.startText.resolveReference(),
endText = row.endText.resolveReference(),
cornersToRound = getCornersToRound(index, rows.size),
iconClick = row.iconClick,
)
if (index < rows.lastIndex) {
RoundedListDivider()
}
this.roundedListWithDividersItems(
rows = rows,
headerContent = headerContent,
footerContent = footerContent,
)
}
}
fun LazyListScope.roundedListWithDividersItems(
rows: ImmutableList<RoundedListWithDividersItemData>,
headerContent: (@Composable () -> Unit)? = null,
footerContent: (@Composable () -> Unit)? = null,
) {
if (headerContent != null) {
item(key = ROUNDED_LIST_WITH_DIVIDERS_HEADER_KEY) {
headerContent()
}
}
itemsIndexed(
items = rows,
key = { _, item -> item.id },
) { index, row ->
InitialInfoContentRow(
startText = row.startText.resolveReference(),
endText = row.endText.resolveReference(),
cornersToRound = getCornersToRound(index, rows.size),
iconClick = row.iconClick,
)
if (index < rows.lastIndex) {
RoundedListDivider()
}
}
if (footerContent != null) {
item(key = ROUNDED_LIST_WITH_DIVIDERS_FOOTER_KEY) {
footerContent()
}
}
}

View file

@ -3,7 +3,7 @@ package com.tangem.core.ui.extensions
import androidx.annotation.DrawableRes
import com.tangem.core.ui.R
@Suppress("ComplexMethod")
@Suppress("ComplexMethod", "LongMethod")
@DrawableRes
fun getActiveIconRes(blockchainId: String): Int {
return when (blockchainId) {
@ -70,11 +70,13 @@ fun getActiveIconRes(blockchainId: String): Int {
"joystream" -> R.drawable.img_joystream_22
"koinos", "koinos/test" -> R.drawable.img_koinos_22
"bittensor" -> R.drawable.img_bittensor_22
"blast", "blast/test" -> R.drawable.img_blast_22
"filecoin" -> R.drawable.img_filecoin_22
else -> R.drawable.ic_alert_24
}
}
@Suppress("ComplexMethod")
@Suppress("ComplexMethod", "LongMethod")
@DrawableRes
fun getActiveIconResByNetworkId(networkId: String): Int {
return when (networkId) {
@ -141,6 +143,8 @@ fun getActiveIconResByNetworkId(networkId: String): Int {
"joystream" -> R.drawable.img_joystream_22
"koinos", "koinos/test" -> R.drawable.img_koinos_22
"bittensor" -> R.drawable.img_bittensor_22
"blast", "blast/test" -> R.drawable.img_blast_22
"filecoin" -> R.drawable.img_filecoin_22
else -> R.drawable.ic_alert_24
}
}
@ -209,11 +213,13 @@ fun getActiveIconResByCoinId(coinId: String): Int {
"joystream" -> R.drawable.img_joystream_22
"koinos", "koinos/test" -> R.drawable.img_koinos_22
"bittensor" -> R.drawable.img_bittensor_22
"blast", "blast/test" -> R.drawable.img_blast_22
"filecoin" -> R.drawable.img_filecoin_22
else -> R.drawable.ic_alert_24
}
}
@Suppress("ComplexMethod")
@Suppress("ComplexMethod", "LongMethod")
@DrawableRes
fun getGreyedOutIconRes(blockchainId: String): Int {
return when (blockchainId) {
@ -280,11 +286,13 @@ fun getGreyedOutIconRes(blockchainId: String): Int {
"joystream" -> R.drawable.ic_joystream_22
"koinos", "koinos/test" -> R.drawable.ic_koinos_22
"bittensor" -> R.drawable.ic_bittensor_22
"blast", "blast/test" -> R.drawable.ic_blast_22
"filecoin" -> R.drawable.ic_filecoin_22
else -> R.drawable.ic_alert_24
}
}
@Suppress("ComplexMethod")
@Suppress("ComplexMethod", "LongMethod")
@DrawableRes
fun getGreyedOutIconResByNetworkId(networkId: String): Int {
return when (networkId) {
@ -351,6 +359,8 @@ fun getGreyedOutIconResByNetworkId(networkId: String): Int {
"joystream" -> R.drawable.ic_joystream_22
"koinos", "koinos/test" -> R.drawable.ic_koinos_22
"bittensor" -> R.drawable.ic_bittensor_22
"blast", "blast/test" -> R.drawable.ic_blast_22
"filecoin" -> R.drawable.ic_filecoin_22
else -> R.drawable.ic_alert_24
}
}

View file

@ -52,10 +52,11 @@ fun TangemTheme(
LocalHapticManager provides hapticManager,
LocalSnackbarHostState provides snackbarHostState,
LocalWindowSize provides windowSize,
LocalTextSelectionColors provides TangemTextSelectionColors,
) {
CompositionLocalProvider(
LocalTangemShimmer provides TangemShimmer,
LocalMainBottomSheetColor provides remember { mutableStateOf(Color.Unspecified) },
LocalTextSelectionColors provides TangemTextSelectionColors,
) {
ProvideTextStyle(
value = TangemTheme.typography.body1,
@ -208,11 +209,13 @@ private fun darkThemeColors(): TangemColors {
)
}
@Stable
private val TangemTextSelectionColors = TextSelectionColors(
handleColor = TangemColorPalette.Azure,
backgroundColor = TangemColorPalette.Azure.copy(alpha = 0.4f),
)
private val TangemTextSelectionColors: TextSelectionColors
@Composable
@ReadOnlyComposable
get() = TextSelectionColors(
handleColor = TangemTheme.colors.text.accent,
backgroundColor = TangemTheme.colors.text.accent.copy(alpha = 0.3f),
)
private val LocalTangemColors = staticCompositionLocalOf<TangemColors> {
error("No TangemColors provided")
@ -246,4 +249,8 @@ val LocalWindowSize = staticCompositionLocalOf<WindowSize> {
val LocalTangemShimmer = staticCompositionLocalOf<Shimmer> {
error("No TangemShimmer provided")
}
val LocalMainBottomSheetColor = staticCompositionLocalOf<MutableState<Color>> {
error("No MainBottomSheetColor provided")
}

View file

@ -61,7 +61,14 @@ object DateTimeFormatters {
*/
val dateMMMMd: DateTimeFormatter by lazy {
DateTimeFormatterBuilder()
.appendPattern(DateFormat.getBestDateTimePattern(Locale.getDefault(), "MMMM d"))
.appendPattern(DateFormat.getBestDateTimePattern(Locale.getDefault(), "dd MMM"))
.toFormatter()
.withLocale(Locale.getDefault())
}
val dateYYYY: DateTimeFormatter by lazy {
DateTimeFormatterBuilder()
.appendPattern(DateFormat.getBestDateTimePattern(Locale.getDefault(), "yyyy"))
.toFormatter()
.withLocale(Locale.getDefault())
}

View file

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="22dp"
android:viewportWidth="22"
android:viewportHeight="22">
<path
android:fillColor="#000000"
android:pathData="M15.257,11.048L17.71,9.826L18.555,7.231L16.864,6H5.604L3,7.934H16.236L15.533,10.111H10.225L9.714,11.701H15.022L13.532,16.281L16.019,15.05L16.906,12.304L15.24,11.082L15.257,11.048Z" />
<path
android:fillColor="#000000"
android:pathData="M6.742,14.313L8.275,9.541L6.575,8.269L4.021,16.281H13.532L14.168,14.313H6.742Z" />
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="22dp"
android:viewportWidth="22"
android:viewportHeight="22">
<path
android:pathData="M12.045,9.68L11.715,11.44L14.85,11.88L14.63,12.705L11.55,12.265C11.33,12.98 11.22,13.75 10.945,14.41C10.67,15.18 10.395,15.95 10.065,16.665C9.625,17.6 8.855,18.26 7.81,18.425C7.205,18.535 6.545,18.48 6.05,18.095C5.885,17.985 5.72,17.765 5.72,17.6C5.72,17.38 5.83,17.105 5.995,16.995C6.105,16.94 6.38,16.995 6.545,17.05C6.71,17.215 6.875,17.435 6.985,17.655C7.315,18.095 7.755,18.15 8.195,17.82C8.69,17.38 8.965,16.775 9.13,16.17C9.46,14.85 9.79,13.585 10.065,12.265V12.045L7.15,11.605L7.26,10.78L10.285,11.22L10.67,9.515L7.535,9.02L7.645,8.14L10.89,8.58C11,8.25 11.055,7.975 11.165,7.7C11.44,6.71 11.715,5.72 12.375,4.84C13.035,3.96 13.805,3.41 14.96,3.465C15.455,3.465 15.95,3.63 16.28,4.015C16.335,4.07 16.445,4.18 16.445,4.29C16.445,4.51 16.445,4.785 16.28,4.95C16.06,5.115 15.785,5.06 15.565,4.84C15.4,4.675 15.29,4.51 15.125,4.345C14.795,3.905 14.3,3.85 13.915,4.235C13.64,4.51 13.365,4.895 13.2,5.28C12.815,6.435 12.54,7.645 12.155,8.855L15.18,9.295L14.96,10.12L12.045,9.68Z"
android:fillColor="#000000"
android:fillType="evenOdd" />
</vector>

View file

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="22dp"
android:viewportWidth="22"
android:viewportHeight="22">
<path
android:fillColor="#000000"
android:pathData="M11,0L11,0A11,11 0,0 1,22 11L22,11A11,11 0,0 1,11 22L11,22A11,11 0,0 1,0 11L0,11A11,11 0,0 1,11 0z" />
<path
android:fillColor="#FCFC03"
android:pathData="M15.257,11.048L17.71,9.826L18.555,7.231L16.864,6H5.604L3,7.934H16.236L15.533,10.111H10.225L9.714,11.701H15.022L13.532,16.281L16.019,15.05L16.906,12.304L15.24,11.082L15.257,11.048Z" />
<path
android:fillColor="#FCFC03"
android:pathData="M6.742,14.313L8.275,9.541L6.575,8.269L4.021,16.281H13.532L14.168,14.313H6.742Z" />
</vector>

View file

@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="22dp"
android:viewportWidth="22"
android:viewportHeight="22">
<group>
<clip-path android:pathData="M11,0L11,0A11,11 0,0 1,22 11L22,11A11,11 0,0 1,11 22L11,22A11,11 0,0 1,0 11L0,11A11,11 0,0 1,11 0z" />
<path
android:pathData="M11,0L11,0A11,11 0,0 1,22 11L22,11A11,11 0,0 1,11 22L11,22A11,11 0,0 1,0 11L0,11A11,11 0,0 1,11 0z"
android:fillColor="#0090FF" />
<path
android:pathData="M12.045,9.68L11.715,11.44L14.85,11.88L14.63,12.705L11.55,12.265C11.33,12.98 11.22,13.75 10.945,14.41C10.67,15.18 10.395,15.95 10.065,16.665C9.625,17.6 8.855,18.26 7.81,18.425C7.205,18.535 6.545,18.48 6.05,18.095C5.885,17.985 5.72,17.765 5.72,17.6C5.72,17.38 5.83,17.105 5.995,16.995C6.105,16.94 6.38,16.995 6.545,17.05C6.71,17.215 6.875,17.435 6.985,17.655C7.315,18.095 7.755,18.15 8.195,17.82C8.69,17.38 8.965,16.775 9.13,16.17C9.46,14.85 9.79,13.585 10.065,12.265V12.045L7.15,11.605L7.26,10.78L10.285,11.22L10.67,9.515L7.535,9.02L7.645,8.14L10.89,8.58C11,8.25 11.055,7.975 11.165,7.7C11.44,6.71 11.715,5.72 12.375,4.84C13.035,3.96 13.805,3.41 14.96,3.465C15.455,3.465 15.95,3.63 16.28,4.015C16.335,4.07 16.445,4.18 16.445,4.29C16.445,4.51 16.445,4.785 16.28,4.95C16.06,5.115 15.785,5.06 15.565,4.84C15.4,4.675 15.29,4.51 15.125,4.345C14.795,3.905 14.3,3.85 13.915,4.235C13.64,4.51 13.365,4.895 13.2,5.28C12.815,6.435 12.54,7.645 12.155,8.855L15.18,9.295L14.96,10.12L12.045,9.68Z"
android:fillColor="#ffffff"
android:fillType="evenOdd" />
</group>
</vector>