Updated on 2026-08-14
This commit is contained in:
commit
8e42589a00
456 changed files with 6580 additions and 3738 deletions
|
|
@ -10,5 +10,13 @@
|
|||
{
|
||||
"name": "NEXA/test",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "fact0rn",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "vanar-chain",
|
||||
"version": "undefined"
|
||||
}
|
||||
]
|
||||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ sealed class ApiConfig {
|
|||
Express,
|
||||
TangemTech,
|
||||
StakeKit,
|
||||
TangemVisaAuth,
|
||||
}
|
||||
|
||||
private fun initializeId(): ID {
|
||||
|
|
@ -31,6 +32,7 @@ sealed class ApiConfig {
|
|||
is Express -> ID.Express
|
||||
is TangemTech -> ID.TangemTech
|
||||
is StakeKit -> ID.StakeKit
|
||||
is TangemVisaAuth -> ID.TangemVisaAuth
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.datasource.api.common.config
|
||||
|
||||
import com.tangem.utils.Provider
|
||||
|
||||
internal class TangemVisaAuth : ApiConfig() {
|
||||
|
||||
override val defaultEnvironment: ApiEnvironment = ApiEnvironment.STAGE
|
||||
|
||||
override val environmentConfigs = listOf(
|
||||
createStageEnvironment(),
|
||||
)
|
||||
|
||||
private fun createStageEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.STAGE,
|
||||
baseUrl = "https://api-s.tangem.org/",
|
||||
headers = createHeaders(),
|
||||
)
|
||||
|
||||
private fun createHeaders() = mapOf<String, Provider<String>>()
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.tangem.datasource.api.visa
|
||||
|
||||
interface TangemVisaApi {
|
||||
// TODO
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
package com.tangem.datasource.api.visa
|
||||
|
||||
import com.tangem.datasource.api.visa.models.response.GenerateNonceResponse
|
||||
import com.tangem.datasource.api.visa.models.response.JWTResponse
|
||||
import retrofit2.http.Field
|
||||
import retrofit2.http.FormUrlEncoded
|
||||
import retrofit2.http.Headers
|
||||
import retrofit2.http.POST
|
||||
|
||||
interface TangemVisaAuthApi {
|
||||
|
||||
@FormUrlEncoded
|
||||
@Headers("Content-Type: application/x-www-form-urlencoded")
|
||||
@POST("auth/clients/mobile-app-android/nonce-challenge")
|
||||
suspend fun generateNonceByWalletAddress(
|
||||
@Field("customer_id") customerId: String? = null,
|
||||
@Field("customer_wallet_address") customerWalletAddress: String,
|
||||
): GenerateNonceResponse
|
||||
|
||||
@FormUrlEncoded
|
||||
@Headers("Content-Type: application/x-www-form-urlencoded")
|
||||
@POST("auth/clients/mobile-app-android/nonce-challenge")
|
||||
suspend fun generateNonceByCard(
|
||||
@Field("card_id") cardId: String,
|
||||
@Field("card_public_key") cardPublicKey: String,
|
||||
): GenerateNonceResponse
|
||||
|
||||
@FormUrlEncoded
|
||||
@Headers("Content-Type: application/x-www-form-urlencoded")
|
||||
@POST("auth/protocol/openid-connect/token")
|
||||
suspend fun getAccessToken(
|
||||
@Field("client_id") clientId: String = "mobile-app-android",
|
||||
@Field("grant_type") grantType: String = "password",
|
||||
@Field("session_id") sessionId: String,
|
||||
@Field("signature") signature: String,
|
||||
@Field("salt") salt: String?,
|
||||
): JWTResponse
|
||||
|
||||
@FormUrlEncoded
|
||||
@Headers("Content-Type: application/x-www-form-urlencoded")
|
||||
@POST("auth/protocol/openid-connect/token")
|
||||
suspend fun refreshAccessToken(
|
||||
@Field("client_id") clientId: String = "mobile-app-android",
|
||||
@Field("grant_type") grantType: String = "refresh_token",
|
||||
@Field("refresh_token") refreshToken: String,
|
||||
): JWTResponse
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.datasource.api.visa.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GenerateNonceResponse(
|
||||
@Json(name = "nonce") val nonce: String,
|
||||
@Json(name = "session_id") val sessionId: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package com.tangem.datasource.api.visa.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class JWTResponse(
|
||||
@Json(name = "access_token") val accessToken: String,
|
||||
@Json(name = "expires_in") val expiresIn: Int,
|
||||
@Json(name = "refresh_expires_in") val refreshExpiresIn: Int,
|
||||
@Json(name = "refresh_token") val refreshToken: String,
|
||||
@Json(name = "token_type") val tokenType: String,
|
||||
@Json(name = "not-before-policy") val notBeforePolicy: Int,
|
||||
@Json(name = "session_state") val sessionState: String,
|
||||
@Json(name = "scope") val scope: String,
|
||||
)
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.datasource.di
|
||||
|
||||
import com.tangem.datasource.api.common.AuthProvider
|
||||
import com.tangem.datasource.api.common.config.ApiConfig
|
||||
import com.tangem.datasource.api.common.config.*
|
||||
import com.tangem.datasource.api.common.config.Express
|
||||
import com.tangem.datasource.api.common.config.StakeKit
|
||||
import com.tangem.datasource.api.common.config.TangemTech
|
||||
|
|
@ -39,4 +39,8 @@ internal object ApiConfigsModule {
|
|||
@IntoSet
|
||||
fun provideTangemTechConfig(appVersionProvider: AppVersionProvider, authProvider: AuthProvider): ApiConfig =
|
||||
TangemTech(appVersionProvider, authProvider)
|
||||
|
||||
@Provides
|
||||
@IntoSet
|
||||
fun provideTangemVisaConfig(): ApiConfig = TangemVisaAuth()
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ import com.tangem.datasource.api.onramp.OnrampApi
|
|||
import com.tangem.datasource.api.stakekit.StakeKitApi
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApiV2
|
||||
import com.tangem.datasource.api.visa.TangemVisaAuthApi
|
||||
import com.tangem.datasource.local.logs.AppLogsStore
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.utils.*
|
||||
|
|
@ -169,6 +170,21 @@ internal object NetworkModule {
|
|||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideTangemVisaApi(
|
||||
@NetworkMoshi moshi: Moshi,
|
||||
@ApplicationContext context: Context,
|
||||
apiConfigsManager: ApiConfigsManager,
|
||||
): TangemVisaAuthApi {
|
||||
return createApi<TangemVisaAuthApi>(
|
||||
id = ApiConfig.ID.TangemVisaAuth,
|
||||
moshi = moshi,
|
||||
context = context,
|
||||
apiConfigsManager = apiConfigsManager,
|
||||
)
|
||||
}
|
||||
|
||||
@Deprecated("use createApi instead")
|
||||
private inline fun <reified T> provideTangemTechApiInternal(
|
||||
moshi: Moshi,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -85,6 +85,7 @@ internal class ProdApiConfigsManagerTest(private val model: Model) {
|
|||
is Express -> createExpressModel()
|
||||
is TangemTech -> createTangemTechModel()
|
||||
is StakeKit -> createStakeKitModel()
|
||||
is TangemVisaAuth -> createVisaAuthModel()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -172,6 +173,16 @@ internal class ProdApiConfigsManagerTest(private val model: Model) {
|
|||
)
|
||||
}
|
||||
|
||||
private fun createVisaAuthModel(): Model {
|
||||
return Model(
|
||||
id = ApiConfig.ID.TangemVisaAuth,
|
||||
expected = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.STAGE,
|
||||
baseUrl = "https://api-s.tangem.org/",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun String.checkHeaderValueOrEmpty(): String {
|
||||
for (i in this.indices) {
|
||||
val c = this[i]
|
||||
|
|
|
|||
|
|
@ -7,8 +7,6 @@ import com.tangem.core.decompose.navigation.AppNavigationProvider
|
|||
import com.tangem.core.decompose.navigation.DefaultAppNavigationProvider
|
||||
import com.tangem.core.decompose.navigation.DefaultRouter
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.core.decompose.ui.DefaultUiMessageSender
|
||||
import com.tangem.core.decompose.ui.UiMessageHandler
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.decompose.utils.ComponentCoroutineScope
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
|
@ -16,9 +14,9 @@ import kotlinx.coroutines.CoroutineScope
|
|||
|
||||
class DefaultAppComponentContext(
|
||||
componentContext: ComponentContext,
|
||||
messageHandler: UiMessageHandler,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
override val hiltComponentBuilder: DecomposeComponent.Builder,
|
||||
override val messageSender: UiMessageSender,
|
||||
private val replaceRouter: Router? = null,
|
||||
) : AppComponentContext, ComponentContext by componentContext {
|
||||
|
||||
|
|
@ -26,8 +24,6 @@ class DefaultAppComponentContext(
|
|||
|
||||
override val componentScope: CoroutineScope = ComponentCoroutineScope(lifecycle, dispatchers)
|
||||
|
||||
override val messageSender: UiMessageSender = DefaultUiMessageSender(messageHandler)
|
||||
|
||||
override val navigationProvider: AppNavigationProvider
|
||||
get() = instanceKeeper.getOrCreate { DefaultAppNavigationProvider() }
|
||||
|
||||
|
|
|
|||
|
|
@ -3,4 +3,7 @@ package com.tangem.core.decompose.di
|
|||
import javax.inject.Qualifier
|
||||
|
||||
@Qualifier
|
||||
annotation class RootAppComponentContext
|
||||
annotation class RootAppComponentContext
|
||||
|
||||
@Qualifier
|
||||
annotation class GlobalUiMessageSender
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.core.decompose.ui
|
||||
|
||||
internal class DefaultUiMessageSender(
|
||||
class DefaultUiMessageSender(
|
||||
private val handler: UiMessageHandler,
|
||||
) : UiMessageSender {
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
82
core/res/src/main/java/com/tangem/core/res/Resources.kt
Normal file
82
core/res/src/main/java/com/tangem/core/res/Resources.kt
Normal 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)
|
||||
}
|
||||
|
|
@ -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?
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package com.tangem.core.ui.components.bottomsheets
|
|||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.focusable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.*
|
||||
|
|
@ -99,7 +98,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> DefaultBottomSheet(
|
|||
crossinline title: @Composable (BoxScope.(T) -> Unit),
|
||||
crossinline content: @Composable (ColumnScope.(T) -> Unit),
|
||||
) {
|
||||
var isVisible by remember { mutableStateOf(value = config.isShow) }
|
||||
var isVisible by remember { mutableStateOf(value = config.isShown) }
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = skipPartiallyExpanded)
|
||||
|
||||
if (isVisible && config.content is T) {
|
||||
|
|
@ -114,8 +113,8 @@ inline fun <reified T : TangemBottomSheetConfigContent> DefaultBottomSheet(
|
|||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(key1 = config.isShow) {
|
||||
if (config.isShow) {
|
||||
LaunchedEffect(key1 = config.isShown) {
|
||||
if (config.isShown) {
|
||||
isVisible = true
|
||||
} else {
|
||||
sheetState.collapse { isVisible = false }
|
||||
|
|
|
|||
|
|
@ -3,12 +3,12 @@ package com.tangem.core.ui.components.bottomsheets
|
|||
/**
|
||||
* Tangem bottom sheet config
|
||||
*
|
||||
* @property isShow flag that determine if bottom sheet is shown
|
||||
* @property isShown flag that determine if bottom sheet is shown
|
||||
* @property onDismissRequest lambda be invoked when bottom sheet is dismissed
|
||||
* @property content content config
|
||||
*/
|
||||
data class TangemBottomSheetConfig(
|
||||
val isShow: Boolean,
|
||||
val isShown: Boolean,
|
||||
val onDismissRequest: () -> Unit,
|
||||
val content: TangemBottomSheetConfigContent,
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,156 @@
|
|||
package com.tangem.core.ui.components.bottomsheets.message
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.*
|
||||
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.res.painterResource
|
||||
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.ui.R
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.components.SecondaryButton
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.extensions.isNullOrEmpty
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
|
||||
@Composable
|
||||
fun MessageBottomSheet(config: TangemBottomSheetConfig) {
|
||||
TangemBottomSheet(config) { notification: MessageBottomSheetUM ->
|
||||
Content(model = notification)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun Content(model: MessageBottomSheetUM, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier.padding(horizontal = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(40.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Box(modifier = Modifier)
|
||||
|
||||
if (model.iconResId != null) {
|
||||
Icon(
|
||||
modifier = Modifier.size(48.dp),
|
||||
painter = painterResource(model.iconResId),
|
||||
tint = TangemTheme.colors.icon.primary1,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
if (!model.title.isNullOrEmpty()) {
|
||||
Text(
|
||||
text = model.title.resolveReference(),
|
||||
style = TangemTheme.typography.h2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = model.message.resolveReference(),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(bottom = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
if (model.primaryAction != null) {
|
||||
PrimaryButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = model.primaryAction.text.resolveReference(),
|
||||
enabled = model.primaryAction.enabled,
|
||||
onClick = model.primaryAction.onClick,
|
||||
)
|
||||
}
|
||||
|
||||
if (model.secondaryAction != null) {
|
||||
SecondaryButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = model.secondaryAction.text.resolveReference(),
|
||||
enabled = model.secondaryAction.enabled,
|
||||
onClick = model.secondaryAction.onClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Composable
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
private fun Preview_NotificationBottomSheet(
|
||||
@PreviewParameter(MessageBottomSheetUMPreviewProvider::class) params: MessageBottomSheetUM,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
MessageBottomSheet(
|
||||
config = TangemBottomSheetConfig(
|
||||
content = params,
|
||||
isShown = true,
|
||||
onDismissRequest = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class MessageBottomSheetUMPreviewProvider : PreviewParameterProvider<MessageBottomSheetUM> {
|
||||
val balancesHiddenMessage = MessageBottomSheetUM(
|
||||
iconResId = R.drawable.ic_eye_off_outline_24,
|
||||
title = resourceReference(R.string.balance_hidden_title),
|
||||
message = resourceReference(R.string.balance_hidden_description),
|
||||
primaryAction = MessageBottomSheetUM.ActionUM(
|
||||
text = resourceReference(R.string.balance_hidden_got_it_button),
|
||||
onClick = {},
|
||||
),
|
||||
secondaryAction = MessageBottomSheetUM.ActionUM(
|
||||
text = resourceReference(R.string.balance_hidden_do_not_show_button),
|
||||
onClick = {},
|
||||
),
|
||||
)
|
||||
|
||||
override val values: Sequence<MessageBottomSheetUM>
|
||||
get() = sequenceOf(
|
||||
balancesHiddenMessage,
|
||||
balancesHiddenMessage.copy(title = null),
|
||||
balancesHiddenMessage.copy(iconResId = null),
|
||||
balancesHiddenMessage.copy(primaryAction = null),
|
||||
balancesHiddenMessage.copy(secondaryAction = null),
|
||||
balancesHiddenMessage.copy(
|
||||
primaryAction = null,
|
||||
secondaryAction = null,
|
||||
),
|
||||
balancesHiddenMessage.copy(
|
||||
title = null,
|
||||
iconResId = null,
|
||||
primaryAction = null,
|
||||
secondaryAction = null,
|
||||
),
|
||||
)
|
||||
}
|
||||
// endregion Preview
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.core.ui.components.bottomsheets.message
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
data class MessageBottomSheetUM(
|
||||
@DrawableRes val iconResId: Int?,
|
||||
val title: TextReference?,
|
||||
val message: TextReference,
|
||||
val primaryAction: ActionUM?,
|
||||
val secondaryAction: ActionUM?,
|
||||
) : TangemBottomSheetConfigContent {
|
||||
|
||||
data class ActionUM(
|
||||
val text: TextReference,
|
||||
val enabled: Boolean = true,
|
||||
val onClick: () -> Unit,
|
||||
)
|
||||
}
|
||||
|
|
@ -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()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -286,7 +279,7 @@ private fun Preview_TokenReceiveBottomSheet(
|
|||
) {
|
||||
TangemThemePreview {
|
||||
val config = TangemBottomSheetConfig(
|
||||
isShow = true,
|
||||
isShown = true,
|
||||
content = params,
|
||||
onDismissRequest = {},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@
|
|||
|
||||
package com.tangem.core.ui.components.progressbar
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.runtime.Composable
|
||||
|
|
@ -15,10 +17,12 @@ import androidx.compose.ui.layout.layout
|
|||
import androidx.compose.ui.semantics.ProgressBarRangeInfo
|
||||
import androidx.compose.ui.semantics.progressBarRangeInfo
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.offset
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import kotlin.math.abs
|
||||
|
||||
/**
|
||||
|
|
@ -123,4 +127,18 @@ private fun DrawScope.drawLinearIndicator(
|
|||
}
|
||||
|
||||
private fun DrawScope.drawLinearIndicatorBackground(color: Color, strokeWidth: Float, strokeCap: StrokeCap) =
|
||||
drawLinearIndicator(0f, 1f, color, strokeWidth, strokeCap)
|
||||
drawLinearIndicator(0f, 1f, color, strokeWidth, strokeCap)
|
||||
|
||||
// region Preview
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun LinearProgressIndicator_Preview() {
|
||||
TangemThemePreview {
|
||||
LinearProgressIndicator(
|
||||
progress = { 0.5f },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -9,15 +9,18 @@ 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.LocalSnackbarHostState
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
|
||||
/**
|
||||
* Tangem snackbar host.
|
||||
* Based on Material3 component. It's best way to show [TangemSnackbar].
|
||||
* Based on Material3 component. It's the 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)
|
||||
* @param modifier Modifier to be applied to the layout.
|
||||
* @param hostState SnackbarHostState that will be used to show snackbar.
|
||||
* Optional parameter. By default, it uses [LocalSnackbarHostState.current].
|
||||
* @param actionOnNewLine Flag that indicates if the action should be displayed on a new line.
|
||||
* Optional parameter, `false` by default.
|
||||
*
|
||||
* @see <a href = https://www.figma.com/design/14ISV23YB1yVW1uNVwqrKv/Android?node-id=682-761&t=kDzSZDx0m0sk4iYz-4
|
||||
* >Figma</a>
|
||||
|
|
@ -25,7 +28,11 @@ import com.tangem.core.ui.res.TangemThemePreview
|
|||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
fun TangemSnackbarHost(hostState: SnackbarHostState, modifier: Modifier = Modifier, actionOnNewLine: Boolean = false) {
|
||||
fun TangemSnackbarHost(
|
||||
modifier: Modifier = Modifier,
|
||||
hostState: SnackbarHostState = LocalSnackbarHostState.current,
|
||||
actionOnNewLine: Boolean = false,
|
||||
) {
|
||||
SnackbarHost(hostState = hostState, modifier = modifier) { data ->
|
||||
TangemSnackbar(data = data, actionOnNewLine = actionOnNewLine)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,106 @@
|
|||
package com.tangem.core.ui.components.stories
|
||||
|
||||
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.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.components.stories.inner.StoriesClickableArea
|
||||
import com.tangem.core.ui.components.stories.inner.StoriesProgressBar
|
||||
import com.tangem.core.ui.components.stories.inner.StoriesStepStateMachine
|
||||
import com.tangem.core.ui.components.stories.model.StoriesContentConfig
|
||||
import com.tangem.core.ui.components.stories.model.StoryConfig
|
||||
import com.tangem.core.ui.res.TangemColorPalette
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
/**
|
||||
* Stories container component.
|
||||
* Implements base logic for switching story pages and handling clicks
|
||||
*
|
||||
* @param config stories configuration
|
||||
* @param modifier composable modifier
|
||||
* @param isPauseStories pause stories progression externally
|
||||
* @param currentStoryContent content of current displaying story
|
||||
*/
|
||||
@Composable
|
||||
inline fun <reified T : StoryConfig> StoriesContainer(
|
||||
config: StoriesContentConfig<T>,
|
||||
modifier: Modifier = Modifier,
|
||||
isPauseStories: Boolean = false,
|
||||
crossinline currentStoryContent: @Composable BoxScope.(T, Boolean) -> Unit,
|
||||
) {
|
||||
var isPressed by remember { mutableStateOf(value = false) }
|
||||
val storyState by remember(config) {
|
||||
mutableStateOf(
|
||||
StoriesStepStateMachine(
|
||||
stories = config.stories,
|
||||
isRepeatable = config.isRestartable,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val isPaused = isPressed || isPauseStories
|
||||
|
||||
Box(modifier = modifier) {
|
||||
StoriesClickableArea(
|
||||
onPress = { isPressed = it },
|
||||
onPreviousStory = storyState::prevStory,
|
||||
onNextStory = storyState::nextStory,
|
||||
)
|
||||
|
||||
StoriesProgressBar(
|
||||
steps = storyState.steps,
|
||||
currentStep = storyState.currentIndex.intValue,
|
||||
stepDuration = storyState.currentStory.duration,
|
||||
paused = isPaused,
|
||||
onStepFinish = storyState::nextStory,
|
||||
)
|
||||
|
||||
currentStoryContent(storyState.currentStory, isPaused)
|
||||
}
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun StoriesContainer_Preview() {
|
||||
TangemThemePreview {
|
||||
StoriesContainer(
|
||||
config = StoryContainerPreviewProviderData(),
|
||||
modifier = Modifier.background(Color(0xFF010101)),
|
||||
isPauseStories = false,
|
||||
currentStoryContent = { pageConfig, _ ->
|
||||
Text(
|
||||
text = pageConfig.title,
|
||||
color = TangemColorPalette.White,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private data class StoryContainerPreviewProviderData(
|
||||
override val stories: ImmutableList<StoryConfigPreviewProviderData> = persistentListOf(
|
||||
StoryConfigPreviewProviderData(title = "Tangem"),
|
||||
StoryConfigPreviewProviderData(title = "Cold"),
|
||||
StoryConfigPreviewProviderData(title = "Wallet"),
|
||||
),
|
||||
override val isRestartable: Boolean = true,
|
||||
) : StoriesContentConfig<StoryConfigPreviewProviderData>
|
||||
|
||||
private data class StoryConfigPreviewProviderData(
|
||||
override val duration: Int = 3_000,
|
||||
val title: String,
|
||||
) : StoryConfig
|
||||
// endregion
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
package com.tangem.core.ui.components.stories.inner
|
||||
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
|
||||
private const val STORY_HOLD_DELAY = 200
|
||||
|
||||
@Composable
|
||||
fun StoriesClickableArea(onPress: (Boolean) -> Unit, onPreviousStory: () -> Unit, onNextStory: () -> Unit) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.weight(1f)
|
||||
.fillMaxHeight()
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures(
|
||||
onPress = {
|
||||
val pressStartTime = System.currentTimeMillis()
|
||||
onPress(true)
|
||||
this.tryAwaitRelease()
|
||||
val pressEndTime = System.currentTimeMillis()
|
||||
val totalPressTime = pressEndTime - pressStartTime
|
||||
if (totalPressTime < STORY_HOLD_DELAY) onPreviousStory()
|
||||
onPress(false)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
Box(
|
||||
Modifier
|
||||
.weight(1f)
|
||||
.fillMaxHeight()
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures(
|
||||
onPress = {
|
||||
val pressStartTime = System.currentTimeMillis()
|
||||
onPress(true)
|
||||
this.tryAwaitRelease()
|
||||
val pressEndTime = System.currentTimeMillis()
|
||||
val totalPressTime = pressEndTime - pressStartTime
|
||||
if (totalPressTime < STORY_HOLD_DELAY) onNextStory()
|
||||
onPress(false)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
package com.tangem.core.ui.components.stories.inner
|
||||
|
||||
import android.provider.Settings
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.AnimationVector1D
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.SpacerW4
|
||||
import com.tangem.core.ui.res.TangemColorPalette
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
private const val STORIES_ANIMATION_SPEED_ZERO_DURATION = 3000L
|
||||
const val STORY_DURATION = 8_000
|
||||
|
||||
private suspend fun animationProgress(
|
||||
paused: Boolean,
|
||||
animatorSpeed: Float,
|
||||
stepDuration: Int,
|
||||
progress: Animatable<Float, AnimationVector1D>,
|
||||
onStepFinish: () -> Unit,
|
||||
) {
|
||||
if (paused) {
|
||||
progress.stop()
|
||||
} else {
|
||||
if (animatorSpeed == 0f) {
|
||||
progress.snapTo(1f)
|
||||
delay(STORIES_ANIMATION_SPEED_ZERO_DURATION)
|
||||
} else {
|
||||
progress.animateTo(
|
||||
targetValue = 1f,
|
||||
animationSpec = tween(
|
||||
durationMillis = (stepDuration * (1f - progress.value)).toInt(),
|
||||
easing = LinearEasing,
|
||||
),
|
||||
)
|
||||
progress.snapTo(0f)
|
||||
}
|
||||
onStepFinish()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun StoriesProgressBar(
|
||||
steps: Int,
|
||||
currentStep: Int,
|
||||
paused: Boolean = false,
|
||||
stepDuration: Int = STORY_DURATION,
|
||||
onStepFinish: () -> Unit = {},
|
||||
) {
|
||||
val progress = remember(currentStep) { Animatable(initialValue = 0f) }
|
||||
|
||||
val context = LocalContext.current
|
||||
val animatorSpeed = Settings.Global.getFloat(
|
||||
context.contentResolver,
|
||||
Settings.Global.ANIMATOR_DURATION_SCALE,
|
||||
1f,
|
||||
)
|
||||
|
||||
LaunchedEffect(paused, currentStep, animatorSpeed) {
|
||||
animationProgress(
|
||||
paused = paused,
|
||||
animatorSpeed = animatorSpeed,
|
||||
stepDuration = stepDuration,
|
||||
progress = progress,
|
||||
onStepFinish = onStepFinish,
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
top = 16.dp,
|
||||
),
|
||||
) {
|
||||
for (index in 0..steps) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.height(2.dp)
|
||||
.weight(1f)
|
||||
.clip(RoundedCornerShape(2.dp))
|
||||
.background(TangemColorPalette.White.copy(alpha = .2f)),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(2.dp))
|
||||
.background(TangemColorPalette.White)
|
||||
.fillMaxHeight()
|
||||
.let {
|
||||
when (index) {
|
||||
currentStep -> it.fillMaxWidth(progress.value)
|
||||
in 0..currentStep -> it.fillMaxWidth(fraction = 1f)
|
||||
else -> it
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
SpacerW4()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun StoriesProgressBarPreview() {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.wrapContentSize()
|
||||
.background(TangemColorPalette.Black)
|
||||
.padding(vertical = TangemTheme.dimens.spacing16),
|
||||
) {
|
||||
StoriesProgressBar(steps = 5, currentStep = 3, paused = false)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package com.tangem.core.ui.components.stories.inner
|
||||
|
||||
import androidx.compose.runtime.IntState
|
||||
import androidx.compose.runtime.asIntState
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
class StoriesStepStateMachine<T>(
|
||||
private val stories: ImmutableList<T>,
|
||||
private val isRepeatable: Boolean,
|
||||
) {
|
||||
|
||||
private var _currentIndex = mutableIntStateOf(FIRST_INDEX)
|
||||
|
||||
val steps = stories.lastIndex
|
||||
val currentIndex: IntState
|
||||
get() = _currentIndex.asIntState()
|
||||
|
||||
val currentStory
|
||||
get() = stories[currentIndex.intValue.coerceIn(FIRST_INDEX, steps)]
|
||||
|
||||
fun nextStory(): Boolean {
|
||||
_currentIndex.intValue = if (currentIndex.intValue == steps && isRepeatable) {
|
||||
FIRST_INDEX
|
||||
} else {
|
||||
currentIndex.intValue.inc().coerceAtMost(stories.size)
|
||||
}
|
||||
|
||||
return currentIndex.intValue > steps
|
||||
}
|
||||
|
||||
fun prevStory() {
|
||||
_currentIndex.intValue = currentIndex.intValue.dec().coerceAtLeast(FIRST_INDEX)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val FIRST_INDEX = 0
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.core.ui.components.stories.model
|
||||
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
/**
|
||||
* Config for stories component
|
||||
*
|
||||
* @property stories configuration list
|
||||
* @property isRestartable indicates than stories progressions starts
|
||||
*/
|
||||
interface StoriesContentConfig<T : StoryConfig> {
|
||||
val stories: ImmutableList<T>
|
||||
val isRestartable: Boolean
|
||||
}
|
||||
|
||||
interface StoryConfig {
|
||||
val duration: Int
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
@ -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))
|
||||
|
|
@ -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
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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>()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
package com.tangem.core.ui.message
|
||||
|
||||
import androidx.compose.material3.SnackbarDuration
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.decompose.ui.UiMessage
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.message.SnackbarMessage.Duration
|
||||
|
||||
/**
|
||||
* Event that is used to show a message in the UI.
|
||||
|
|
@ -19,16 +22,40 @@ sealed interface EventMessage : UiMessage
|
|||
* Shows a snackbar.
|
||||
*
|
||||
* @param message The message to show.
|
||||
* @param duration The duration of the snackbar.
|
||||
* @param actionLabel The label of the action button.
|
||||
* @param action The action to perform when the action button is clicked.
|
||||
* @param duration The duration of the snackbar. Optional, [Duration.Short] by default.
|
||||
* @param onDismissRequest The action to perform when the snackbar is dismissed. Optional, empty lambda by default.
|
||||
* @param actionLabel The label of the action button. Optional, `null` by default.
|
||||
* @param action The action to perform when the action button is clicked. Optional, `null` by default.
|
||||
* */
|
||||
data class SnackbarMessage(
|
||||
val message: TextReference,
|
||||
val duration: SnackbarDuration = SnackbarDuration.Short,
|
||||
val duration: Duration = Duration.Short,
|
||||
val onDismissRequest: () -> Unit = {},
|
||||
val actionLabel: TextReference? = null,
|
||||
val action: (() -> Unit)? = null,
|
||||
) : EventMessage
|
||||
) : EventMessage {
|
||||
|
||||
/**
|
||||
* The duration of the snackbar.
|
||||
* */
|
||||
enum class Duration {
|
||||
|
||||
/**
|
||||
* Shows the snackbar for a short period of time.
|
||||
* */
|
||||
Short,
|
||||
|
||||
/**
|
||||
* Shows the snackbar for a long period of time.
|
||||
* */
|
||||
Long,
|
||||
|
||||
/**
|
||||
* Shows the snackbar indefinitely, until dismissed or action performed.
|
||||
* */
|
||||
Indefinite,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a [content] in the UI.
|
||||
|
|
@ -44,4 +71,170 @@ data class ContentMessage(val content: Content) : EventMessage {
|
|||
@Composable
|
||||
operator fun invoke(onDismiss: () -> Unit)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows an alert dialog.
|
||||
*
|
||||
* @param message The message to show.
|
||||
* @param title The title of the dialog. Optional, `null` by default.
|
||||
* @param firstAction The first dialog action to perform.
|
||||
* @param secondAction The second dialog action to perform. Optional, `null` by default.
|
||||
* Performing the second action always dismisses the dialog.
|
||||
* @param isDismissable If `false` then dialog can not be dismissed by back button click or by outside click.
|
||||
* @param dismissOnFirstAction If `true` then dialog is dismissed when first action is performed,
|
||||
* ignoring lambda passed to [EventMessageAction.onClick].
|
||||
* `true` by default.
|
||||
* @param onDismissRequest The action to perform when the dialog is dismissed.
|
||||
* */
|
||||
data class DialogMessage(
|
||||
val message: TextReference,
|
||||
val title: TextReference? = null,
|
||||
val firstAction: EventMessageAction,
|
||||
val secondAction: EventMessageAction? = null,
|
||||
val isDismissable: Boolean = true,
|
||||
val dismissOnFirstAction: Boolean = true,
|
||||
val onDismissRequest: () -> Unit = {},
|
||||
) : EventMessage {
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* Builder for [DialogMessage].
|
||||
*
|
||||
* @param message The message to show.
|
||||
* @param title The title of the dialog. Optional, `null` by default.
|
||||
* @param isDismissable If `false` then dialog can not be dismissed by back button click or by outside click.
|
||||
* `true` by default.
|
||||
* @param dismissOnFirstAction If `true` then dialog is dismissed when first action is performed,
|
||||
* ignoring lambda passed to [EventMessageAction.onClick].
|
||||
* `true` by default.
|
||||
* @param onDismissRequest The action to perform when the dialog is dismissed.
|
||||
* @param firstActionBuilder The builder for the first action.
|
||||
* By default, it builds an action with title "OK".
|
||||
* @param secondActionBuilder The builder for the second action. Optional, `null` by default.
|
||||
* Performing the second action always dismisses the dialog.
|
||||
*
|
||||
* @return [DialogMessage] with the specified parameters.
|
||||
* */
|
||||
operator fun invoke(
|
||||
message: TextReference,
|
||||
title: TextReference? = null,
|
||||
isDismissable: Boolean = true,
|
||||
dismissOnFirstAction: Boolean = true,
|
||||
onDismissRequest: () -> Unit = {},
|
||||
firstActionBuilder: EventMessageAction.BuilderScope.() -> EventMessageAction = { okAction() },
|
||||
secondActionBuilder: (EventMessageAction.BuilderScope.() -> EventMessageAction)? = null,
|
||||
): DialogMessage {
|
||||
val buttonsScope = EventMessageAction.BuilderScope(onDismissRequest)
|
||||
|
||||
return DialogMessage(
|
||||
message = message,
|
||||
title = title,
|
||||
isDismissable = isDismissable,
|
||||
dismissOnFirstAction = dismissOnFirstAction,
|
||||
onDismissRequest = onDismissRequest,
|
||||
firstAction = firstActionBuilder(buttonsScope),
|
||||
secondAction = secondActionBuilder?.invoke(buttonsScope),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a bottom sheet.
|
||||
*
|
||||
* @param iconResId The icon to show in the bottom sheet.
|
||||
* Optional, `null` by default.
|
||||
* @param title The title of the bottom sheet. Optional, `null` by default.
|
||||
* @param message The message to show in the bottom sheet.
|
||||
* @param firstAction The first action to perform. Optional, `null` by default.
|
||||
* @param secondAction The second action to perform. Optional, `null` by default.
|
||||
* @param onDismissRequest The action to perform when the bottom sheet is dismissed.
|
||||
* */
|
||||
data class BottomSheetMessage(
|
||||
@DrawableRes val iconResId: Int? = null,
|
||||
val title: TextReference? = null,
|
||||
val message: TextReference,
|
||||
val firstAction: EventMessageAction? = null,
|
||||
val secondAction: EventMessageAction? = null,
|
||||
val onDismissRequest: () -> Unit = {},
|
||||
) : EventMessage {
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* Builder for [BottomSheetMessage].
|
||||
*
|
||||
* @param iconResId The icon to show in the bottom sheet. Optional, `null` by default.
|
||||
* @param title The title of the bottom sheet. Optional, `null` by default.
|
||||
* @param message The message to show in the bottom sheet.
|
||||
* @param onDismissRequest The action to perform when the bottom sheet is dismissed.
|
||||
* @param firstActionBuilder The builder for the first action. Optional, `null` by default.
|
||||
* @param secondActionBuilder The builder for the second action. Optional, `null` by default.
|
||||
* */
|
||||
operator fun invoke(
|
||||
@DrawableRes iconResId: Int?,
|
||||
title: TextReference?,
|
||||
message: TextReference,
|
||||
onDismissRequest: () -> Unit = {},
|
||||
firstActionBuilder: (EventMessageAction.BuilderScope.() -> EventMessageAction)? = null,
|
||||
secondActionBuilder: (EventMessageAction.BuilderScope.() -> EventMessageAction)? = null,
|
||||
): BottomSheetMessage {
|
||||
val buttonsScope = EventMessageAction.BuilderScope(onDismissRequest)
|
||||
|
||||
return BottomSheetMessage(
|
||||
iconResId = iconResId,
|
||||
title = title,
|
||||
message = message,
|
||||
onDismissRequest = onDismissRequest,
|
||||
firstAction = firstActionBuilder?.invoke(buttonsScope),
|
||||
secondAction = secondActionBuilder?.invoke(buttonsScope),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an action button in the dialog.
|
||||
*
|
||||
* @param title The title of the action.
|
||||
* @param warning If `true` then the action is highlighted as a warning.
|
||||
* @param enabled If `false` then the action is disabled.
|
||||
* @param onClick The action to perform when the button is clicked.
|
||||
* */
|
||||
data class EventMessageAction(
|
||||
val title: TextReference,
|
||||
val warning: Boolean = false,
|
||||
val enabled: Boolean = true,
|
||||
val onClick: () -> Unit,
|
||||
) {
|
||||
|
||||
/**
|
||||
* Scope for building buttons for the message.
|
||||
*
|
||||
* @param onDismissRequest The action to perform when the dialog is dismissed.
|
||||
* */
|
||||
class BuilderScope internal constructor(val onDismissRequest: () -> Unit) {
|
||||
|
||||
/**
|
||||
* Builds an action with title "OK".
|
||||
*
|
||||
* @param onClick The action to perform when the button is clicked. By default, it dismisses the message.
|
||||
* */
|
||||
fun okAction(onClick: () -> Unit = onDismissRequest) = EventMessageAction(
|
||||
title = resourceReference(id = R.string.common_ok),
|
||||
onClick = onClick,
|
||||
)
|
||||
|
||||
/**
|
||||
* Builds an action with title "Cancel".
|
||||
*
|
||||
* @param onClick The action to perform when the button is clicked. By default, it dismisses the message.
|
||||
* */
|
||||
fun cancelAction(onClick: () -> Unit = onDismissRequest) = EventMessageAction(
|
||||
title = resourceReference(id = R.string.common_cancel),
|
||||
onClick = onClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,26 +1,49 @@
|
|||
package com.tangem.core.ui.message
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.material3.SnackbarDuration
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.SnackbarResult
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import com.tangem.core.ui.components.BasicDialog
|
||||
import com.tangem.core.ui.components.DialogButtonUM
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUM
|
||||
import com.tangem.core.ui.event.EventEffect
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.LocalEventMessageHandler
|
||||
import com.tangem.core.ui.res.LocalSnackbarHostState
|
||||
|
||||
@Composable
|
||||
fun EventMessageEffect(messageHandler: EventMessageHandler, snackbarHostState: SnackbarHostState) {
|
||||
fun EventMessageEffect(
|
||||
messageHandler: EventMessageHandler = LocalEventMessageHandler.current,
|
||||
snackbarHostState: SnackbarHostState = LocalSnackbarHostState.current,
|
||||
onShowSnackbar: suspend (SnackbarMessage, Context) -> Unit = { message, context ->
|
||||
showSnackbar(snackbarHostState, message, context)
|
||||
},
|
||||
) {
|
||||
val messageEvent by messageHandler.collectAsState()
|
||||
val context = LocalContext.current
|
||||
|
||||
var contentMessage: ContentMessage? by remember { mutableStateOf(value = null) }
|
||||
var dialogMessage: DialogMessage? by remember { mutableStateOf(value = null) }
|
||||
var bottomSheetMessage: BottomSheetMessage? by remember { mutableStateOf(value = null) }
|
||||
|
||||
EventEffect(event = messageEvent) { message ->
|
||||
when (message) {
|
||||
is SnackbarMessage -> {
|
||||
onShowSnackbar(message, context)
|
||||
}
|
||||
is ContentMessage -> {
|
||||
contentMessage = message
|
||||
}
|
||||
is SnackbarMessage -> {
|
||||
showSnackbar(snackbarHostState, message, context)
|
||||
is DialogMessage -> {
|
||||
dialogMessage = message
|
||||
}
|
||||
is BottomSheetMessage -> {
|
||||
bottomSheetMessage = message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -28,16 +51,110 @@ fun EventMessageEffect(messageHandler: EventMessageHandler, snackbarHostState: S
|
|||
contentMessage?.content?.invoke(
|
||||
onDismiss = { contentMessage = null },
|
||||
)
|
||||
|
||||
dialogMessage?.let { message ->
|
||||
MessageDialog(
|
||||
message = message,
|
||||
onDismissRequest = {
|
||||
dialogMessage = null
|
||||
message.onDismissRequest()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
bottomSheetMessage?.let { message ->
|
||||
MessageBottomSheet(
|
||||
message = message,
|
||||
onDismissRequest = {
|
||||
bottomSheetMessage = null
|
||||
message.onDismissRequest()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MessageBottomSheet(message: BottomSheetMessage, onDismissRequest: () -> Unit) {
|
||||
val config = TangemBottomSheetConfig(
|
||||
isShown = true,
|
||||
content = MessageBottomSheetUM(
|
||||
iconResId = message.iconResId,
|
||||
title = message.title,
|
||||
message = message.message,
|
||||
primaryAction = message.firstAction?.let { action ->
|
||||
MessageBottomSheetUM.ActionUM(
|
||||
text = action.title,
|
||||
enabled = action.enabled,
|
||||
onClick = {
|
||||
action.onClick()
|
||||
onDismissRequest()
|
||||
},
|
||||
)
|
||||
},
|
||||
secondaryAction = message.secondAction?.let { action ->
|
||||
MessageBottomSheetUM.ActionUM(
|
||||
text = action.title,
|
||||
enabled = action.enabled,
|
||||
onClick = {
|
||||
action.onClick()
|
||||
onDismissRequest()
|
||||
},
|
||||
)
|
||||
},
|
||||
),
|
||||
onDismissRequest = onDismissRequest,
|
||||
)
|
||||
|
||||
MessageBottomSheet(config)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MessageDialog(message: DialogMessage, onDismissRequest: () -> Unit) {
|
||||
BasicDialog(
|
||||
message = message.message.resolveReference(),
|
||||
title = message.title?.resolveReference(),
|
||||
confirmButton = message.firstAction.let { action ->
|
||||
DialogButtonUM(
|
||||
title = action.title.resolveReference(),
|
||||
warning = action.warning,
|
||||
enabled = action.enabled,
|
||||
onClick = {
|
||||
action.onClick()
|
||||
|
||||
if (message.dismissOnFirstAction) {
|
||||
onDismissRequest()
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
dismissButton = message.secondAction?.let { action ->
|
||||
DialogButtonUM(
|
||||
title = action.title.resolveReference(),
|
||||
warning = action.warning,
|
||||
enabled = action.enabled,
|
||||
onClick = {
|
||||
action.onClick()
|
||||
onDismissRequest()
|
||||
},
|
||||
)
|
||||
},
|
||||
onDismissDialog = onDismissRequest,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun showSnackbar(snackbarHostState: SnackbarHostState, message: SnackbarMessage, context: Context) {
|
||||
val result = snackbarHostState.showSnackbar(
|
||||
message = message.message.resolveReference(context.resources),
|
||||
actionLabel = message.actionLabel?.resolveReference(context.resources),
|
||||
duration = message.duration,
|
||||
duration = when (message.duration) {
|
||||
SnackbarMessage.Duration.Short -> SnackbarDuration.Short
|
||||
SnackbarMessage.Duration.Long -> SnackbarDuration.Long
|
||||
SnackbarMessage.Duration.Indefinite -> SnackbarDuration.Indefinite
|
||||
},
|
||||
)
|
||||
|
||||
if (result == SnackbarResult.ActionPerformed) {
|
||||
message.action?.invoke()
|
||||
when (result) {
|
||||
SnackbarResult.Dismissed -> message.onDismissRequest()
|
||||
SnackbarResult.ActionPerformed -> message.action?.invoke()
|
||||
}
|
||||
}
|
||||
|
|
@ -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,42 @@ 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.message.EventMessageHandler
|
||||
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,
|
||||
eventMessageHandler = uiDependencies.eventMessageHandler,
|
||||
overrideSystemBarColors = overrideSystemBarColors,
|
||||
typography = typography,
|
||||
dimens = dimens,
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TangemTheme(
|
||||
isDark: Boolean = false,
|
||||
|
|
@ -24,6 +55,7 @@ fun TangemTheme(
|
|||
typography: TangemTypography = TangemTheme.typography,
|
||||
dimens: TangemDimens = TangemTheme.dimens,
|
||||
vibratorHapticManager: VibratorHapticManager? = null,
|
||||
eventMessageHandler: EventMessageHandler = remember { EventMessageHandler() },
|
||||
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
|
||||
overrideSystemBarColors: Boolean = true,
|
||||
content: @Composable () -> Unit,
|
||||
|
|
@ -64,6 +96,7 @@ fun TangemTheme(
|
|||
LocalIsInDarkTheme provides isDark,
|
||||
LocalHapticManager provides hapticManager,
|
||||
LocalSnackbarHostState provides snackbarHostState,
|
||||
LocalEventMessageHandler provides eventMessageHandler,
|
||||
LocalWindowSize provides windowSize,
|
||||
) {
|
||||
CompositionLocalProvider(
|
||||
|
|
@ -256,6 +289,10 @@ val LocalSnackbarHostState = staticCompositionLocalOf<SnackbarHostState> {
|
|||
error("No SnackbarHostState provided")
|
||||
}
|
||||
|
||||
val LocalEventMessageHandler = staticCompositionLocalOf<EventMessageHandler> {
|
||||
error("No EventMessageHandler provided")
|
||||
}
|
||||
|
||||
val LocalWindowSize = staticCompositionLocalOf<WindowSize> {
|
||||
error("No WindowSize provided")
|
||||
}
|
||||
|
|
@ -266,4 +303,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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
10
core/ui/src/main/res/drawable/ic_fact0rn_22.xml
Normal file
10
core/ui/src/main/res/drawable/ic_fact0rn_22.xml
Normal 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>
|
||||
BIN
core/ui/src/main/res/drawable/img_card_visa.webp
Normal file
BIN
core/ui/src/main/res/drawable/img_card_visa.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 70 KiB |
20
core/ui/src/main/res/drawable/img_fact0rn_22.xml
Normal file
20
core/ui/src/main/res/drawable/img_fact0rn_22.xml
Normal 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>
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue