Updated on 2026-08-14

This commit is contained in:
Tangem 2023-09-11 11:07:03 +03:00
commit f0d03e3648
286 changed files with 6353 additions and 5898 deletions

View file

@ -1,44 +1,33 @@
package com.tangem.core.ui.components
import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material.RadioButton
import androidx.compose.material.RadioButtonDefaults
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SelctorDialogParamsProvider.SelectorDialogParams
import com.tangem.core.ui.res.TangemTheme
/**
* Dialog button params
*
* @param title Button text. If not provided default values will be used
* @param warning If true then button text will be in theme warning color
* @param enabled If false button will be disabled
* @param onClick Button click callback
*/
data class DialogButton(
val title: String? = null,
val warning: Boolean = false,
val enabled: Boolean = true,
val onClick: () -> Unit,
)
/**
* Additional params for dialog text field
*/
data class AdditionalTextInputDialogParams(
val label: String? = null,
val placeholder: String? = null,
val caption: String? = null,
val enabled: Boolean = true,
val isError: Boolean = false,
)
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
/**
* Simple alert dialog with a message and 'OK' button
@ -129,6 +118,54 @@ fun TextInputDialog(
)
}
@Composable
fun SelectorDialog(
selectedItemIndex: Int,
items: ImmutableList<String>,
confirmButton: DialogButton,
onSelect: (index: Int) -> Unit,
onDismissDialog: () -> Unit,
title: String? = null,
isDismissable: Boolean = true,
) {
TangemDialog(
type = DialogType.Selector(selectedItemIndex, items, onSelect),
confirmButton = confirmButton,
title = title,
onDismissDialog = onDismissDialog,
properties = DialogProperties(
dismissOnBackPress = isDismissable,
dismissOnClickOutside = isDismissable,
),
)
}
/**
* Dialog button params
*
* @param title Button text. If not provided default values will be used
* @param warning If true then button text will be in theme warning color
* @param enabled If false button will be disabled
* @param onClick Button click callback
*/
data class DialogButton(
val title: String? = null,
val warning: Boolean = false,
val enabled: Boolean = true,
val onClick: () -> Unit,
)
/**
* Additional params for dialog text field
*/
data class AdditionalTextInputDialogParams(
val label: String? = null,
val placeholder: String? = null,
val caption: String? = null,
val enabled: Boolean = true,
val isError: Boolean = false,
)
// region Defaults
@Composable
private fun TangemDialog(
@ -146,15 +183,18 @@ private fun TangemDialog(
shape = TangemTheme.shapes.roundedCornersLarge,
color = TangemTheme.colors.background.plain,
)
.padding(all = TangemTheme.dimens.spacing24),
.padding(vertical = TangemTheme.dimens.spacing24),
) {
if (title != null) {
Text(
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing24)
.fillMaxWidth(),
text = title,
style = when (type) {
is DialogType.Message -> TangemTheme.typography.h2
is DialogType.TextInput -> TangemTheme.typography.h3
is DialogType.Selector -> TangemTheme.typography.h2
},
color = TangemTheme.colors.text.primary1,
)
@ -162,7 +202,13 @@ private fun TangemDialog(
}
DialogContent(type = type)
SpacerH24()
DialogButtons(confirmButton = confirmButton, dismissButton = dismissButton)
DialogButtons(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing24)
.fillMaxWidth(),
confirmButton = confirmButton,
dismissButton = dismissButton,
)
}
}
}
@ -177,7 +223,9 @@ private fun DialogContent(type: DialogType, modifier: Modifier = Modifier) {
when (type) {
is DialogType.Message -> {
Text(
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing24)
.fillMaxWidth(),
text = type.message,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
@ -185,7 +233,9 @@ private fun DialogContent(type: DialogType, modifier: Modifier = Modifier) {
}
is DialogType.TextInput -> {
OutlineTextField(
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing24)
.fillMaxWidth(),
value = type.value,
label = type.params.label,
placeholder = type.params.placeholder,
@ -197,6 +247,14 @@ private fun DialogContent(type: DialogType, modifier: Modifier = Modifier) {
},
)
}
is DialogType.Selector -> {
SelectorDialogContent(
modifier = Modifier.fillMaxWidth(),
selectedItemIndex = type.selectedItemIndex,
items = type.items,
onSelect = type.onSelect,
)
}
}
}
}
@ -204,7 +262,7 @@ private fun DialogContent(type: DialogType, modifier: Modifier = Modifier) {
@Composable
private fun DialogButtons(confirmButton: DialogButton, dismissButton: DialogButton?, modifier: Modifier = Modifier) {
Row(
modifier = modifier.fillMaxWidth(),
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(
space = TangemTheme.dimens.spacing4,
alignment = Alignment.End,
@ -252,14 +310,72 @@ private fun DialogButton(
}
}
private sealed interface DialogType {
data class Message(val message: String) : DialogType
@Composable
private fun SelectorDialogContent(
selectedItemIndex: Int,
items: ImmutableList<String>,
onSelect: (index: Int) -> Unit,
modifier: Modifier = Modifier,
) {
LazyColumn(modifier = modifier) {
itemsIndexed(items = items) { index, itemText ->
val onClick = remember(index) {
{ onSelect(index) }
}
val interactionSource = remember { MutableInteractionSource() }
Row(
modifier = Modifier
.clickable(
interactionSource = interactionSource,
indication = LocalIndication.current,
onClick = onClick,
)
.padding(
vertical = TangemTheme.dimens.spacing16,
horizontal = TangemTheme.dimens.spacing18,
)
.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16),
verticalAlignment = Alignment.CenterVertically,
) {
RadioButton(
modifier = Modifier.size(TangemTheme.dimens.size24),
selected = index == selectedItemIndex,
onClick = onClick,
colors = RadioButtonDefaults.colors(
selectedColor = TangemTheme.colors.icon.accent,
unselectedColor = TangemTheme.colors.icon.secondary,
),
interactionSource = interactionSource,
)
Text(
text = itemText,
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.primary1,
)
}
}
}
}
@Immutable
private sealed class DialogType {
data class Message(val message: String) : DialogType()
data class TextInput(
val value: TextFieldValue,
val onValueChange: (TextFieldValue) -> Unit,
val params: AdditionalTextInputDialogParams = AdditionalTextInputDialogParams(),
) : DialogType
) : DialogType()
data class Selector(
val selectedItemIndex: Int,
val items: ImmutableList<String>,
val onSelect: (index: Int) -> Unit,
) : DialogType()
}
// endregion Defaults
@ -381,4 +497,65 @@ private fun TextInputDialogPreview_Dark() {
TextInputDialogSample()
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun SelectorDialogPreview_Light(
@PreviewParameter(SelctorDialogParamsProvider::class) param: SelectorDialogParams,
) {
TangemTheme(isDark = false) {
SelectorDialog(
title = param.title,
items = param.items,
selectedItemIndex = param.selectedItemIndex,
confirmButton = DialogButton(title = "Cancel", onClick = {}),
onSelect = {},
onDismissDialog = {},
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun SelectorDialogPreview_Dark(
@PreviewParameter(SelctorDialogParamsProvider::class) param: SelectorDialogParams,
) {
TangemTheme(isDark = true) {
SelectorDialog(
title = param.title,
items = param.items,
selectedItemIndex = param.selectedItemIndex,
confirmButton = DialogButton(title = "Cancel", onClick = {}),
onSelect = {},
onDismissDialog = {},
)
}
}
private class SelctorDialogParamsProvider : CollectionPreviewParameterProvider<SelectorDialogParams>(
collection = listOf(
SelectorDialogParams(
title = "Theme",
selectedItemIndex = 0,
persistentListOf("Light", "Dark", "Follow system"),
),
SelectorDialogParams(
title = null,
selectedItemIndex = 2,
persistentListOf("Light", "Dark", "Follow system"),
),
SelectorDialogParams(
title = "Count",
selectedItemIndex = 8,
List(size = 10) { it.toString() }.toImmutableList(),
),
),
) {
data class SelectorDialogParams(
val title: String?,
val selectedItemIndex: Int,
val items: ImmutableList<String>,
)
}
// endregion Preview

View file

@ -20,7 +20,6 @@ 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.components.SpacerW8
import com.tangem.core.ui.components.buttons.common.*
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
@ -99,7 +98,7 @@ private fun Button(
val iconTint by animateColorAsState(
targetValue = when {
!config.enabled -> TangemTheme.colors.icon.informative
config.dimContent -> TangemTheme.colors.icon.secondary
config.dimContent -> TangemTheme.colors.icon.informative
else -> TangemTheme.colors.icon.primary1
},
label = "Update tint color",
@ -117,7 +116,7 @@ private fun Button(
val textColor by animateColorAsState(
targetValue = when {
!config.enabled -> TangemTheme.colors.text.disabled
config.dimContent -> TangemTheme.colors.text.secondary
config.dimContent -> TangemTheme.colors.text.tertiary
else -> TangemTheme.colors.text.primary1
},
label = "Update text color",

View file

@ -1,45 +1,49 @@
package com.tangem.core.ui.extensions
import androidx.annotation.DrawableRes
import com.tangem.core.ui.R
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.luminance
import androidx.core.graphics.toColorInt
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.domain.tokens.models.CryptoCurrency
private const val LIGHT_LUMINANCE = 0.5f
private const val COLOR_HEX_START_INDEX = 2
private const val COLOR_HEX_END_INDEX = 7
/**
* Retrieves the resource ID for the network badge of a [CryptoCurrency].
* Retrieves the resource ID for the network of a [CryptoCurrency].
*
* This property provides a way to fetch the appropriate drawable resource ID
* for the network badge of a given cryptocurrency. For coins, this will typically
* return null as they do not have network badges, while tokens will fetch the icon
* based on their associated network ID.
*
* @return Drawable resource ID for the network badge or null if the cryptocurrency is a coin.
* @return Drawable resource ID for the network.
*/
@get:DrawableRes
val CryptoCurrency.networkBadgeIconResId: Int?
get() = when (this) {
is CryptoCurrency.Coin -> null
is CryptoCurrency.Token -> getActiveIconRes(network.id.value)
val CryptoCurrency.networkIconResId: Int
get() = getActiveIconRes(network.id.value)
/**
* Tries to extract a background color from the contract address of a token.
*
* @param fallbackColor The color to use as a fallback.
* @return The extracted background color or the fallback color if extraction fails or if it is a test network token.
*/
fun CryptoCurrency.Token.tryGetBackgroundForTokenIcon(fallbackColor: Color = TangemColorPalette.Black): Color {
if (network.isTestnet) return fallbackColor
return try {
val colorHex = "#" + contractAddress.substring(range = COLOR_HEX_START_INDEX..COLOR_HEX_END_INDEX)
Color(colorHex.toColorInt())
} catch (exception: Exception) {
fallbackColor
}
}
/**
* Retrieves the resource ID for the icon of a [CryptoCurrency].
* Determines the tint color to be used for a token icon based on its background color.
* If the icon's background color is light, a dark tint is chosen; otherwise, a light tint is chosen.
*
* This property provides a way to fetch the appropriate drawable resource ID
* for the icon of a given cryptocurrency.
*
* @return Drawable resource ID for the cryptocurrency icon.
* @param iconBackground The background color of the custom token icon.
* @return The tint color to be used for the icon.
*/
@get:DrawableRes
val CryptoCurrency.iconResId: Int
get() = when (this) {
is CryptoCurrency.Coin -> {
val rawCoinId = id.rawCurrencyId
if (rawCoinId != null) {
getActiveIconResByCoinId(rawCoinId, network.id.value)
} else {
R.drawable.ic_alert_24
}
}
is CryptoCurrency.Token -> R.drawable.ic_alert_24
}
fun getTintForTokenIcon(iconBackground: Color): Color {
return if (iconBackground.luminance() > LIGHT_LUMINANCE) TangemColorPalette.Black else TangemColorPalette.White
}

View file

@ -50,6 +50,53 @@ sealed interface TextReference {
data class Combined(val refs: WrappedList<TextReference>) : TextReference
}
/**
* Creates a [TextReference] using a string resource ID with optional format arguments.
*
* @param id The resource ID of the string.
* @param formatArgs A list of format arguments to be applied to the string resource.
* @return A [TextReference] representing the string resource with format arguments.
*/
fun resourceReference(@StringRes id: Int, formatArgs: WrappedList<Any> = WrappedList(emptyList())): TextReference {
return TextReference.Res(id, formatArgs)
}
/**
* Creates a [TextReference] using a plain string value.
*
* @param value The plain string value.
* @return A [TextReference] representing the provided string value.
*/
fun stringReference(value: String): TextReference {
return TextReference.Str(value)
}
/**
* Creates a [TextReference] using a plural string resource ID with count and optional format arguments.
*
* @param id The resource ID of the plural string.
* @param count The count value to determine the plural form.
* @param formatArgs A list of format arguments to be applied to the plural string resource.
* @return A [TextReference] representing the plural string resource with count and format arguments.
*/
fun pluralReference(
@PluralsRes id: Int,
count: Int,
formatArgs: WrappedList<Any> = WrappedList(emptyList()),
): TextReference {
return TextReference.PluralRes(id, count, formatArgs)
}
/**
* Combines multiple [TextReference] instances into a single [TextReference].
*
* @param refs A list of [TextReference] instances to be combined.
* @return A [TextReference] representing the combined text references.
*/
fun combinedReference(refs: WrappedList<TextReference>): TextReference {
return TextReference.Combined(refs)
}
/** Resolve [TextReference] as [String] */
@Composable
@ReadOnlyComposable

View file

@ -4,7 +4,7 @@ import androidx.compose.runtime.Immutable
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@Suppress("ConstructorParameterNaming")
@Suppress("ConstructorParameterNaming", "MagicNumber")
@Immutable
data class TangemDimens internal constructor(
// region Elevation

View file

@ -88,6 +88,7 @@ private fun materialThemeColors(colors: TangemColors, isDark: Boolean): Colors {
}
@Composable
@ReadOnlyComposable
private fun lightThemeColors(): TangemColors {
return TangemColors(
text = TangemColors.Text(
@ -135,6 +136,7 @@ private fun lightThemeColors(): TangemColors {
}
@Composable
@ReadOnlyComposable
private fun darkThemeColors(): TangemColors {
return TangemColors(
text = TangemColors.Text(

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="44dp"
android:height="44dp"
android:viewportWidth="44"
android:viewportHeight="44">
<path
android:fillColor="#656565"
android:pathData="M22,34L22.69,29.327C23,27.23 23.155,26.182 23.648,25.352C24.065,24.651 24.651,24.065 25.352,23.648C26.182,23.155 27.23,23 29.327,22.69L34,22L29.327,21.31C27.23,21 26.182,20.845 25.352,20.352C24.651,19.935 24.065,19.349 23.648,18.648C23.155,17.818 23,16.77 22.69,14.673L22,10L21.31,14.673C21,16.77 20.845,17.818 20.352,18.648C19.935,19.349 19.349,19.935 18.648,20.352C17.818,20.845 16.77,21 14.673,21.31L10,22L14.673,22.69C16.77,23 17.818,23.155 18.648,23.648C19.349,24.065 19.935,24.651 20.352,25.352C20.845,26.182 21,27.23 21.31,29.327L22,34Z" />
</vector>