Updated on 2026-08-14

This commit is contained in:
Tangem 2024-12-24 17:36:59 +03:00
commit fbb99bcda0
317 changed files with 3800 additions and 2894 deletions

View file

@ -10,5 +10,9 @@
{
"name": "NEXA/test",
"version": "undefined"
},
{
"name": "fact0rn",
"version": "undefined"
}
]

View file

@ -11,10 +11,6 @@
"name": "IS_ETHEREUM_EIP_1559_ENABLED",
"version": "5.17.0"
},
{
"name": "MIGRATE_USER_COUNTRY_CODE_ENABLED",
"version": "5.17.0"
},
{
"name": "ONRAMP_ENABLED",
"version": "undefined"
@ -26,5 +22,9 @@
{
"name": "ONBOARDING_CODE_REFACTORING_ENABLED",
"version": "undefined"
},
{
"name": "NAVIGATION_REFACTORING",
"version": "undefined"
}
]

View file

@ -1,44 +0,0 @@
package com.tangem.datasource.api.tangemTech
import com.tangem.datasource.api.common.MoshiConverter
import com.tangem.datasource.api.common.response.ApiResponseCallAdapterFactory
import com.tangem.datasource.utils.RequestHeader
import com.tangem.datasource.utils.RequestHeader.AuthenticationHeader
import com.tangem.datasource.utils.RequestHeader.CacheControlHeader
import com.tangem.datasource.utils.addHeaders
import com.tangem.datasource.utils.addLoggers
import okhttp3.OkHttpClient
import retrofit2.Retrofit
/**
[REDACTED_AUTHOR]
*/
// TODO("Remove after removing Redux")
@Deprecated("Use TangemTechApi")
object TangemTechService {
var api: TangemTechApi = createApi()
private set
private const val TANGEM_TECH_BASE_URL = "https://api.tangem-tech.com/v1/"
fun addAuthenticationHeader(header: AuthenticationHeader) {
api = createApi(header)
}
private fun createApi(header: RequestHeader? = null): TangemTechApi {
val headers = mutableListOf<RequestHeader>(CacheControlHeader).apply { header?.let(::add) }
return Retrofit.Builder()
.addConverterFactory(MoshiConverter.networkMoshiConverter)
.addCallAdapterFactory(ApiResponseCallAdapterFactory.create())
.baseUrl(TANGEM_TECH_BASE_URL)
.client(
OkHttpClient.Builder()
.addHeaders(*headers.toTypedArray())
.addLoggers()
.build(),
)
.build()
.create(TangemTechApi::class.java)
}
}

View file

@ -87,6 +87,19 @@ internal object BlockchainSDKConfigConverter : Converter<EnvironmentConfigModel,
base = GetBlockAccessToken(jsonRpc = accessTokens.base?.jsonRPC),
blast = GetBlockAccessToken(jsonRpc = accessTokens.blast?.jsonRPC),
filecoin = GetBlockAccessToken(jsonRpc = accessTokens.filecoin?.jsonRPC),
arbitrum = GetBlockAccessToken(jsonRpc = accessTokens.arbitrum?.jsonRPC),
bitcoinCash = GetBlockAccessToken(
jsonRpc = accessTokens.bitcoinCash?.jsonRPC,
blockBookRest = accessTokens.bitcoinCash?.blockBookRest,
),
kusama = GetBlockAccessToken(jsonRpc = accessTokens.kusama?.jsonRPC),
moonbeam = GetBlockAccessToken(jsonRpc = accessTokens.moonbeam?.jsonRPC),
optimism = GetBlockAccessToken(jsonRpc = accessTokens.optimism?.jsonRPC),
polkadot = GetBlockAccessToken(jsonRpc = accessTokens.polkadot?.jsonRPC),
shibarium = GetBlockAccessToken(jsonRpc = accessTokens.shibarium?.jsonRPC),
sui = GetBlockAccessToken(jsonRpc = accessTokens.sui?.jsonRPC),
telos = GetBlockAccessToken(jsonRpc = accessTokens.telos?.jsonRPC),
tezos = GetBlockAccessToken(rest = accessTokens.tezos?.rest),
)
}
}

View file

@ -66,6 +66,16 @@ data class GetBlockAccessTokens(
@Json(name = "base") val base: GetBlockToken?,
@Json(name = "blast") val blast: GetBlockToken?,
@Json(name = "filecoin") val filecoin: GetBlockToken?,
@Json(name = "arbitrum") val arbitrum: GetBlockToken?,
@Json(name = "bitcoinCash") val bitcoinCash: GetBlockToken?,
@Json(name = "kusama") val kusama: GetBlockToken?,
@Json(name = "moonbeam") val moonbeam: GetBlockToken?,
@Json(name = "optimism") val optimism: GetBlockToken?,
@Json(name = "polkadot") val polkadot: GetBlockToken?,
@Json(name = "shibarium") val shibarium: GetBlockToken?,
@Json(name = "sui") val sui: GetBlockToken?,
@Json(name = "telos") val telos: GetBlockToken?,
@Json(name = "tezos") val tezos: GetBlockToken?,
)
@JsonClass(generateAdapter = true)

View file

@ -6,4 +6,17 @@ plugins {
android {
namespace = "com.tangem.core.res"
}
dependencies {
implementation(projects.core.utils)
implementation(deps.timber)
// region Firebase libraries
implementation(platform(deps.firebase.bom))
implementation(deps.firebase.analytics)
implementation(deps.firebase.crashlytics)
// endregion
}

View file

@ -0,0 +1,82 @@
package com.tangem.core.res
import android.content.res.Resources
import androidx.annotation.PluralsRes
import androidx.annotation.StringRes
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.tangem.utils.SupportedLanguages
import timber.log.Timber
/**
* Get a string resource safely or the resource name if an exception is thrown
*
* @param id resource id
*/
fun Resources.getStringSafe(@StringRes id: Int): String {
return runCatching { getString(id) }
.getOrResourceName(resources = this, id)
}
/**
* Get a string resource safely or the resource name if an exception is thrown
*
* @param id resource id
* @param formatArgs format args
*/
fun Resources.getStringSafe(@StringRes id: Int, vararg formatArgs: Any): String {
return runCatching { getString(id, *formatArgs) }
.recoverCatching {
// If something goes wrong, returns the resource without arguments
val string = getString(id)
reportIssue(resources = this, id, *formatArgs)
string
}
.getOrResourceName(resources = this, id, *formatArgs)
}
/**
* Get a plural string resource safely or the resource name if an exception is thrown
*
* @param id resource id
* @param count count
*/
fun Resources.getPluralStringSafe(@PluralsRes id: Int, count: Int): String {
return runCatching { getQuantityString(id, count) }
.getOrResourceName(resources = this, id)
}
/**
* Get a plural string resource safely or the resource name if an exception is thrown
*
* @param id resource id
* @param count count
* @param formatArgs format args
*/
fun Resources.getPluralStringSafe(@PluralsRes id: Int, count: Int, vararg formatArgs: Any): String {
return runCatching { getQuantityString(id, count, *formatArgs) }
.getOrResourceName(resources = this, id, *formatArgs)
}
private fun Result<String>.getOrResourceName(resources: Resources, id: Int, vararg formatArgs: Any): String {
return getOrElse {
reportIssue(resources, id, formatArgs)
// If something still goes wrong, returns the resource name
resources.getResourceEntryName(id)
}
}
private fun reportIssue(resources: Resources, id: Int, vararg formatArgs: Any) {
val exception = IllegalStateException(
"An error occurred while parsing the string:\n" +
"\tname: R.string.${resources.getResourceEntryName(id)}\n" +
"\targs: ${formatArgs.joinToString(prefix = "[", postfix = "]") { it.toString() }}\n" +
"\tlocale: ${SupportedLanguages.getCurrentSupportedLanguageCode()}\n",
)
Timber.tag("Resources").e(exception)
FirebaseCrashlytics.getInstance().recordException(exception)
}

View file

@ -9,6 +9,19 @@ import androidx.compose.runtime.Stable
@Stable
interface ClipboardManager {
/** Copies to clipboard [text] with optional [label] */
fun setText(label: String = "", text: String)
/**
* Copies to clipboard [text] with optional [label]
*
* @param text text
* @param isSensitive flag that determines whether the text is sensitive
* @param label label
*/
fun setText(text: String, isSensitive: Boolean, label: String = "")
/**
* Get text (text/plain) from clipboard
*
* @param default default value
*/
fun getText(default: String? = null): String?
}

View file

@ -16,7 +16,6 @@ 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
@ -27,6 +26,7 @@ import com.tangem.core.ui.R
import com.tangem.core.ui.components.SelctorDialogParamsProvider.SelectorDialogParams
import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults
import com.tangem.core.ui.components.fields.SimpleDialogTextField
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import kotlinx.collections.immutable.ImmutableList
@ -318,14 +318,14 @@ private fun DialogButtons(
) {
if (dismissButton != null) {
DialogButton(
text = dismissButton.title ?: stringResource(id = R.string.common_cancel),
text = dismissButton.title ?: stringResourceSafe(id = R.string.common_cancel),
warning = dismissButton.warning,
enabled = dismissButton.enabled,
onClick = dismissButton.onClick,
)
}
DialogButton(
text = confirmButton.title ?: stringResource(id = R.string.common_ok),
text = confirmButton.title ?: stringResourceSafe(id = R.string.common_ok),
warning = confirmButton.warning,
enabled = confirmButton.enabled,
onClick = confirmButton.onClick,

View file

@ -20,16 +20,14 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.core.res.getStringSafe
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SecondaryButtonIconStart
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
@ -75,10 +73,9 @@ private fun TokenReceiveBottomSheetContent(content: TokenReceiveBottomSheetConfi
)
Buttons(
address = selectedAddress.value,
snackbarHostState = snackbarHostState,
onShareClick = content.onShareClick,
onCopyClick = content.onCopyClick,
onShareClick = { content.onShareClick(selectedAddress.value) },
onCopyClick = { content.onCopyClick(selectedAddress.value) },
)
}
}
@ -96,7 +93,7 @@ private fun Info(currency: String, network: String, showMemoDisclaimer: Boolean,
modifier = Modifier
.padding(horizontal = 18.dp)
.fillMaxWidth(),
text = stringResource(R.string.receive_bottom_sheet_no_memo_required_message),
text = stringResourceSafe(R.string.receive_bottom_sheet_no_memo_required_message),
style = TangemTheme.typography.caption1,
color = TangemTheme.colors.text.tertiary,
textAlign = TextAlign.Center,
@ -191,7 +188,7 @@ private fun QrCodePage(content: TokenReceiveBottomSheetConfig, qrCodePainter: Pa
verticalArrangement = Arrangement.spacedBy(24.dp),
) {
Text(
text = stringResource(
text = stringResourceSafe(
R.string.receive_bottom_sheet_warning_message,
getName(content = content, index = currentIndex),
content.symbol,
@ -229,14 +226,12 @@ private fun getName(content: TokenReceiveBottomSheetConfig, index: Int): String
@Composable
private fun Buttons(
address: String,
snackbarHostState: SnackbarHostState,
onShareClick: () -> Unit,
onCopyClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val hapticFeedback = LocalHapticFeedback.current
val clipboardManager = LocalClipboardManager.current
val coroutineScope = rememberCoroutineScope()
val context = LocalContext.current
val resources = context.resources
@ -247,17 +242,16 @@ private fun Buttons(
) {
SecondaryButtonIconStart(
modifier = Modifier.weight(1f),
text = stringResource(id = R.string.common_copy),
text = stringResourceSafe(id = R.string.common_copy),
iconResId = R.drawable.ic_copy_24,
onClick = {
onCopyClick()
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
clipboardManager.setText(AnnotatedString(address))
coroutineScope.launch {
snackbarHostState.showSnackbar(
message = resources.getString(R.string.wallet_notification_address_copied),
message = resources.getStringSafe(R.string.wallet_notification_address_copied),
)
}
},
@ -265,13 +259,12 @@ private fun Buttons(
SecondaryButtonIconStart(
modifier = Modifier.weight(1f),
text = stringResource(id = R.string.common_share),
text = stringResourceSafe(id = R.string.common_share),
iconResId = R.drawable.ic_share_24,
onClick = {
onShareClick()
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
context.shareText(address)
onShareClick()
},
)
}

View file

@ -9,6 +9,6 @@ data class TokenReceiveBottomSheetConfig(
val network: String,
val addresses: ImmutableList<AddressModel>,
val showMemoDisclaimer: Boolean,
val onCopyClick: () -> Unit,
val onShareClick: () -> Unit,
val onCopyClick: (String) -> Unit,
val onShareClick: (String) -> Unit,
) : TangemBottomSheetConfigContent

View file

@ -4,7 +4,10 @@ import android.content.res.Configuration
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.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
@ -14,7 +17,6 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
@ -24,8 +26,9 @@ import com.tangem.core.ui.components.inputrow.inner.DividerContainer
import com.tangem.core.ui.components.inputrow.inner.InputRowAsyncImage
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
/**
* [Input Row Best Rate](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-889&mode=dev)
@ -112,7 +115,7 @@ private fun InnerTitle(title: TextReference, titleExtra: TextReference, showTag:
)
if (showTag) {
Text(
text = stringResource(R.string.express_provider_best_rate),
text = stringResourceSafe(R.string.express_provider_best_rate),
style = TangemTheme.typography.caption1,
color = TangemTheme.colors.icon.accent,
modifier = Modifier

View file

@ -18,9 +18,9 @@ import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.R
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.DEFAULT_ANIMATION_DURATION
@ -51,7 +51,7 @@ fun PasteButton(isPasteButtonVisible: Boolean, onClick: (String) -> Unit, modifi
modifier = modifier,
) {
Text(
text = stringResource(R.string.common_paste),
text = stringResourceSafe(R.string.common_paste),
style = TangemTheme.typography.caption1,
color = TangemTheme.colors.text.primary2,
modifier = Modifier
@ -86,7 +86,7 @@ fun CrossIcon(onClick: (String) -> Unit, modifier: Modifier = Modifier) {
Icon(
painter = painterResource(id = R.drawable.ic_close_24),
tint = TangemTheme.colors.icon.informative,
contentDescription = stringResource(R.string.common_close),
contentDescription = stringResourceSafe(R.string.common_close),
modifier = modifier
.size(TangemTheme.dimens.size24)
.clickable(

View file

@ -10,7 +10,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
@ -18,6 +17,7 @@ import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameter
import androidx.compose.ui.unit.Dp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.BigDecimalFormatter
@ -61,7 +61,7 @@ private fun Title(currencyName: String, modifier: Modifier = Modifier) {
contentAlignment = Alignment.CenterStart,
) {
Text(
text = stringResource(id = R.string.wallet_marketplace_block_title, currencyName),
text = stringResourceSafe(id = R.string.wallet_marketplace_block_title, currencyName),
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.subtitle2,
)
@ -157,7 +157,7 @@ private fun LoadingContent() {
@Composable
private fun QuoteTimeStatus() {
Text(
text = stringResource(id = R.string.wallet_marketprice_block_update_time),
text = stringResourceSafe(id = R.string.wallet_marketprice_block_update_time),
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.body2,
)

View file

@ -14,7 +14,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
@ -23,8 +22,9 @@ import com.tangem.core.ui.components.atoms.text.EllipsisText
import com.tangem.core.ui.components.atoms.text.TextEllipsis
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.utils.StringsSigns
@Composable
@ -77,7 +77,7 @@ fun SelectorRowItem(
contentDescription = null,
)
Text(
text = stringResource(titleRes),
text = stringResourceSafe(titleRes),
style = textStyle,
color = TangemTheme.colors.text.primary1,
modifier = Modifier.padding(start = TangemTheme.dimens.spacing8),

View file

@ -10,10 +10,10 @@ 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.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import com.tangem.core.ui.R
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import org.burnoutcrew.reorderable.ReorderableLazyListState
import org.burnoutcrew.reorderable.detectReorder
@ -67,7 +67,7 @@ private fun DraggableImage(reorderableTokenListState: ReorderableLazyListState?)
@Composable
private fun NonFiatContentText(@StringRes text: Int) {
Text(
text = stringResource(id = text),
text = stringResourceSafe(id = text),
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,

View file

@ -6,13 +6,13 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import com.tangem.core.ui.R
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.audits.AuditLabel
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.extensions.orMaskWithStars
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.components.token.state.TokenItemState.Subtitle2State as TokenCryptoAmountState
@ -33,7 +33,7 @@ internal fun TokenCryptoAmount(
AuditLabel(state = state.auditLabelUM, modifier = modifier)
}
is TokenCryptoAmountState.Unreachable -> {
CryptoAmountText(amount = stringResource(id = R.string.common_unreachable), modifier = modifier)
CryptoAmountText(amount = stringResourceSafe(id = R.string.common_unreachable), modifier = modifier)
}
is TokenCryptoAmountState.Loading -> {
RectangleShimmer(modifier = modifier.placeholderSize(), radius = TangemTheme.dimens.radius4)

View file

@ -8,7 +8,6 @@ import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.fields.SearchBar
import com.tangem.core.ui.components.token.TokenItem
import com.tangem.core.ui.components.tokenlist.internal.GroupTitleItem
import com.tangem.core.ui.components.tokenlist.internal.NetworkTitleItem
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
@ -25,9 +24,6 @@ import com.tangem.core.ui.res.TangemTheme
@Composable
fun TokenListItem(state: TokensListItemUM, isBalanceHidden: Boolean, modifier: Modifier = Modifier) {
when (state) {
is TokensListItemUM.NetworkGroupTitle -> {
NetworkTitleItem(networkName = state.name.resolveReference(), modifier = modifier)
}
is TokensListItemUM.GroupTitle -> {
GroupTitleItem(state, modifier)
}

View file

@ -1,12 +1,32 @@
package com.tangem.core.ui.components.tokenlist.internal
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.res.vectorResource
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.components.rows.NetworkTitle
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
import com.tangem.core.ui.extensions.TextReference
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.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import org.burnoutcrew.reorderable.ReorderableLazyListState
import org.burnoutcrew.reorderable.detectReorder
import org.burnoutcrew.reorderable.rememberReorderableLazyListState
/**
* Group title item
@ -18,14 +38,90 @@ import com.tangem.core.ui.res.TangemTheme
*/
@Composable
fun GroupTitleItem(state: TokensListItemUM.GroupTitle, modifier: Modifier = Modifier) {
BaseGroupTitleItem(text = state.text, modifier = modifier)
}
/**
* Draggable network group item
*
* @param state state
* @param reorderableTokenListState reorderable token list state
* @param modifier modifier
*/
@Composable
fun DraggableGroupTitleItem(
state: TokensListItemUM.GroupTitle,
reorderableTokenListState: ReorderableLazyListState,
modifier: Modifier = Modifier,
) {
BaseGroupTitleItem(
text = state.text,
modifier = modifier,
action = { DraggableIcon(reorderableTokenListState = reorderableTokenListState) },
)
}
@Composable
private fun BaseGroupTitleItem(
text: TextReference,
modifier: Modifier = Modifier,
action: (@Composable BoxScope.() -> Unit)? = null,
) {
NetworkTitle(
title = {
Text(
text = state.text.resolveReference(),
text = text.resolveReference(),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
)
},
modifier = modifier,
action = action,
)
}
}
@Composable
private fun DraggableIcon(reorderableTokenListState: ReorderableLazyListState) {
Box(
modifier = Modifier.detectReorder(reorderableTokenListState),
contentAlignment = Alignment.Center,
) {
Icon(
painter = rememberVectorPainter(
image = ImageVector.vectorResource(id = R.drawable.ic_group_drop_24),
),
contentDescription = null,
tint = TangemTheme.colors.icon.informative,
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun NetworkTitleItemPreview(@PreviewParameter(GroupTitleItemProvider::class) isDraggable: Boolean) {
TangemThemePreview {
val state = TokensListItemUM.GroupTitle(
id = 1,
text = resourceReference(
id = R.string.wallet_network_group_title,
formatArgs = wrappedList(resourceReference(id = R.string.main_tokens)),
),
)
if (isDraggable) {
DraggableGroupTitleItem(
state = state,
reorderableTokenListState = rememberReorderableLazyListState(onMove = { _, _ -> }),
modifier = Modifier.background(color = TangemTheme.colors.background.primary),
)
} else {
GroupTitleItem(
state = state,
modifier = Modifier.background(color = TangemTheme.colors.background.primary),
)
}
}
}
private object GroupTitleItemProvider : CollectionPreviewParameterProvider<Boolean>(collection = listOf(true, false))

View file

@ -1,113 +0,0 @@
package com.tangem.core.ui.components.tokenlist.internal
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
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.components.rows.NetworkTitle
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import org.burnoutcrew.reorderable.ReorderableLazyListState
import org.burnoutcrew.reorderable.detectReorder
import org.burnoutcrew.reorderable.rememberReorderableLazyListState
/**
* Network title item
*
* @param networkName network name
* @param modifier modifier
*/
@Composable
internal fun NetworkTitleItem(networkName: String, modifier: Modifier = Modifier) {
BaseNetworkTitleItem(networkName = networkName, modifier = modifier)
}
/**
* Draggable network title item
*
* @param networkName network name
* @param reorderableTokenListState reorderable token list state
* @param modifier modifier
*/
@Composable
fun DraggableNetworkTitleItem(
networkName: String,
reorderableTokenListState: ReorderableLazyListState,
modifier: Modifier = Modifier,
) {
BaseNetworkTitleItem(
networkName = networkName,
modifier = modifier,
action = { DraggableIcon(reorderableTokenListState = reorderableTokenListState) },
)
}
@Composable
private fun BaseNetworkTitleItem(
networkName: String,
modifier: Modifier = Modifier,
action: (@Composable BoxScope.() -> Unit)? = null,
) {
NetworkTitle(
title = {
Text(
text = stringResource(id = R.string.wallet_network_group_title, networkName),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
)
},
modifier = modifier,
action = action,
)
}
@Composable
private fun DraggableIcon(reorderableTokenListState: ReorderableLazyListState) {
Box(
modifier = Modifier.detectReorder(reorderableTokenListState),
contentAlignment = Alignment.Center,
) {
Icon(
painter = rememberVectorPainter(
image = ImageVector.vectorResource(id = R.drawable.ic_group_drop_24),
),
contentDescription = null,
tint = TangemTheme.colors.icon.informative,
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun NetworkTitleItemPreview(@PreviewParameter(NetworkTitleItemProvider::class) isDraggable: Boolean) {
TangemThemePreview {
if (isDraggable) {
DraggableNetworkTitleItem(
networkName = "Ethereum",
reorderableTokenListState = rememberReorderableLazyListState(onMove = { _, _ -> }),
modifier = Modifier.background(color = TangemTheme.colors.background.primary),
)
} else {
NetworkTitleItem(
networkName = "Ethereum",
modifier = Modifier.background(color = TangemTheme.colors.background.primary),
)
}
}
}
private object NetworkTitleItemProvider : CollectionPreviewParameterProvider<Boolean>(collection = listOf(true, false))

View file

@ -23,15 +23,6 @@ sealed interface TokensListItemUM {
val searchBarUM: SearchBarUM,
) : TokensListItemUM
/**
* Network group title
*
* @property id id
* @property name network group name
*/
@Deprecated("Use GroupTitle instead") // TODO: [REDACTED_JIRA]
data class NetworkGroupTitle(override val id: Int, val name: TextReference) : TokensListItemUM
/**
* Group title
*

View file

@ -14,7 +14,6 @@ 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.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
@ -234,7 +233,7 @@ private fun Subtitle(state: TransactionState, modifier: Modifier = Modifier) {
is TransactionState.Content -> {
Text(
text = when (state.status) {
is Status.Failed -> stringResource(id = R.string.common_transaction_failed)
is Status.Failed -> stringResourceSafe(id = R.string.common_transaction_failed)
else -> state.subtitle.resolveReference()
},
modifier = modifier,

View file

@ -10,11 +10,11 @@ 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.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.R
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
/**
* Transactions block title
@ -35,7 +35,7 @@ internal fun TxHistoryTitle(onExploreClick: () -> Unit, modifier: Modifier = Mod
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(
text = stringResource(id = R.string.common_transactions),
text = stringResourceSafe(id = R.string.common_transactions),
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.subtitle2,
)
@ -52,7 +52,7 @@ internal fun TxHistoryTitle(onExploreClick: () -> Unit, modifier: Modifier = Mod
contentDescription = null,
)
Text(
text = stringResource(id = R.string.common_explorer),
text = stringResourceSafe(id = R.string.common_explorer),
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.subtitle2,
)

View file

@ -84,6 +84,7 @@ fun getActiveIconRes(blockchainId: String): Int {
"canxium" -> R.drawable.img_canxium_22
"chiliz", "chiliz/test" -> R.drawable.img_chiliz_22
"clore-ai" -> R.drawable.img_clore_22
"fact0rn" -> R.drawable.img_fact0rn_22
else -> R.drawable.ic_alert_24
}
}
@ -166,6 +167,7 @@ fun getActiveIconResByCoinId(coinId: String): Int {
"canxium" -> R.drawable.img_canxium_22
"chiliz", "chiliz/test" -> R.drawable.img_chiliz_22
"clore-ai" -> R.drawable.img_clore_22
"fact0rn" -> R.drawable.img_fact0rn_22
else -> R.drawable.ic_alert_24
}
}
@ -251,6 +253,7 @@ fun getGreyedOutIconRes(blockchainId: String): Int {
"canxium" -> R.drawable.ic_canxium_22
"chiliz", "chiliz/test" -> R.drawable.ic_chiliz_22
"clore-ai" -> R.drawable.ic_clore_22
"fact0rn" -> R.drawable.ic_fact0rn_22
else -> R.drawable.ic_alert_24
}
}

View file

@ -1,15 +0,0 @@
package com.tangem.core.ui.extensions
import android.content.Context
import android.content.Intent
import androidx.core.content.ContextCompat
fun Context.shareText(text: String) {
val sendIntent: Intent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, text)
type = "text/plain"
}
val shareIntent = Intent.createChooser(sendIntent, null)
ContextCompat.startActivity(this, shareIntent, null)
}

View file

@ -2,12 +2,56 @@ package com.tangem.core.ui.extensions
import android.graphics.Bitmap
import android.graphics.Color
import androidx.annotation.PluralsRes
import androidx.annotation.StringRes
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.platform.LocalContext
import com.google.zxing.BarcodeFormat
import com.google.zxing.EncodeHintType
import com.google.zxing.qrcode.QRCodeWriter
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
import com.tangem.core.res.getPluralStringSafe
import com.tangem.core.res.getStringSafe
import java.util.Hashtable
/**
* Get a string resource safely or the resource name if an exception is thrown
*
* @param id resource id
* @param formatArgs format args
*/
@Composable
@ReadOnlyComposable
fun stringResourceSafe(@StringRes id: Int, vararg formatArgs: Any): String {
val resources = LocalContext.current.resources
return if (formatArgs.isEmpty()) {
resources.getStringSafe(id)
} else {
resources.getStringSafe(id, *formatArgs)
}
}
/**
* Get a plural string resource safely or the resource name if an exception is thrown
*
* @param id resource id
* @param count count
* @param formatArgs format args
*/
@Composable
@ReadOnlyComposable
fun pluralStringResourceSafe(@PluralsRes id: Int, count: Int, vararg formatArgs: Any): String {
val resources = LocalContext.current.resources
return if (formatArgs.isEmpty()) {
resources.getPluralStringSafe(id, count)
} else {
resources.getPluralStringSafe(id, count, *formatArgs)
}
}
@Suppress("MagicNumber")
fun String.toQrCode(sizePx: Int = 256, paddingPx: Int = 0): Bitmap {
val hintMap = Hashtable<EncodeHintType, Any>()

View file

@ -6,12 +6,11 @@ import androidx.annotation.StringRes
import androidx.compose.runtime.Composable
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.AnnotatedString.Builder
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.decapitalize
import com.tangem.core.res.getPluralStringSafe
import com.tangem.core.res.getStringSafe
import org.intellij.markdown.MarkdownElementTypes
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
@ -172,7 +171,7 @@ fun TextReference.resolveReference(): String {
.map { if (it is TextReference) it.resolveReference() else it }
.toTypedArray()
val resolvedReference = stringResource(id = id, *args)
val resolvedReference = stringResourceSafe(id = id, *args)
if (decapitalize) {
resolvedReference.replaceFirstChar { char -> char.lowercase() }
@ -180,7 +179,7 @@ fun TextReference.resolveReference(): String {
resolvedReference
}
}
is TextReference.PluralRes -> pluralStringResource(id, count, *formatArgs.toTypedArray())
is TextReference.PluralRes -> pluralStringResourceSafe(id, count, *formatArgs.toTypedArray())
is TextReference.Str -> value
is TextReference.Annotated -> value.text
is TextReference.Combined -> {
@ -201,9 +200,9 @@ fun TextReference.resolveReference(resources: Resources): String {
.map { if (it is TextReference) it.resolveReference(resources) else it }
.toTypedArray()
resources.getString(id, *args)
resources.getStringSafe(id, *args)
}
is TextReference.PluralRes -> resources.getQuantityString(id, count, *formatArgs.toTypedArray())
is TextReference.PluralRes -> resources.getPluralStringSafe(id, count, *formatArgs.toTypedArray())
is TextReference.Str -> value
is TextReference.Annotated -> value.text
is TextReference.Combined -> {
@ -225,10 +224,10 @@ fun TextReference.resolveAnnotatedReference(): AnnotatedString {
.map { if (it is TextReference) it.resolveReference() else it }
.toTypedArray()
formatAnnotated(stringResource(id = id, *args))
formatAnnotated(stringResourceSafe(id = id, *args))
}
is TextReference.PluralRes -> formatAnnotated(
pluralStringResource(id, count, *formatArgs.toTypedArray()),
pluralStringResourceSafe(id, count, *formatArgs.toTypedArray()),
)
is TextReference.Str -> formatAnnotated(value)
is TextReference.Annotated -> value

View file

@ -1,5 +1,7 @@
package com.tangem.core.ui.res
import android.app.Activity
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
import androidx.compose.foundation.text.selection.TextSelectionColors
import androidx.compose.material.Colors
@ -10,13 +12,40 @@ import androidx.compose.runtime.*
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalView
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.components.TangemShimmer
import com.tangem.core.ui.haptic.DefaultHapticManager
import com.tangem.core.ui.haptic.HapticManager
import com.tangem.core.ui.haptic.VibratorHapticManager
import com.tangem.core.ui.windowsize.WindowSize
import com.tangem.core.ui.windowsize.rememberWindowSize
import com.tangem.domain.apptheme.model.AppThemeMode
import com.valentinilk.shimmer.Shimmer
@Composable
fun TangemTheme(
activity: Activity,
uiDependencies: UiDependencies,
typography: TangemTypography = TangemTheme.typography,
dimens: TangemDimens = TangemTheme.dimens,
overrideSystemBarColors: Boolean = true,
content: @Composable () -> Unit,
) {
val appThemeMode by uiDependencies.appThemeModeHolder.appThemeMode
val windowSize = rememberWindowSize(activity = activity)
TangemTheme(
isDark = shouldUseDarkTheme(appThemeMode),
windowSize = windowSize,
vibratorHapticManager = uiDependencies.vibratorHapticManager,
snackbarHostState = uiDependencies.globalSnackbarHostState,
overrideSystemBarColors = overrideSystemBarColors,
typography = typography,
dimens = dimens,
content = content,
)
}
@Composable
fun TangemTheme(
isDark: Boolean = false,
@ -266,4 +295,20 @@ val LocalTangemShimmer = staticCompositionLocalOf<Shimmer> {
val LocalMainBottomSheetColor = staticCompositionLocalOf<MutableState<Color>> {
error("No MainBottomSheetColor provided")
}
/**
* Determines whether the dark theme should be used based on the given [AppThemeMode].
*
* @param appThemeMode The application theme mode.
* @return `true` if the dark theme should be used, `false` otherwise.
*/
@Composable
@ReadOnlyComposable
private fun shouldUseDarkTheme(appThemeMode: AppThemeMode): Boolean {
return when (appThemeMode) {
AppThemeMode.FORCE_DARK -> true
AppThemeMode.FORCE_LIGHT -> false
AppThemeMode.FOLLOW_SYSTEM -> isSystemInDarkTheme()
}
}

View file

@ -2,17 +2,13 @@ package com.tangem.core.ui.screen
import android.app.Activity
import android.content.Context
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
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.UiDependencies
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.windowsize.rememberWindowSize
import com.tangem.domain.apptheme.model.AppThemeMode
/**
* Interface representing a Compose screen with common theming and content composition properties.
@ -60,34 +56,13 @@ internal fun ComposeScreen.createComposeView(
): ComposeView {
return ComposeView(context).apply {
setContent {
val appThemeMode by uiDependencies.appThemeModeHolder.appThemeMode
val windowSize = rememberWindowSize(activity = activity)
TangemTheme(
isDark = shouldUseDarkTheme(appThemeMode),
windowSize = windowSize,
vibratorHapticManager = uiDependencies.vibratorHapticManager,
snackbarHostState = uiDependencies.globalSnackbarHostState,
activity = activity,
uiDependencies = uiDependencies,
overrideSystemBarColors = overrideSystemBarColors,
) {
ScreenContent(modifier = screenModifier)
}
}
}
}
/**
* Determines whether the dark theme should be used based on the given [AppThemeMode].
*
* @param appThemeMode The application theme mode.
* @return `true` if the dark theme should be used, `false` otherwise.
*/
@Composable
@ReadOnlyComposable
private fun shouldUseDarkTheme(appThemeMode: AppThemeMode): Boolean {
return when (appThemeMode) {
AppThemeMode.FORCE_DARK -> true
AppThemeMode.FORCE_LIGHT -> false
AppThemeMode.FOLLOW_SYSTEM -> isSystemInDarkTheme()
}
}

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="M11.573,4L11.591,9.381V14.813H11.146H10.701L10.726,9.412L10.752,4.01L11.174,4.003L11.573,4ZM9.691,9.412L9.666,4.01H9.271H8.876L8.85,9.412L8.825,14.813H9.271H9.717L9.691,9.412ZM3.889,15.807V14.411L4.308,14.633C5.114,15.059 6.309,14.861 6.919,14.2C7.477,13.597 7.7,12.428 7.43,11.52C7.174,10.66 6.32,10.034 5.403,10.034C4.852,10.034 4.325,10.182 4.062,10.412C3.904,10.549 3.889,10.543 3.889,10.348C3.889,10.16 3.834,10.134 3.444,10.134H3V13.602C3,15.509 3.03,17.1 3.066,17.136C3.102,17.173 3.302,17.203 3.51,17.203H3.889V15.807ZM13.985,11.801L14.011,8.789L16.035,11.799L18.059,14.807L18.53,14.81L19,14.813L18.974,11.104L18.944,7.331L18.539,7.342L18.112,7.334L18.086,10.383L18.059,13.431L16.023,10.413L13.987,7.396H13.555H13.122L13.096,11.104L13.07,14.813H13.514H13.958L13.985,11.801ZM4.556,13.86C3.936,13.521 3.674,12.232 4.086,11.551C4.631,10.65 5.943,10.598 6.5,11.455C6.914,12.092 6.768,13.21 6.205,13.709C5.909,13.97 5.78,14.016 5.342,14.013C5.059,14.01 4.705,13.942 4.556,13.86Z"
android:fillColor="#000000"
android:fillType="evenOdd"/>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

View file

@ -0,0 +1,20 @@
<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="#ffffff"/>
<path
android:pathData="M0,0h22v22h-22z"
android:fillColor="#F8FBF8"/>
<path
android:pathData="M11.573,4L11.591,9.381V14.813H11.146H10.701L10.726,9.412L10.752,4.01L11.174,4.003L11.573,4ZM9.691,9.412L9.666,4.01H9.271H8.876L8.85,9.412L8.825,14.813H9.271H9.717L9.691,9.412ZM3.889,15.807V14.411L4.308,14.633C5.114,15.059 6.309,14.861 6.919,14.2C7.477,13.597 7.7,12.428 7.43,11.52C7.174,10.66 6.32,10.034 5.403,10.034C4.852,10.034 4.325,10.182 4.062,10.412C3.904,10.549 3.889,10.543 3.889,10.348C3.889,10.16 3.834,10.134 3.444,10.134H3V13.602C3,15.509 3.03,17.1 3.066,17.136C3.102,17.173 3.302,17.203 3.51,17.203H3.889V15.807ZM13.985,11.801L14.011,8.789L16.035,11.799L18.059,14.807L18.53,14.81L19,14.813L18.974,11.104L18.944,7.331L18.539,7.342L18.112,7.334L18.086,10.383L18.059,13.431L16.023,10.413L13.987,7.396H13.555H13.122L13.096,11.104L13.07,14.813H13.514H13.958L13.985,11.801ZM4.556,13.86C3.936,13.521 3.674,12.232 4.086,11.551C4.631,10.65 5.943,10.598 6.5,11.455C6.914,12.092 6.768,13.21 6.205,13.709C5.909,13.97 5.78,14.016 5.342,14.013C5.059,14.01 4.705,13.942 4.556,13.86Z"
android:fillColor="#0C090A"
android:fillType="evenOdd"/>
</group>
</vector>

View file

@ -25,11 +25,12 @@ class Debouncer {
coroutineScope: CoroutineScope,
waitMs: Long = 300L,
context: CoroutineContext = EmptyCoroutineContext,
forceUpdate: Boolean = false,
destinationFunction: suspend () -> Unit,
) {
debounceJob?.cancel()
debounceJob = coroutineScope.launch(context) {
delay(waitMs)
if (!forceUpdate) delay(waitMs)
destinationFunction.invoke()
}
}