Updated on 2026-08-14
This commit is contained in:
commit
ff09b9f79b
361 changed files with 10298 additions and 8064 deletions
|
|
@ -0,0 +1,43 @@
|
|||
package com.tangem.datasource.api.common
|
||||
|
||||
import com.tangem.datasource.api.common.config.ApiConfig
|
||||
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import okio.IOException
|
||||
|
||||
/**
|
||||
* Switch base url [Interceptor]
|
||||
*
|
||||
* @property id api config id [ApiConfig.ID]
|
||||
* @property apiConfigsManager api configs manager
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class SwitchBaseUrlInterceptor(
|
||||
private val id: ApiConfig.ID,
|
||||
private val apiConfigsManager: ApiConfigsManager,
|
||||
) : Interceptor {
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
var request = chain.request()
|
||||
val builder = request.newBuilder()
|
||||
|
||||
request = builder
|
||||
.url(url = request.url.adjustBaseUrl())
|
||||
.build()
|
||||
|
||||
return chain.proceed(request)
|
||||
}
|
||||
|
||||
private fun HttpUrl.adjustBaseUrl(): HttpUrl {
|
||||
val host = apiConfigsManager.getBaseUrl(id).toHttpUrl().host
|
||||
|
||||
return this.newBuilder()
|
||||
.host(host)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
package com.tangem.datasource.api.common.config
|
||||
|
||||
/**
|
||||
* Api config
|
||||
*
|
||||
* @property currentEnvironment current api environment
|
||||
*
|
||||
* @see <a href="https://www.notion.so/tangem/API-eacb264e7daf420a88b419a8a26f5b26?pvs=4">API configuration</a>
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
sealed class ApiConfig(open val currentEnvironment: ApiEnvironment) {
|
||||
|
||||
/** Available environments with base url */
|
||||
abstract val environments: Map<ApiEnvironment, String>
|
||||
|
||||
/** Unique id */
|
||||
val id: ID = initializeId()
|
||||
|
||||
enum class ID {
|
||||
Express,
|
||||
TangemTech,
|
||||
}
|
||||
|
||||
/** Copy method for sealed class [ApiConfig] */
|
||||
fun copySealed(currentEnvironment: ApiEnvironment): ApiConfig {
|
||||
return when (this) {
|
||||
is Express -> copy(currentEnvironment = currentEnvironment)
|
||||
is TangemTech -> copy(currentEnvironment = currentEnvironment)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initializeId(): ID {
|
||||
return when (this) {
|
||||
is Express -> ID.Express
|
||||
is TangemTech -> ID.TangemTech
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
internal const val DEBUG_BUILD_TYPE = "debug"
|
||||
internal const val INTERNAL_BUILD_TYPE = "internal"
|
||||
internal const val MOCKED_BUILD_TYPE = "mocked"
|
||||
internal const val EXTERNAL_BUILD_TYPE = "external"
|
||||
internal const val RELEASE_BUILD_TYPE = "release"
|
||||
|
||||
/** All api configs */
|
||||
fun values() = listOf(Express(), TangemTech())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.datasource.api.common.config
|
||||
|
||||
/**
|
||||
* Api environment
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
enum class ApiEnvironment {
|
||||
DEV, STAGE, PROD
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package com.tangem.datasource.api.common.config
|
||||
|
||||
import com.tangem.datasource.BuildConfig
|
||||
|
||||
/**
|
||||
* Express [ApiConfig]
|
||||
*
|
||||
* @property currentEnvironment current api environment
|
||||
*/
|
||||
internal data class Express(
|
||||
override val currentEnvironment: ApiEnvironment = initializeCurrentEnvironment(),
|
||||
) : ApiConfig(currentEnvironment) {
|
||||
|
||||
override val environments: Map<ApiEnvironment, String> = mapOf(
|
||||
ApiEnvironment.DEV to "[REDACTED_ENV_URL]",
|
||||
ApiEnvironment.STAGE to "[REDACTED_ENV_URL]",
|
||||
ApiEnvironment.PROD to "https://express.tangem.com/v1/",
|
||||
)
|
||||
|
||||
private companion object {
|
||||
|
||||
fun initializeCurrentEnvironment(): ApiEnvironment {
|
||||
return when (BuildConfig.BUILD_TYPE) {
|
||||
DEBUG_BUILD_TYPE -> ApiEnvironment.DEV
|
||||
INTERNAL_BUILD_TYPE,
|
||||
MOCKED_BUILD_TYPE,
|
||||
-> ApiEnvironment.STAGE
|
||||
EXTERNAL_BUILD_TYPE,
|
||||
RELEASE_BUILD_TYPE,
|
||||
-> ApiEnvironment.PROD
|
||||
else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package com.tangem.datasource.api.common.config
|
||||
|
||||
import com.tangem.datasource.BuildConfig
|
||||
|
||||
/**
|
||||
* TangemTech [ApiConfig]
|
||||
*
|
||||
* @property currentEnvironment current api environment
|
||||
*/
|
||||
internal data class TangemTech(
|
||||
override val currentEnvironment: ApiEnvironment = initializeCurrentEnvironment(),
|
||||
) : ApiConfig(currentEnvironment) {
|
||||
|
||||
override val environments: Map<ApiEnvironment, String> = mapOf(
|
||||
ApiEnvironment.DEV to "https://devapi.tangem-tech.com/v1/",
|
||||
ApiEnvironment.PROD to "https://api.tangem-tech.com/v1/",
|
||||
)
|
||||
|
||||
private companion object {
|
||||
|
||||
fun initializeCurrentEnvironment(): ApiEnvironment {
|
||||
return when (BuildConfig.BUILD_TYPE) {
|
||||
DEBUG_BUILD_TYPE,
|
||||
INTERNAL_BUILD_TYPE,
|
||||
-> ApiEnvironment.DEV
|
||||
MOCKED_BUILD_TYPE,
|
||||
EXTERNAL_BUILD_TYPE,
|
||||
RELEASE_BUILD_TYPE,
|
||||
-> ApiEnvironment.PROD
|
||||
else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.tangem.datasource.api.common.config.managers
|
||||
|
||||
import com.tangem.datasource.api.common.config.ApiConfig
|
||||
|
||||
/**
|
||||
* Api configs manager
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface ApiConfigsManager {
|
||||
|
||||
/** Initialize resources */
|
||||
suspend fun initialize() {}
|
||||
|
||||
/** Get base url of api by [id] */
|
||||
fun getBaseUrl(id: ApiConfig.ID): String
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
package com.tangem.datasource.api.common.config.managers
|
||||
|
||||
import com.tangem.datasource.api.common.config.ApiConfig
|
||||
import com.tangem.datasource.api.common.config.ApiEnvironment
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.datasource.local.preferences.utils.getObjectMap
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
||||
/**
|
||||
* Implementation of [ApiConfigsManager] in DEV environment
|
||||
*
|
||||
* @property appPreferencesStore app preferences store
|
||||
* @property dispatchers coroutine dispatcher provider
|
||||
*/
|
||||
internal class DevApiConfigsManager(
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : MutableApiConfigsManager {
|
||||
|
||||
override val configs: Flow<List<ApiConfig>> get() = _apiConfigs
|
||||
|
||||
private val _apiConfigs = MutableStateFlow(value = ApiConfig.values())
|
||||
|
||||
override suspend fun initialize() {
|
||||
// We can't use appPreferencesStore.getObjectMap as base flow,
|
||||
// because we should keep possibility to work with configs synchronous.
|
||||
// See [getBaseUrl]
|
||||
appPreferencesStore.getObjectMap<ApiEnvironment>(PreferencesKeys.apiConfigsEnvironmentKey)
|
||||
.onEach { savedEnvironments ->
|
||||
_apiConfigs.value = _apiConfigs.value.map { config ->
|
||||
val savedEnvironment = savedEnvironments[config.id.name]
|
||||
|
||||
if (savedEnvironment != null) {
|
||||
config.copySealed(currentEnvironment = savedEnvironment)
|
||||
} else {
|
||||
config
|
||||
}
|
||||
}
|
||||
}
|
||||
.launchIn(CoroutineScope(dispatchers.main))
|
||||
}
|
||||
|
||||
override fun getBaseUrl(id: ApiConfig.ID): String {
|
||||
val config = _apiConfigs.value.firstOrNull { it.id == id }
|
||||
?: error("Api config with id [$id] not found. Check ApiConfig implementations")
|
||||
|
||||
return config.environments[config.currentEnvironment]
|
||||
?: error(
|
||||
"Api config with id [$id] doesn't contain environment [${config.currentEnvironment}]. " +
|
||||
"Check ApiConfig implementations",
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun changeEnvironment(id: String, environment: ApiEnvironment) {
|
||||
appPreferencesStore.editData { mutablePreferences ->
|
||||
val updatedMap = mutablePreferences.getObjectMap<ApiEnvironment>(PreferencesKeys.apiConfigsEnvironmentKey)
|
||||
.toMutableMap()
|
||||
.apply { put(id, environment) }
|
||||
|
||||
mutablePreferences.setObjectMap(PreferencesKeys.apiConfigsEnvironmentKey, updatedMap)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.tangem.datasource.api.common.config.managers
|
||||
|
||||
import com.tangem.datasource.api.common.config.ApiConfig
|
||||
import com.tangem.datasource.api.common.config.ApiEnvironment
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* Mutable [ApiConfigsManager] for change information about the current api environment
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface MutableApiConfigsManager : ApiConfigsManager {
|
||||
|
||||
/** Configs */
|
||||
val configs: Flow<List<ApiConfig>>
|
||||
|
||||
/** Change api environment [environment] by [id] */
|
||||
suspend fun changeEnvironment(id: String, environment: ApiEnvironment)
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.datasource.api.common.config.managers
|
||||
|
||||
import com.tangem.datasource.api.common.config.ApiConfig
|
||||
|
||||
/** Implementation of [ApiConfigsManager] in PROD environment */
|
||||
internal class ProdApiConfigsManager : ApiConfigsManager {
|
||||
|
||||
override fun getBaseUrl(id: ApiConfig.ID): String {
|
||||
val config = ApiConfig.values().firstOrNull { it.id == id }
|
||||
?: error("Api config with id [$id] not found. Check ApiConfig implementations")
|
||||
|
||||
return config.environments[config.currentEnvironment]
|
||||
?: error(
|
||||
"Api config with id [$id] doesn't contain " +
|
||||
"environment [${config.currentEnvironment}]. Check ApiConfig implementations",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -16,18 +16,20 @@ interface TangemTechMarketsApi {
|
|||
@Query("offset") offset: Int,
|
||||
@Query("limit") limit: Int,
|
||||
@Query("order") order: String,
|
||||
@Query("general_coins") generalCoins: Boolean,
|
||||
@Query("search") search: String?,
|
||||
@Query("timestamp") timestamp: Long?,
|
||||
): ApiResponse<TokenMarketListResponse>
|
||||
|
||||
@GET("coins/{coin_id}")
|
||||
suspend fun getCoinMarketData(
|
||||
@Path("coin_id") coinId: String,
|
||||
@Query("currency") currency: String,
|
||||
): ApiResponse<TokenMarketDetailsResponse>
|
||||
@Query("language") language: String,
|
||||
): ApiResponse<TokenMarketInfoResponse>
|
||||
|
||||
@GET("coins/{coin_id}/history")
|
||||
suspend fun getCoinChart(
|
||||
@Path("coin_id") coinId: String,
|
||||
@Query("currency") currency: String,
|
||||
@Query("interval") interval: String,
|
||||
): ApiResponse<TokenMarketChartResponse>
|
||||
|
|
|
|||
|
|
@ -3,130 +3,129 @@ package com.tangem.datasource.api.markets.models.response
|
|||
import com.squareup.moshi.Json
|
||||
import java.math.BigDecimal
|
||||
|
||||
data class TokenMarketDetailsResponse(
|
||||
data class TokenMarketInfoResponse(
|
||||
@Json(name = "id")
|
||||
val id: String,
|
||||
@Json(name = "name")
|
||||
val name: String,
|
||||
@Json(name = "symbol")
|
||||
val symbol: String,
|
||||
@Json(name = "active")
|
||||
val active: Boolean,
|
||||
@Json(name = "current_price")
|
||||
val currentPrice: BigDecimal,
|
||||
@Json(name = "price_change_percentage")
|
||||
val priceChangePercentage: PriceChangePercentage,
|
||||
val priceChangePercentage: PriceChangePercentage?,
|
||||
@Json(name = "networks")
|
||||
val networks: List<Network>,
|
||||
val networks: List<Network>?,
|
||||
@Json(name = "short_description")
|
||||
val shortDescription: String?,
|
||||
@Json(name = "full_description")
|
||||
val fullDescription: String?,
|
||||
@Json(name = "insights")
|
||||
val insights: List<Insight>?,
|
||||
val insights: Insights?,
|
||||
@Json(name = "metrics")
|
||||
val metrics: Metrics,
|
||||
val metrics: Metrics?,
|
||||
@Json(name = "links")
|
||||
val links: Links,
|
||||
val links: Links?,
|
||||
@Json(name = "price_performance")
|
||||
val pricePerformance: PricePerformance,
|
||||
val pricePerformance: PricePerformance?,
|
||||
) {
|
||||
|
||||
data class PriceChangePercentage(
|
||||
@Json(name = "24h")
|
||||
val h24: BigDecimal,
|
||||
val day: BigDecimal?,
|
||||
@Json(name = "1w")
|
||||
val week1: BigDecimal,
|
||||
val week: BigDecimal?,
|
||||
@Json(name = "1m")
|
||||
val month1: BigDecimal,
|
||||
val month: BigDecimal?,
|
||||
@Json(name = "3m")
|
||||
val month3: BigDecimal,
|
||||
val threeMonths: BigDecimal?,
|
||||
@Json(name = "6m")
|
||||
val month6: BigDecimal,
|
||||
val sixMonths: BigDecimal?,
|
||||
@Json(name = "1y")
|
||||
val year1: BigDecimal,
|
||||
val year: BigDecimal?,
|
||||
@Json(name = "all_time")
|
||||
val allTime: BigDecimal,
|
||||
val allTime: BigDecimal?,
|
||||
)
|
||||
|
||||
data class Network(
|
||||
@Json(name = "network_id")
|
||||
val networkId: String,
|
||||
@Json(name = "exchangeable")
|
||||
val exchangeable: Boolean,
|
||||
val exchangeable: Boolean = false,
|
||||
@Json(name = "contract_address")
|
||||
val contractAddress: String,
|
||||
@Json(name = "decimalCount")
|
||||
val decimalCount: Int,
|
||||
val contractAddress: String?,
|
||||
@Json(name = "decimal_count")
|
||||
val decimalCount: Int?,
|
||||
)
|
||||
|
||||
data class Insight(
|
||||
data class Insights(
|
||||
@Json(name = "holders_change")
|
||||
val holdersChange: Change,
|
||||
val holdersChange: Change?,
|
||||
@Json(name = "liquidity_change")
|
||||
val liquidityChange: Change,
|
||||
val liquidityChange: Change?,
|
||||
@Json(name = "buy_pressure_change")
|
||||
val buyPressureChange: Change,
|
||||
val buyPressureChange: Change?,
|
||||
@Json(name = "experienced_buyer_change")
|
||||
val experiencedBuyerChange: Change,
|
||||
) {
|
||||
data class Change(
|
||||
@Json(name = "1d")
|
||||
val day1: Int,
|
||||
@Json(name = "1w")
|
||||
val week1: Int,
|
||||
@Json(name = "1m")
|
||||
val month1: Int,
|
||||
)
|
||||
}
|
||||
val experiencedBuyerChange: Change?,
|
||||
)
|
||||
|
||||
data class Change(
|
||||
@Json(name = "24h")
|
||||
val day: BigDecimal?,
|
||||
@Json(name = "1w")
|
||||
val week: BigDecimal?,
|
||||
@Json(name = "1m")
|
||||
val month: BigDecimal?,
|
||||
)
|
||||
|
||||
data class Metrics(
|
||||
@Json(name = "market_rating")
|
||||
val marketRating: Int,
|
||||
val marketRating: Int?,
|
||||
@Json(name = "circulating_supply")
|
||||
val circulatingSupply: BigDecimal,
|
||||
val circulatingSupply: BigDecimal?,
|
||||
@Json(name = "market_cap")
|
||||
val marketCap: BigDecimal,
|
||||
val marketCap: BigDecimal?,
|
||||
@Json(name = "volume_24h")
|
||||
val volume24h: BigDecimal,
|
||||
val volume24h: BigDecimal?,
|
||||
@Json(name = "total_supply")
|
||||
val totalSupply: BigDecimal,
|
||||
val totalSupply: BigDecimal?,
|
||||
@Json(name = "fully_diluted_valuation")
|
||||
val fullyDilutedValuation: BigDecimal,
|
||||
val fullyDilutedValuation: BigDecimal?,
|
||||
)
|
||||
|
||||
data class Links(
|
||||
@Json(name = "official_links")
|
||||
val officialLinks: List<Link> = emptyList(),
|
||||
val officialLinks: List<Link>?,
|
||||
@Json(name = "social")
|
||||
val social: List<Link> = emptyList(),
|
||||
val social: List<Link>?,
|
||||
@Json(name = "repository")
|
||||
val repository: List<Link> = emptyList(),
|
||||
val repository: List<Link>?,
|
||||
@Json(name = "blockchain_site")
|
||||
val blockchainSite: List<Link> = emptyList(),
|
||||
val blockchainSite: List<Link>?,
|
||||
)
|
||||
|
||||
data class Link(
|
||||
@Json(name = "title")
|
||||
val title: String?,
|
||||
val title: String,
|
||||
@Json(name = "id")
|
||||
val id: String,
|
||||
val id: String?,
|
||||
@Json(name = "link")
|
||||
val url: String,
|
||||
val link: String,
|
||||
)
|
||||
|
||||
data class PricePerformance(
|
||||
@Json(name = "high_price")
|
||||
val highPrice: Price,
|
||||
@Json(name = "24h")
|
||||
val day: Range?,
|
||||
@Json(name = "1m")
|
||||
val month: Range?,
|
||||
@Json(name = "all_time")
|
||||
val allTime: Range?,
|
||||
)
|
||||
|
||||
data class Range(
|
||||
@Json(name = "low_price")
|
||||
val lowPrice: Price,
|
||||
) {
|
||||
data class Price(
|
||||
@Json(name = "24h")
|
||||
val h24: BigDecimal,
|
||||
@Json(name = "1m")
|
||||
val month1: BigDecimal,
|
||||
@Json(name = "all_time")
|
||||
val allTime: BigDecimal,
|
||||
)
|
||||
}
|
||||
val low: BigDecimal?,
|
||||
@Json(name = "high_price")
|
||||
val high: BigDecimal?,
|
||||
)
|
||||
}
|
||||
|
|
@ -14,6 +14,8 @@ data class TokenMarketListResponse(
|
|||
val limit: Int,
|
||||
@Json(name = "offset")
|
||||
val offset: Int,
|
||||
@Json(name = "timestamp")
|
||||
val timestamp: Long? = null,
|
||||
) {
|
||||
data class Token(
|
||||
@Json(name = "id")
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.tangem.datasource.api.stakekit.models.response.model
|
|||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.math.BigDecimal
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class AddressArgumentDTO(
|
||||
|
|
@ -10,7 +11,7 @@ data class AddressArgumentDTO(
|
|||
@Json(name = "network")
|
||||
val network: String? = null,
|
||||
@Json(name = "minimum")
|
||||
val minimum: Double? = null,
|
||||
val minimum: BigDecimal? = null,
|
||||
@Json(name = "maximum")
|
||||
val maximum: Double? = null,
|
||||
val maximum: BigDecimal? = null,
|
||||
)
|
||||
|
|
@ -16,7 +16,7 @@ class StakeKitErrorResponse(
|
|||
@Json(name = "code")
|
||||
val code: String? = null,
|
||||
@Json(name = "countryCode")
|
||||
val countryCode: String,
|
||||
val countryCode: String?,
|
||||
@Json(name = "regionCode")
|
||||
val regionCode: String? = null,
|
||||
@Json(name = "tags")
|
||||
|
|
|
|||
|
|
@ -154,6 +154,8 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager {
|
|||
zkSyncEra = GetBlockAccessToken(rest = accessTokens.zksync?.jsonRPC),
|
||||
polygonZkEvm = GetBlockAccessToken(rest = accessTokens.polygonZkevm?.jsonRPC),
|
||||
base = GetBlockAccessToken(rest = accessTokens.base?.jsonRPC),
|
||||
blast = GetBlockAccessToken(jsonRpc = accessTokens.blast?.jsonRPC),
|
||||
filecoin = GetBlockAccessToken(jsonRpc = accessTokens.filecoin?.jsonRPC),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,6 +76,8 @@ data class GetBlockAccessTokens(
|
|||
@Json(name = "polygon-zkevm") val polygonZkevm: GetBlockToken?,
|
||||
@Json(name = "zksync") val zksync: GetBlockToken?,
|
||||
@Json(name = "base") val base: GetBlockToken?,
|
||||
@Json(name = "blast") val blast: GetBlockToken?,
|
||||
@Json(name = "filecoin") val filecoin: GetBlockToken?,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,10 @@ package com.tangem.datasource.di
|
|||
import android.content.Context
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.datasource.BuildConfig
|
||||
import com.tangem.datasource.api.common.config.ApiConfig
|
||||
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
|
||||
import com.tangem.datasource.api.common.config.managers.DevApiConfigsManager
|
||||
import com.tangem.datasource.api.common.config.managers.ProdApiConfigsManager
|
||||
import com.tangem.datasource.api.common.response.ApiResponseCallAdapterFactory
|
||||
import com.tangem.datasource.api.express.TangemExpressApi
|
||||
import com.tangem.datasource.api.markets.TangemTechMarketsApi
|
||||
|
|
@ -10,12 +14,15 @@ 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.tangemTech.TangemTechServiceApi
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.utils.RequestHeader
|
||||
import com.tangem.datasource.utils.RequestHeader.*
|
||||
import com.tangem.datasource.utils.addEnvironmentSwitcher
|
||||
import com.tangem.datasource.utils.addHeaders
|
||||
import com.tangem.datasource.utils.addLoggers
|
||||
import com.tangem.lib.auth.ExpressAuthProvider
|
||||
import com.tangem.lib.auth.StakeKitAuthProvider
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.version.AppVersionProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
|
@ -32,6 +39,19 @@ import javax.inject.Singleton
|
|||
@InstallIn(SingletonComponent::class)
|
||||
class NetworkModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideApiConfigManager(
|
||||
appPreferencesStore: AppPreferencesStore,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): ApiConfigsManager {
|
||||
return if (BuildConfig.TESTER_MENU_ENABLED) {
|
||||
DevApiConfigsManager(appPreferencesStore, dispatchers)
|
||||
} else {
|
||||
ProdApiConfigsManager()
|
||||
}
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideExpressApi(
|
||||
|
|
@ -39,18 +59,15 @@ class NetworkModule {
|
|||
@ApplicationContext context: Context,
|
||||
expressAuthProvider: ExpressAuthProvider,
|
||||
appVersionProvider: AppVersionProvider,
|
||||
apiConfigsManager: ApiConfigsManager,
|
||||
): TangemExpressApi {
|
||||
val url = if (BuildConfig.ENVIRONMENT == "dev") {
|
||||
STAGE_EXPRESS_BASE_URL
|
||||
} else {
|
||||
PROD_EXPRESS_BASE_URL
|
||||
}
|
||||
return Retrofit.Builder()
|
||||
.addConverterFactory(MoshiConverterFactory.create(moshi))
|
||||
.addCallAdapterFactory(ApiResponseCallAdapterFactory.create())
|
||||
.baseUrl(url)
|
||||
.baseUrl(apiConfigsManager.getBaseUrl(id = ApiConfig.ID.Express))
|
||||
.client(
|
||||
OkHttpClient.Builder()
|
||||
.addEnvironmentSwitcher(ApiConfig.ID.Express, apiConfigsManager)
|
||||
.addHeaders(Express(expressAuthProvider))
|
||||
.addHeaders(AppVersionPlatformHeaders(appVersionProvider))
|
||||
.addLoggers(context)
|
||||
|
|
@ -87,8 +104,15 @@ class NetworkModule {
|
|||
@NetworkMoshi moshi: Moshi,
|
||||
@ApplicationContext context: Context,
|
||||
appVersionProvider: AppVersionProvider,
|
||||
apiConfigsManager: ApiConfigsManager,
|
||||
): TangemTechApi {
|
||||
return provideTangemTechApiInternal(moshi, context, appVersionProvider, PROD_V1_TANGEM_TECH_BASE_URL)
|
||||
return provideTangemTechApiInternal(
|
||||
moshi = moshi,
|
||||
context = context,
|
||||
appVersionProvider = appVersionProvider,
|
||||
apiConfigsManager = apiConfigsManager,
|
||||
baseUrl = apiConfigsManager.getBaseUrl(id = ApiConfig.ID.TangemTech),
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
|
@ -97,8 +121,15 @@ class NetworkModule {
|
|||
@NetworkMoshi moshi: Moshi,
|
||||
@ApplicationContext context: Context,
|
||||
appVersionProvider: AppVersionProvider,
|
||||
apiConfigsManager: ApiConfigsManager,
|
||||
): TangemTechApiV2 {
|
||||
return provideTangemTechApiInternal(moshi, context, appVersionProvider, PROD_V2_TANGEM_TECH_BASE_URL)
|
||||
return provideTangemTechApiInternal(
|
||||
moshi = moshi,
|
||||
context = context,
|
||||
appVersionProvider = appVersionProvider,
|
||||
apiConfigsManager = apiConfigsManager,
|
||||
baseUrl = PROD_V2_TANGEM_TECH_BASE_URL,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
|
@ -108,8 +139,15 @@ class NetworkModule {
|
|||
@NetworkMoshi moshi: Moshi,
|
||||
@ApplicationContext context: Context,
|
||||
appVersionProvider: AppVersionProvider,
|
||||
apiConfigsManager: ApiConfigsManager,
|
||||
): TangemTechApi {
|
||||
return provideTangemTechApiInternal(moshi, context, appVersionProvider, DEV_V1_TANGEM_TECH_BASE_URL)
|
||||
return provideTangemTechApiInternal(
|
||||
moshi = moshi,
|
||||
context = context,
|
||||
appVersionProvider = appVersionProvider,
|
||||
apiConfigsManager = apiConfigsManager,
|
||||
baseUrl = DEV_V1_TANGEM_TECH_BASE_URL,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
|
@ -118,12 +156,14 @@ class NetworkModule {
|
|||
@NetworkMoshi moshi: Moshi,
|
||||
@ApplicationContext context: Context,
|
||||
appVersionProvider: AppVersionProvider,
|
||||
apiConfigsManager: ApiConfigsManager,
|
||||
): TangemTechServiceApi {
|
||||
return provideTangemTechApiInternal(
|
||||
moshi = moshi,
|
||||
context = context,
|
||||
appVersionProvider = appVersionProvider,
|
||||
baseUrl = PROD_V1_TANGEM_TECH_BASE_URL,
|
||||
apiConfigsManager = apiConfigsManager,
|
||||
baseUrl = apiConfigsManager.getBaseUrl(id = ApiConfig.ID.TangemTech),
|
||||
timeouts = Timeouts(
|
||||
callTimeoutSeconds = TANGEM_TECH_SERVICE_TIMEOUT_SECONDS,
|
||||
),
|
||||
|
|
@ -138,11 +178,13 @@ class NetworkModule {
|
|||
@NetworkMoshi moshi: Moshi,
|
||||
@ApplicationContext context: Context,
|
||||
appVersionProvider: AppVersionProvider,
|
||||
apiConfigsManager: ApiConfigsManager,
|
||||
): TangemTechMarketsApi {
|
||||
return provideTangemTechApiInternal(
|
||||
moshi = moshi,
|
||||
context = context,
|
||||
appVersionProvider = appVersionProvider,
|
||||
apiConfigsManager = apiConfigsManager,
|
||||
baseUrl = DEV_V1_TANGEM_TECH_BASE_URL,
|
||||
timeouts = Timeouts(
|
||||
callTimeoutSeconds = TANGEM_TECH_MARKETS_SERVICE_TIMEOUT_SECONDS,
|
||||
|
|
@ -157,11 +199,13 @@ class NetworkModule {
|
|||
moshi: Moshi,
|
||||
context: Context,
|
||||
appVersionProvider: AppVersionProvider,
|
||||
apiConfigsManager: ApiConfigsManager,
|
||||
baseUrl: String,
|
||||
timeouts: Timeouts = Timeouts(),
|
||||
requestHeaders: List<RequestHeader> = listOf(CacheControlHeader, AppVersionPlatformHeaders(appVersionProvider)),
|
||||
): T {
|
||||
val client = OkHttpClient.Builder()
|
||||
.addEnvironmentSwitcher(id = ApiConfig.ID.TangemTech, apiConfigsManager = apiConfigsManager)
|
||||
.let { builder ->
|
||||
var b = builder
|
||||
if (timeouts.callTimeoutSeconds != null) {
|
||||
|
|
@ -204,13 +248,9 @@ class NetworkModule {
|
|||
|
||||
private companion object {
|
||||
const val STAKEKIT_BASE_URL = "https://api.stakek.it/v1/"
|
||||
const val PROD_EXPRESS_BASE_URL = "https://express.tangem.com/v1/"
|
||||
const val STAGE_EXPRESS_BASE_URL = "[REDACTED_ENV_URL]"
|
||||
const val DEV_EXPRESS_BASE_URL = "[REDACTED_ENV_URL]"
|
||||
|
||||
const val DEV_V1_TANGEM_TECH_BASE_URL = "https://devapi.tangem-tech.com/v1/"
|
||||
|
||||
const val PROD_V1_TANGEM_TECH_BASE_URL = "https://api.tangem-tech.com/v1/"
|
||||
const val PROD_V2_TANGEM_TECH_BASE_URL = "https://api.tangem-tech.com/v2/"
|
||||
|
||||
const val TANGEM_TECH_SERVICE_TIMEOUT_SECONDS = 5L
|
||||
|
|
|
|||
|
|
@ -95,6 +95,8 @@ object PreferencesKeys {
|
|||
booleanPreferencesKey(name = "isTokenSwapPromoOkxShown")
|
||||
}
|
||||
|
||||
val apiConfigsEnvironmentKey by lazy { stringPreferencesKey(name = "apiConfigsEnvironment") }
|
||||
|
||||
// region Permission
|
||||
fun getShouldShowPermission(permission: String) = booleanPreferencesKey("shouldShowPushPermission_$permission")
|
||||
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ suspend inline fun <reified V> AppPreferencesStore.storeObjectMap(
|
|||
}
|
||||
|
||||
/** Get map with [String] key and value [V] by string [key], or empty if data is not found */
|
||||
suspend inline fun <reified V> AppPreferencesStore.getObjectMap(key: Preferences.Key<String>): Map<String, V> {
|
||||
suspend inline fun <reified V> AppPreferencesStore.getObjectMapSync(key: Preferences.Key<String>): Map<String, V> {
|
||||
val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java)
|
||||
val adapter = moshi.adapter<Map<String, V>>(type)
|
||||
|
||||
|
|
@ -135,6 +135,14 @@ suspend inline fun <reified V> AppPreferencesStore.getObjectMap(key: Preferences
|
|||
.orEmpty()
|
||||
}
|
||||
|
||||
/** Get flow of map with [String] key and value [V] by string [key], or empty if data is not found */
|
||||
inline fun <reified V> AppPreferencesStore.getObjectMap(key: Preferences.Key<String>): Flow<Map<String, V>> {
|
||||
val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java)
|
||||
val adapter = moshi.adapter<Map<String, V>>(type)
|
||||
|
||||
return data.map { it[key]?.let(adapter::fromJson) ?: emptyMap() }
|
||||
}
|
||||
|
||||
/** Get set of data [T] by string [key], or empty if data is not found */
|
||||
suspend inline fun <reified T> AppPreferencesStore.getObjectSetSync(key: Preferences.Key<String>): Set<T> {
|
||||
val adapter = moshi.adapter<Set<T>>(Types.newParameterizedType(Set::class.java, T::class.java))
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@ package com.tangem.datasource.utils
|
|||
import android.content.Context
|
||||
import com.chuckerteam.chucker.api.ChuckerInterceptor
|
||||
import com.tangem.datasource.BuildConfig
|
||||
import com.tangem.datasource.api.common.SwitchBaseUrlInterceptor
|
||||
import com.tangem.datasource.api.common.config.ApiConfig
|
||||
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
|
||||
import com.tangem.datasource.api.common.createNetworkLoggingInterceptor
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
|
|
@ -25,7 +28,7 @@ internal fun OkHttpClient.Builder.addHeaders(vararg requestHeaders: RequestHeade
|
|||
/**
|
||||
* Extension for logging each [OkHttpClient] request
|
||||
*
|
||||
* @param level logging level. By default, only the request body.
|
||||
* @param context context
|
||||
*/
|
||||
internal fun OkHttpClient.Builder.addLoggers(context: Context? = null): OkHttpClient.Builder {
|
||||
return if (BuildConfig.LOG_ENABLED) {
|
||||
|
|
@ -36,4 +39,26 @@ internal fun OkHttpClient.Builder.addLoggers(context: Context? = null): OkHttpCl
|
|||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add environment switcher
|
||||
*
|
||||
* @param id class of [ApiConfig]
|
||||
* @param apiConfigsManager api configs manager
|
||||
*/
|
||||
internal fun OkHttpClient.Builder.addEnvironmentSwitcher(
|
||||
id: ApiConfig.ID,
|
||||
apiConfigsManager: ApiConfigsManager,
|
||||
): OkHttpClient.Builder {
|
||||
return if (BuildConfig.TESTER_MENU_ENABLED) {
|
||||
addInterceptor(
|
||||
interceptor = SwitchBaseUrlInterceptor(
|
||||
id = id,
|
||||
apiConfigsManager = apiConfigsManager,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
package com.tangem.datasource.api.common.config.managers
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.datasource.BuildConfig
|
||||
import com.tangem.datasource.api.common.config.ApiConfig
|
||||
import com.tangem.datasource.api.common.config.ApiConfig.Companion.DEBUG_BUILD_TYPE
|
||||
import com.tangem.datasource.api.common.config.ApiConfig.Companion.EXTERNAL_BUILD_TYPE
|
||||
import com.tangem.datasource.api.common.config.ApiConfig.Companion.INTERNAL_BUILD_TYPE
|
||||
import com.tangem.datasource.api.common.config.ApiConfig.Companion.MOCKED_BUILD_TYPE
|
||||
import com.tangem.datasource.api.common.config.ApiConfig.Companion.RELEASE_BUILD_TYPE
|
||||
import com.tangem.datasource.api.common.config.Express
|
||||
import com.tangem.datasource.api.common.config.TangemTech
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.Parameterized
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@RunWith(Parameterized::class)
|
||||
internal class ProdApiConfigsManagerTest(private val model: Model) {
|
||||
|
||||
private val manager = ProdApiConfigsManager()
|
||||
|
||||
@Test
|
||||
fun test_getBaseUrl() {
|
||||
val actual = manager.getBaseUrl(id = model.id)
|
||||
|
||||
Truth.assertThat(actual).isEqualTo(model.expected)
|
||||
}
|
||||
|
||||
data class Model(val id: ApiConfig.ID, val expected: String)
|
||||
|
||||
private companion object {
|
||||
|
||||
@JvmStatic
|
||||
@Parameterized.Parameters
|
||||
fun data(): Collection<Model> = ApiConfig.values().map {
|
||||
when (it) {
|
||||
is Express -> createExpressModel()
|
||||
is TangemTech -> createTangemTechModel()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createExpressModel(): Model {
|
||||
return Model(
|
||||
id = ApiConfig.ID.Express,
|
||||
expected = when (BuildConfig.BUILD_TYPE) {
|
||||
DEBUG_BUILD_TYPE -> "[REDACTED_ENV_URL]"
|
||||
INTERNAL_BUILD_TYPE,
|
||||
MOCKED_BUILD_TYPE,
|
||||
-> "[REDACTED_ENV_URL]"
|
||||
EXTERNAL_BUILD_TYPE,
|
||||
RELEASE_BUILD_TYPE,
|
||||
-> "https://express.tangem.com/v1/"
|
||||
else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]")
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun createTangemTechModel(): Model {
|
||||
return Model(
|
||||
id = ApiConfig.ID.TangemTech,
|
||||
expected = when (BuildConfig.BUILD_TYPE) {
|
||||
DEBUG_BUILD_TYPE,
|
||||
INTERNAL_BUILD_TYPE,
|
||||
-> "https://devapi.tangem-tech.com/v1/"
|
||||
MOCKED_BUILD_TYPE,
|
||||
EXTERNAL_BUILD_TYPE,
|
||||
RELEASE_BUILD_TYPE,
|
||||
-> "https://api.tangem-tech.com/v1/"
|
||||
else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]")
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,10 +3,6 @@
|
|||
"name": "NEW_CARD_SCANNING_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "REDESIGNED_SEND_SCREEN_ENABLED",
|
||||
"version": "5.10.0"
|
||||
},
|
||||
{
|
||||
"name": "LOCAL_USER_LOGS_ENABLED",
|
||||
"version": "5.13.0"
|
||||
|
|
@ -38,5 +34,13 @@
|
|||
{
|
||||
"name": "MARKETS_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "HOME_SCREEN_CALLBACKS_REFACTORING_ENABLED",
|
||||
"version": "5.14.0"
|
||||
},
|
||||
{
|
||||
"name": "NEW_MANAGE_TOKENS",
|
||||
"version": "undefined"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -114,6 +114,12 @@ private class DefaultBatchListSource<TKey, TData, TRequestParams : Any, TUpdate>
|
|||
loadMoreActionJob?.cancel()
|
||||
reloadActionJob?.cancel()
|
||||
stopAllUpdates()
|
||||
|
||||
state.value = BatchListState(
|
||||
data = emptyList(),
|
||||
status = PaginationStatus.InitialLoading,
|
||||
)
|
||||
|
||||
reloadActionJob = scope.launchFetch {
|
||||
reloadTask(action)
|
||||
}
|
||||
|
|
@ -221,11 +227,6 @@ private class DefaultBatchListSource<TKey, TData, TRequestParams : Any, TUpdate>
|
|||
}
|
||||
|
||||
private suspend fun reloadTask(action: BatchAction.Reload<TRequestParams>) {
|
||||
state.value = BatchListState(
|
||||
data = emptyList(),
|
||||
status = PaginationStatus.InitialLoading,
|
||||
)
|
||||
|
||||
val res = runCatching {
|
||||
batchFetcher.fetchFirst(action.requestParams)
|
||||
}.getOrElse {
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@
|
|||
<string name="common_biometrics">生物</string>
|
||||
<string name="common_buy">購買</string>
|
||||
<string name="common_camera_denied_alert_message">您尚未授予相機訪問權限,請更改您的隱私設置</string>
|
||||
<string name="common_cancel">刪除</string>
|
||||
<string name="common_cancel">删除</string>
|
||||
<string name="common_close">關閉</string>
|
||||
<string name="common_continue">繼續</string>
|
||||
<string name="common_copy">複製</string>
|
||||
|
|
|
|||
|
|
@ -2,14 +2,14 @@ package com.tangem.core.ui
|
|||
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Stable
|
||||
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.theme.AppThemeModeHolder
|
||||
|
||||
@Stable
|
||||
interface UiDependencies {
|
||||
|
||||
val hapticManager: HapticManager
|
||||
val vibratorHapticManager: VibratorHapticManager
|
||||
|
||||
val appThemeModeHolder: AppThemeModeHolder
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import androidx.compose.material.ButtonColors
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.buttons.common.*
|
||||
|
|
@ -26,6 +27,7 @@ fun TextButton(
|
|||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
colors: ButtonColors = TangemButtonsDefaults.defaultTextButtonColors,
|
||||
textStyle: TextStyle = TangemTheme.typography.button,
|
||||
enabled: Boolean = true,
|
||||
) {
|
||||
TangemButton(
|
||||
|
|
@ -37,6 +39,7 @@ fun TextButton(
|
|||
showProgress = false,
|
||||
colors = colors,
|
||||
size = TangemButtonSize.Text,
|
||||
textStyle = textStyle,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.tangem.core.ui.components
|
|||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.ime
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
|
|
@ -14,11 +15,15 @@ sealed interface Keyboard {
|
|||
|
||||
data class Opened(override val height: Dp) : Keyboard
|
||||
|
||||
object Closed : Keyboard {
|
||||
data object Closed : Keyboard {
|
||||
override val height: Dp = 0.dp
|
||||
}
|
||||
}
|
||||
|
||||
val Keyboard.isOpened: Boolean
|
||||
@Stable
|
||||
get() = this is Keyboard.Opened
|
||||
|
||||
/**
|
||||
* Allows to subscribe to a soft keyboard to detect when it's open/closed
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -9,14 +9,23 @@ import androidx.compose.foundation.background
|
|||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.BlendMode
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.buttons.PrimarySmallButton
|
||||
import com.tangem.core.ui.components.buttons.SmallButtonConfig
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.res.*
|
||||
import com.valentinilk.shimmer.*
|
||||
|
||||
|
|
@ -45,6 +54,70 @@ fun CircleShimmer(modifier: Modifier = Modifier) {
|
|||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Shimmer for text
|
||||
* Height will be set automatically
|
||||
*
|
||||
* @param textSizeHeight if true, height will be set to font size height.
|
||||
*/
|
||||
@Composable
|
||||
fun TextShimmer(
|
||||
style: TextStyle,
|
||||
modifier: Modifier = Modifier,
|
||||
text: String = "A",
|
||||
radius: Dp = TangemTheme.dimens.radius3,
|
||||
textSizeHeight: Boolean = false,
|
||||
) {
|
||||
if (textSizeHeight) {
|
||||
val lineHeight = with(LocalDensity.current) { style.lineHeight.toDp() }
|
||||
|
||||
Box(
|
||||
modifier = Modifier.requiredHeight(lineHeight),
|
||||
contentAlignment = Alignment.CenterStart,
|
||||
) {
|
||||
Text(
|
||||
modifier = modifier
|
||||
.clip(RoundedCornerShape(size = radius))
|
||||
.shimmer(LocalTangemShimmer.current),
|
||||
text = text,
|
||||
style = style.copy(lineHeight = style.fontSize),
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Text(
|
||||
modifier = modifier
|
||||
.clip(RoundedCornerShape(size = radius))
|
||||
.shimmer(LocalTangemShimmer.current),
|
||||
text = text,
|
||||
style = style,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shimmer for SmallButton
|
||||
* Height and min width will be set automatically
|
||||
*/
|
||||
@Composable
|
||||
fun SmallButtonShimmer(modifier: Modifier = Modifier, withIcon: Boolean = false) {
|
||||
PrimarySmallButton(
|
||||
config = SmallButtonConfig(
|
||||
text = stringReference("B"),
|
||||
onClick = {},
|
||||
icon = if (withIcon) {
|
||||
TangemButtonIconPosition.Start(iconResId = R.drawable.ic_plus_24)
|
||||
} else {
|
||||
TangemButtonIconPosition.None
|
||||
},
|
||||
),
|
||||
modifier = modifier
|
||||
.clip(RoundedCornerShape(size = TangemTheme.dimens.radius16))
|
||||
.shimmer(LocalTangemShimmer.current),
|
||||
)
|
||||
}
|
||||
|
||||
internal val TangemShimmer: Shimmer
|
||||
@Composable
|
||||
get() = rememberShimmer(
|
||||
|
|
@ -106,6 +179,17 @@ private fun ShimmersPreview() {
|
|||
.height(TangemTheme.dimens.size24),
|
||||
)
|
||||
CircleShimmer(modifier = Modifier.size(size = TangemTheme.dimens.size42))
|
||||
TextShimmer(
|
||||
style = TangemTheme.typography.body1,
|
||||
modifier = Modifier.fillMaxWidth(fraction = 0.4f),
|
||||
)
|
||||
TextShimmer(
|
||||
style = TangemTheme.typography.body1,
|
||||
textSizeHeight = true,
|
||||
modifier = Modifier.fillMaxWidth(fraction = 0.4f),
|
||||
)
|
||||
SmallButtonShimmer(withIcon = true)
|
||||
SmallButtonShimmer(withIcon = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -97,7 +97,6 @@ private fun Preview_Grid() {
|
|||
},
|
||||
content = {
|
||||
GridItems(
|
||||
itemPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing4),
|
||||
items = persistentListOf(
|
||||
stringReference("Fist item"),
|
||||
stringReference("Second item"),
|
||||
|
|
@ -105,7 +104,6 @@ private fun Preview_Grid() {
|
|||
itemContent = {
|
||||
PreviewItem(text = it)
|
||||
},
|
||||
horizontalArragement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -44,7 +44,6 @@ inline fun <T : Any> InformationBlockContentScope.GridItems(
|
|||
items: ImmutableList<T>,
|
||||
itemContent: @Composable BoxScope.(T) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
itemPadding: PaddingValues = PaddingValues(all = TangemTheme.dimens.spacing0),
|
||||
verticalAlignment: Alignment.Vertical = Alignment.Top,
|
||||
horizontalArragement: Arrangement.Horizontal = Arrangement.Start,
|
||||
) {
|
||||
|
|
@ -59,7 +58,6 @@ inline fun <T : Any> InformationBlockContentScope.GridItems(
|
|||
|
||||
Column(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
) {
|
||||
rowItems.fastForEach { row ->
|
||||
|
|
@ -70,10 +68,7 @@ inline fun <T : Any> InformationBlockContentScope.GridItems(
|
|||
) {
|
||||
row.fastForEach { item ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(itemPadding)
|
||||
.weight(1f),
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
itemContent(item)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import androidx.compose.runtime.*
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.res.LocalBottomSheetAlwaysVisible
|
||||
|
|
@ -29,6 +30,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> TangemBottomSheet(
|
|||
titleAction: TopAppBarButtonUM? = null,
|
||||
containerColor: Color = TangemTheme.colors.background.primary,
|
||||
addBottomInsets: Boolean = true,
|
||||
skipPartiallyExpanded: Boolean = true,
|
||||
crossinline content: @Composable ColumnScope.(T) -> Unit,
|
||||
) {
|
||||
TangemBottomSheet(
|
||||
|
|
@ -36,6 +38,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> TangemBottomSheet(
|
|||
containerColor = containerColor,
|
||||
addBottomInsets = addBottomInsets,
|
||||
title = { TangemBottomSheetTitle(title = titleText, endButton = titleAction) },
|
||||
skipPartiallyExpanded = skipPartiallyExpanded,
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
|
@ -48,6 +51,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> TangemBottomSheet(
|
|||
config: TangemBottomSheetConfig,
|
||||
containerColor: Color = TangemTheme.colors.background.primary,
|
||||
addBottomInsets: Boolean = true,
|
||||
skipPartiallyExpanded: Boolean = true,
|
||||
crossinline title: @Composable BoxScope.(T) -> Unit = {},
|
||||
crossinline content: @Composable ColumnScope.(T) -> Unit,
|
||||
) {
|
||||
|
|
@ -60,6 +64,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> TangemBottomSheet(
|
|||
addBottomInsets = addBottomInsets,
|
||||
title = title,
|
||||
content = content,
|
||||
skipPartiallyExpanded = skipPartiallyExpanded,
|
||||
)
|
||||
} else {
|
||||
DefaultBottomSheet<T>(
|
||||
|
|
@ -68,6 +73,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> TangemBottomSheet(
|
|||
addBottomInsets = addBottomInsets,
|
||||
title = title,
|
||||
content = content,
|
||||
skipPartiallyExpanded = skipPartiallyExpanded,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -78,11 +84,12 @@ inline fun <reified T : TangemBottomSheetConfigContent> DefaultBottomSheet(
|
|||
config: TangemBottomSheetConfig,
|
||||
containerColor: Color,
|
||||
addBottomInsets: Boolean,
|
||||
skipPartiallyExpanded: Boolean = true,
|
||||
crossinline title: @Composable (BoxScope.(T) -> Unit),
|
||||
crossinline content: @Composable (ColumnScope.(T) -> Unit),
|
||||
) {
|
||||
var isVisible by remember { mutableStateOf(value = config.isShow) }
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = skipPartiallyExpanded)
|
||||
|
||||
if (isVisible && config.content is T) {
|
||||
BasicBottomSheet<T>(
|
||||
|
|
@ -110,13 +117,15 @@ inline fun <reified T : TangemBottomSheetConfigContent> PreviewBottomSheet(
|
|||
config: TangemBottomSheetConfig,
|
||||
containerColor: Color,
|
||||
addBottomInsets: Boolean,
|
||||
skipPartiallyExpanded: Boolean = true,
|
||||
crossinline title: @Composable (BoxScope.(T) -> Unit),
|
||||
crossinline content: @Composable (ColumnScope.(T) -> Unit),
|
||||
) {
|
||||
BasicBottomSheet<T>(
|
||||
modifier = Modifier.width(360.dp),
|
||||
config = config,
|
||||
sheetState = SheetState(
|
||||
skipPartiallyExpanded = true,
|
||||
skipPartiallyExpanded = skipPartiallyExpanded,
|
||||
initialValue = Expanded,
|
||||
density = LocalDensity.current,
|
||||
),
|
||||
|
|
@ -137,6 +146,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicBottomSheet(
|
|||
addBottomInsets: Boolean,
|
||||
crossinline title: @Composable (BoxScope.(T) -> Unit),
|
||||
crossinline content: @Composable (ColumnScope.(T) -> Unit),
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val model = config.content as? T ?: return
|
||||
|
||||
|
|
@ -145,7 +155,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicBottomSheet(
|
|||
|
||||
ModalBottomSheet(
|
||||
// FIXME temporary solution to fix height of the bottom sheet
|
||||
modifier = Modifier.sizeIn(maxHeight = LocalWindowSize.current.height - statusBarHeight),
|
||||
modifier = modifier.heightIn(max = LocalWindowSize.current.height - statusBarHeight),
|
||||
onDismissRequest = config.onDismissRequest,
|
||||
sheetState = sheetState,
|
||||
containerColor = containerColor,
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ sealed interface TangemButtonIconPosition {
|
|||
|
||||
data class End(@DrawableRes override val iconResId: Int) : TangemButtonIconPosition
|
||||
|
||||
object None : TangemButtonIconPosition {
|
||||
data object None : TangemButtonIconPosition {
|
||||
@DrawableRes
|
||||
override val iconResId: Int? = null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
package com.tangem.core.ui.components.fields
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
|
||||
import androidx.compose.foundation.text.selection.TextSelectionColors
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
@ -40,6 +39,7 @@ fun SimpleTextField(
|
|||
textStyle: TextStyle = TangemTheme.typography.body2.copy(color = color),
|
||||
placeholderColor: Color = TangemTheme.colors.text.disabled,
|
||||
readOnly: Boolean = false,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
isValuePasted: Boolean = false,
|
||||
onValuePastedTriggerDismiss: () -> Unit = {},
|
||||
decorationBox: (@Composable (innerTextField: @Composable () -> Unit) -> Unit)? = null,
|
||||
|
|
@ -54,10 +54,6 @@ fun SimpleTextField(
|
|||
)
|
||||
}
|
||||
val focusRequester = remember { FocusRequester.Default }
|
||||
val customTextSelectionColors = TextSelectionColors(
|
||||
handleColor = TangemTheme.colors.text.accent,
|
||||
backgroundColor = TangemTheme.colors.text.accent.copy(alpha = 0.3f),
|
||||
)
|
||||
val textFieldValue = textFieldValueState.copy(text = value)
|
||||
var lastTextValue by remember(proxyValue, isValuePasted) {
|
||||
textFieldValueState = textFieldValueState.copy(
|
||||
|
|
@ -85,37 +81,36 @@ fun SimpleTextField(
|
|||
}
|
||||
}
|
||||
|
||||
CompositionLocalProvider(LocalTextSelectionColors provides customTextSelectionColors) {
|
||||
BasicTextField(
|
||||
value = textFieldValue,
|
||||
onValueChange = { newTextFieldValueState ->
|
||||
textFieldValueState = newTextFieldValueState
|
||||
BasicTextField(
|
||||
value = textFieldValue,
|
||||
onValueChange = { newTextFieldValueState ->
|
||||
textFieldValueState = newTextFieldValueState
|
||||
|
||||
val stringChangedSinceLastInvocation = lastTextValue != newTextFieldValueState.text
|
||||
lastTextValue = newTextFieldValueState.text
|
||||
val stringChangedSinceLastInvocation = lastTextValue != newTextFieldValueState.text
|
||||
lastTextValue = newTextFieldValueState.text
|
||||
|
||||
if (stringChangedSinceLastInvocation) onValueChange(newTextFieldValueState.text)
|
||||
},
|
||||
textStyle = textStyle.copy(color = color),
|
||||
cursorBrush = SolidColor(TangemTheme.colors.text.primary1),
|
||||
singleLine = singleLine,
|
||||
readOnly = readOnly,
|
||||
visualTransformation = visualTransformation,
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = keyboardActions,
|
||||
decorationBox = decorationBox ?: { textValue ->
|
||||
SimpleTextPlaceholder(
|
||||
placeholder = placeholder,
|
||||
value = value,
|
||||
textStyle = textStyle,
|
||||
textValue = textValue,
|
||||
color = placeholderColor,
|
||||
)
|
||||
},
|
||||
modifier = modifier
|
||||
.focusRequester(focusRequester),
|
||||
)
|
||||
}
|
||||
if (stringChangedSinceLastInvocation) onValueChange(newTextFieldValueState.text)
|
||||
},
|
||||
textStyle = textStyle.copy(color = color),
|
||||
cursorBrush = SolidColor(TangemTheme.colors.text.primary1),
|
||||
singleLine = singleLine,
|
||||
readOnly = readOnly,
|
||||
visualTransformation = visualTransformation,
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = keyboardActions,
|
||||
interactionSource = interactionSource,
|
||||
decorationBox = decorationBox ?: { textValue ->
|
||||
SimpleTextPlaceholder(
|
||||
placeholder = placeholder,
|
||||
value = value,
|
||||
textStyle = textStyle,
|
||||
textValue = textValue,
|
||||
color = placeholderColor,
|
||||
)
|
||||
},
|
||||
modifier = modifier
|
||||
.focusRequester(focusRequester),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ fun InputRowEnter(
|
|||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = title.resolveReference(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = titleColor,
|
||||
)
|
||||
SimpleTextField(
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ fun InputRowEnterAmount(
|
|||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = title.resolveReference(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = titleColor,
|
||||
)
|
||||
AmountTextField(
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ fun InputRowEnterInfo(
|
|||
) {
|
||||
Text(
|
||||
text = title.resolveReference(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = titleColor,
|
||||
)
|
||||
Row {
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ fun InputRowEnterInfoAmount(
|
|||
) {
|
||||
Text(
|
||||
text = title.resolveReference(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = titleColor,
|
||||
)
|
||||
Row {
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ fun InputRowImage(
|
|||
) {
|
||||
Text(
|
||||
text = title.resolveReference(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = titleColor,
|
||||
)
|
||||
Row(
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import com.tangem.core.ui.res.TangemTheme
|
|||
@Composable
|
||||
internal fun InputRowImageBase(
|
||||
subtitle: TextReference,
|
||||
caption: TextReference,
|
||||
caption: TextReference?,
|
||||
imageUrl: String,
|
||||
modifier: Modifier = Modifier,
|
||||
subtitleColor: Color = TangemTheme.colors.text.primary1,
|
||||
|
|
@ -40,12 +40,14 @@ internal fun InputRowImageBase(
|
|||
style = TangemTheme.typography.subtitle2,
|
||||
color = subtitleColor,
|
||||
)
|
||||
Text(
|
||||
text = caption.resolveAnnotatedReference(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = captionColor,
|
||||
modifier = Modifier.padding(top = TangemTheme.dimens.spacing2),
|
||||
)
|
||||
if (caption != null) {
|
||||
Text(
|
||||
text = caption.resolveAnnotatedReference(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = captionColor,
|
||||
modifier = Modifier.padding(top = TangemTheme.dimens.spacing2),
|
||||
)
|
||||
}
|
||||
}
|
||||
extraContent()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ fun InputRowRecipient(
|
|||
AnimatedContent(targetState = titleText, label = "Title Change") {
|
||||
Text(
|
||||
text = it.resolveReference(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = color,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ fun InputRowRecipientDefault(
|
|||
) {
|
||||
Text(
|
||||
text = title.resolveReference(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = titleColor,
|
||||
)
|
||||
Row(
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import android.content.res.Configuration
|
|||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
@ -15,24 +16,57 @@ import com.tangem.core.ui.extensions.TextReference
|
|||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
private const val ROUNDED_LIST_WITH_DIVIDERS_HEADER_KEY = "ROUNDED_LIST_WITH_DIVIDERS_HEADER_KEY"
|
||||
private const val ROUNDED_LIST_WITH_DIVIDERS_FOOTER_KEY = "ROUNDED_LIST_WITH_DIVIDERS_FOOTER_KEY"
|
||||
|
||||
@Composable
|
||||
fun RoundedListWithDividers(rows: List<RoundedListWithDividersItemData>, modifier: Modifier = Modifier) {
|
||||
fun RoundedListWithDividers(
|
||||
rows: ImmutableList<RoundedListWithDividersItemData>,
|
||||
modifier: Modifier = Modifier,
|
||||
headerContent: (@Composable () -> Unit)? = null,
|
||||
footerContent: (@Composable () -> Unit)? = null,
|
||||
) {
|
||||
LazyColumn(modifier = modifier) {
|
||||
itemsIndexed(
|
||||
items = rows,
|
||||
key = { _, item -> item.id },
|
||||
) { index, row ->
|
||||
InitialInfoContentRow(
|
||||
startText = row.startText.resolveReference(),
|
||||
endText = row.endText.resolveReference(),
|
||||
cornersToRound = getCornersToRound(index, rows.size),
|
||||
iconClick = row.iconClick,
|
||||
)
|
||||
if (index < rows.lastIndex) {
|
||||
RoundedListDivider()
|
||||
}
|
||||
this.roundedListWithDividersItems(
|
||||
rows = rows,
|
||||
headerContent = headerContent,
|
||||
footerContent = footerContent,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun LazyListScope.roundedListWithDividersItems(
|
||||
rows: ImmutableList<RoundedListWithDividersItemData>,
|
||||
headerContent: (@Composable () -> Unit)? = null,
|
||||
footerContent: (@Composable () -> Unit)? = null,
|
||||
) {
|
||||
if (headerContent != null) {
|
||||
item(key = ROUNDED_LIST_WITH_DIVIDERS_HEADER_KEY) {
|
||||
headerContent()
|
||||
}
|
||||
}
|
||||
|
||||
itemsIndexed(
|
||||
items = rows,
|
||||
key = { _, item -> item.id },
|
||||
) { index, row ->
|
||||
InitialInfoContentRow(
|
||||
startText = row.startText.resolveReference(),
|
||||
endText = row.endText.resolveReference(),
|
||||
cornersToRound = getCornersToRound(index, rows.size),
|
||||
iconClick = row.iconClick,
|
||||
)
|
||||
if (index < rows.lastIndex) {
|
||||
RoundedListDivider()
|
||||
}
|
||||
}
|
||||
|
||||
if (footerContent != null) {
|
||||
item(key = ROUNDED_LIST_WITH_DIVIDERS_FOOTER_KEY) {
|
||||
footerContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ 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.text.TextStyle
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
|
@ -28,7 +29,7 @@ fun TooltipText(
|
|||
text: TextReference,
|
||||
onInfoClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
useSmallerText: Boolean = false,
|
||||
textStyle: TextStyle = TangemTheme.typography.caption2,
|
||||
) {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
|
||||
|
|
@ -45,18 +46,16 @@ fun TooltipText(
|
|||
Text(
|
||||
modifier = Modifier.weight(1f, fill = false),
|
||||
text = text.resolveReference(),
|
||||
style = if (useSmallerText) {
|
||||
TangemTheme.typography.caption2
|
||||
} else {
|
||||
TangemTheme.typography.subtitle2
|
||||
},
|
||||
style = textStyle,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
|
||||
IconButton(
|
||||
modifier = Modifier.requiredSize(TangemTheme.dimens.size24),
|
||||
modifier = Modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing4)
|
||||
.requiredSize(TangemTheme.dimens.size16),
|
||||
interactionSource = interactionSource,
|
||||
onClick = onInfoClick,
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package com.tangem.core.ui.extensions
|
|||
import androidx.annotation.DrawableRes
|
||||
import com.tangem.core.ui.R
|
||||
|
||||
@Suppress("ComplexMethod")
|
||||
@Suppress("ComplexMethod", "LongMethod")
|
||||
@DrawableRes
|
||||
fun getActiveIconRes(blockchainId: String): Int {
|
||||
return when (blockchainId) {
|
||||
|
|
@ -70,11 +70,13 @@ fun getActiveIconRes(blockchainId: String): Int {
|
|||
"joystream" -> R.drawable.img_joystream_22
|
||||
"koinos", "koinos/test" -> R.drawable.img_koinos_22
|
||||
"bittensor" -> R.drawable.img_bittensor_22
|
||||
"blast", "blast/test" -> R.drawable.img_blast_22
|
||||
"filecoin" -> R.drawable.img_filecoin_22
|
||||
else -> R.drawable.ic_alert_24
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("ComplexMethod")
|
||||
@Suppress("ComplexMethod", "LongMethod")
|
||||
@DrawableRes
|
||||
fun getActiveIconResByNetworkId(networkId: String): Int {
|
||||
return when (networkId) {
|
||||
|
|
@ -141,6 +143,8 @@ fun getActiveIconResByNetworkId(networkId: String): Int {
|
|||
"joystream" -> R.drawable.img_joystream_22
|
||||
"koinos", "koinos/test" -> R.drawable.img_koinos_22
|
||||
"bittensor" -> R.drawable.img_bittensor_22
|
||||
"blast", "blast/test" -> R.drawable.img_blast_22
|
||||
"filecoin" -> R.drawable.img_filecoin_22
|
||||
else -> R.drawable.ic_alert_24
|
||||
}
|
||||
}
|
||||
|
|
@ -209,11 +213,13 @@ fun getActiveIconResByCoinId(coinId: String): Int {
|
|||
"joystream" -> R.drawable.img_joystream_22
|
||||
"koinos", "koinos/test" -> R.drawable.img_koinos_22
|
||||
"bittensor" -> R.drawable.img_bittensor_22
|
||||
"blast", "blast/test" -> R.drawable.img_blast_22
|
||||
"filecoin" -> R.drawable.img_filecoin_22
|
||||
else -> R.drawable.ic_alert_24
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("ComplexMethod")
|
||||
@Suppress("ComplexMethod", "LongMethod")
|
||||
@DrawableRes
|
||||
fun getGreyedOutIconRes(blockchainId: String): Int {
|
||||
return when (blockchainId) {
|
||||
|
|
@ -280,11 +286,13 @@ fun getGreyedOutIconRes(blockchainId: String): Int {
|
|||
"joystream" -> R.drawable.ic_joystream_22
|
||||
"koinos", "koinos/test" -> R.drawable.ic_koinos_22
|
||||
"bittensor" -> R.drawable.ic_bittensor_22
|
||||
"blast", "blast/test" -> R.drawable.ic_blast_22
|
||||
"filecoin" -> R.drawable.ic_filecoin_22
|
||||
else -> R.drawable.ic_alert_24
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("ComplexMethod")
|
||||
@Suppress("ComplexMethod", "LongMethod")
|
||||
@DrawableRes
|
||||
fun getGreyedOutIconResByNetworkId(networkId: String): Int {
|
||||
return when (networkId) {
|
||||
|
|
@ -351,6 +359,8 @@ fun getGreyedOutIconResByNetworkId(networkId: String): Int {
|
|||
"joystream" -> R.drawable.ic_joystream_22
|
||||
"koinos", "koinos/test" -> R.drawable.ic_koinos_22
|
||||
"bittensor" -> R.drawable.ic_bittensor_22
|
||||
"blast", "blast/test" -> R.drawable.ic_blast_22
|
||||
"filecoin" -> R.drawable.ic_filecoin_22
|
||||
else -> R.drawable.ic_alert_24
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package com.tangem.core.ui.haptic
|
||||
|
||||
import android.view.View
|
||||
import androidx.core.view.ViewCompat
|
||||
|
||||
internal class DefaultHapticManager(
|
||||
private val view: View,
|
||||
private val vibratorHapticManager: VibratorHapticManager?,
|
||||
) : HapticManager {
|
||||
|
||||
override fun perform(effect: TangemHapticEffect) {
|
||||
when (effect) {
|
||||
is TangemHapticEffect.View -> {
|
||||
effect.androidHapticFeedbackCode?.let {
|
||||
ViewCompat.performHapticFeedback(view, it)
|
||||
}
|
||||
}
|
||||
is TangemHapticEffect.OneTime -> {
|
||||
if (vibratorHapticManager != null) {
|
||||
vibratorHapticManager.performOneTime(effect)
|
||||
} else {
|
||||
when (effect) {
|
||||
TangemHapticEffect.OneTime.Tick -> perform(TangemHapticEffect.View.SegmentTick)
|
||||
TangemHapticEffect.OneTime.Click -> perform(TangemHapticEffect.View.ContextClick)
|
||||
TangemHapticEffect.OneTime.DoubleClick -> perform(TangemHapticEffect.View.ContextClick)
|
||||
TangemHapticEffect.OneTime.HeavyClick -> perform(TangemHapticEffect.View.LongPress)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,12 +2,13 @@ package com.tangem.core.ui.haptic
|
|||
|
||||
import androidx.compose.runtime.Stable
|
||||
|
||||
/**
|
||||
* Haptic feedback.
|
||||
* @see [TangemHapticEffect.OneTime] for one-time effects.
|
||||
* @see [TangemHapticEffect.View] for view effects.
|
||||
*/
|
||||
@Stable
|
||||
interface HapticManager {
|
||||
|
||||
fun vibrateShort()
|
||||
|
||||
fun vibrateMeduim()
|
||||
|
||||
fun vibrateLong()
|
||||
fun perform(effect: TangemHapticEffect)
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
package com.tangem.core.ui.haptic
|
||||
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedback
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
|
||||
@Suppress("FunctionName")
|
||||
fun MockHapticManager(mockHapticFeedback: HapticFeedback? = null): HapticManager =
|
||||
if (mockHapticFeedback == null) MockHapticManager else MockHapticManagerImpl(mockHapticFeedback)
|
||||
|
||||
val MockHapticManager: HapticManager = MockHapticManagerImpl()
|
||||
|
||||
private class MockHapticManagerImpl(
|
||||
private val mockHapticFeedback: HapticFeedback? = null,
|
||||
) : HapticManager {
|
||||
|
||||
override fun vibrateShort() {
|
||||
mockHapticFeedback?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
|
||||
/** Intentionally do nothing */
|
||||
}
|
||||
|
||||
override fun vibrateMeduim() {
|
||||
mockHapticFeedback?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
|
||||
/** Intentionally do nothing */
|
||||
}
|
||||
|
||||
override fun vibrateLong() {
|
||||
mockHapticFeedback?.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
/** Intentionally do nothing */
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
package com.tangem.core.ui.haptic
|
||||
|
||||
import android.os.Build
|
||||
import android.os.VibrationEffect
|
||||
import androidx.annotation.RequiresApi
|
||||
|
||||
sealed interface TangemHapticEffect {
|
||||
|
||||
/**
|
||||
* For cases when view could not be visible on screen (ex. Activity is not in foreground)
|
||||
* or there is no view context (ex. background service, Model, ViewModel)
|
||||
*/
|
||||
enum class OneTime : TangemHapticEffect {
|
||||
Click,
|
||||
DoubleClick,
|
||||
HeavyClick,
|
||||
Tick,
|
||||
;
|
||||
|
||||
val code: Int
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
get() = when (this) {
|
||||
Click -> VibrationEffect.EFFECT_CLICK
|
||||
DoubleClick -> VibrationEffect.EFFECT_DOUBLE_CLICK
|
||||
HeavyClick -> VibrationEffect.EFFECT_HEAVY_CLICK
|
||||
Tick -> VibrationEffect.EFFECT_TICK
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Preferred way to provide haptic feedback for UI components
|
||||
* @see [androidx.core.view.HapticFeedbackConstantsCompat]
|
||||
*/
|
||||
@Suppress("MagicNumber")
|
||||
enum class View(internal val androidHapticFeedbackCode: Int? = null) : TangemHapticEffect {
|
||||
/**
|
||||
* The user has performed a long press on an object that is resulting in an action being
|
||||
* performed
|
||||
*/
|
||||
LongPress(0),
|
||||
/**
|
||||
* The user has pressed on a virtual on-screen key
|
||||
*/
|
||||
VirtualKey(1),
|
||||
/**
|
||||
* The user has pressed either an hour or minute tick of a Clock
|
||||
*/
|
||||
ClockTick(4),
|
||||
/**
|
||||
* The user has performed a context click on an object
|
||||
*/
|
||||
ContextClick(6),
|
||||
/**
|
||||
* The user has pressed a virtual or software keyboard key
|
||||
*/
|
||||
KeyboardPress(3),
|
||||
/**
|
||||
* The user has released a virtual keyboard key
|
||||
*/
|
||||
KeyboardRelease(7),
|
||||
/**
|
||||
* The user has released a virtual key
|
||||
*/
|
||||
VirtualKeyRelease(8),
|
||||
/**
|
||||
* The user has performed a selection/insertion handle move on text field
|
||||
*/
|
||||
TextHandleMove(9),
|
||||
/**
|
||||
* The user has started a gesture (e.g. on the soft keyboard)
|
||||
*/
|
||||
GestureStart(12),
|
||||
/**
|
||||
* The user has finished a gesture (e.g. on the soft keyboard)
|
||||
*/
|
||||
GestureEnd(13),
|
||||
/**
|
||||
* A haptic effect to signal the confirmation or successful completion of a user interaction
|
||||
*/
|
||||
Confirm(16),
|
||||
/**
|
||||
* A haptic effect to signal the rejection or failure of a user interaction
|
||||
*/
|
||||
Reject(17),
|
||||
/**
|
||||
* The user has toggled a switch or button into the on position
|
||||
*/
|
||||
ToggleOn(21),
|
||||
/**
|
||||
* The user has toggled a switch or button into the off position
|
||||
*/
|
||||
ToggleOff(22),
|
||||
/**
|
||||
* The user is executing a swipe/drag-style gesture, such as pull-to-refresh, where the
|
||||
* gesture action is “eligible” at a certain threshold of movement, and can be cancelled by
|
||||
* moving back past the threshold. This constant indicates that the user's motion has just
|
||||
* passed the threshold for the action to be activated on release
|
||||
*/
|
||||
GestureThresholdActivate(23),
|
||||
/**
|
||||
* The user is executing a swipe/drag-style gesture, such as pull-to-refresh, where the
|
||||
* gesture action is “eligible” at a certain threshold of movement, and can be cancelled by
|
||||
* moving back past the threshold. This constant indicates that the user's motion has just
|
||||
* re-crossed back "under" the threshold for the action to be activated, meaning the gesture is
|
||||
* currently in a cancelled state
|
||||
*/
|
||||
GestureThresholdDeactivate(24),
|
||||
/**
|
||||
* The user has started a drag-and-drop gesture. The drag target has just been "picked up"
|
||||
*/
|
||||
DragStart(25),
|
||||
/**
|
||||
* The user is switching between a series of potential choices, for example items in a list
|
||||
* or discrete points on a slider
|
||||
*/
|
||||
SegmentTick(26),
|
||||
/**
|
||||
* The user is switching between a series of many potential choices, for example minutes on a
|
||||
* clock face, or individual percentages. This constant is expected to be very soft, so as
|
||||
* not to be uncomfortable when performed a lot in quick succession. If the device can’t make
|
||||
* a suitably soft vibration, then it may not make any vibration
|
||||
*/
|
||||
SegmentFrequentTick(27),
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.core.ui.haptic
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
|
||||
/**
|
||||
* Haptic feedback
|
||||
* For cases when view could not be visible on screen (ex. Activity is not in foreground)
|
||||
* or there is no view context (ex. background service, Model, ViewModel)
|
||||
* @see [HapticManager] for view effects.
|
||||
*/
|
||||
@Stable
|
||||
interface VibratorHapticManager {
|
||||
|
||||
fun performOneTime(effect: TangemHapticEffect.OneTime)
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package com.tangem.core.ui.res
|
||||
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.runtime.*
|
||||
|
||||
@Immutable
|
||||
object TangemAnimations {
|
||||
|
||||
val transitionSpecs = TransitionSpecs
|
||||
|
||||
@Composable
|
||||
@NonRestartableComposable
|
||||
fun horizontalIndicatorAsState(targetFraction: Float): State<Float> {
|
||||
return animateFloatAsState(
|
||||
targetValue = targetFraction,
|
||||
animationSpec = tween(durationMillis = 300),
|
||||
label = "Indicator fraction",
|
||||
)
|
||||
}
|
||||
|
||||
@Immutable
|
||||
object TransitionSpecs {
|
||||
// TODO add more transition specs
|
||||
}
|
||||
}
|
||||
|
|
@ -8,10 +8,12 @@ import androidx.compose.material.ProvideTextStyle
|
|||
import androidx.compose.material3.SnackbarHostState
|
||||
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.components.TangemShimmer
|
||||
import com.tangem.core.ui.haptic.DefaultHapticManager
|
||||
import com.tangem.core.ui.haptic.HapticManager
|
||||
import com.tangem.core.ui.haptic.MockHapticManager
|
||||
import com.tangem.core.ui.haptic.VibratorHapticManager
|
||||
import com.tangem.core.ui.windowsize.WindowSize
|
||||
import com.valentinilk.shimmer.Shimmer
|
||||
|
||||
|
|
@ -21,7 +23,7 @@ fun TangemTheme(
|
|||
windowSize: WindowSize,
|
||||
typography: TangemTypography = TangemTheme.typography,
|
||||
dimens: TangemDimens = TangemTheme.dimens,
|
||||
hapticManager: HapticManager = MockHapticManager,
|
||||
vibratorHapticManager: VibratorHapticManager? = null,
|
||||
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
|
|
@ -40,6 +42,12 @@ fun TangemTheme(
|
|||
)
|
||||
}
|
||||
|
||||
val view = LocalView.current
|
||||
|
||||
val hapticManager = remember(view) {
|
||||
DefaultHapticManager(view = view, vibratorHapticManager = vibratorHapticManager)
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colors = materialThemeColors(colors = themeColors, isDark = isDark),
|
||||
) {
|
||||
|
|
@ -52,10 +60,11 @@ fun TangemTheme(
|
|||
LocalHapticManager provides hapticManager,
|
||||
LocalSnackbarHostState provides snackbarHostState,
|
||||
LocalWindowSize provides windowSize,
|
||||
LocalTextSelectionColors provides TangemTextSelectionColors,
|
||||
) {
|
||||
CompositionLocalProvider(
|
||||
LocalTangemShimmer provides TangemShimmer,
|
||||
LocalMainBottomSheetColor provides remember { mutableStateOf(Color.Unspecified) },
|
||||
LocalTextSelectionColors provides TangemTextSelectionColors,
|
||||
) {
|
||||
ProvideTextStyle(
|
||||
value = TangemTheme.typography.body1,
|
||||
|
|
@ -208,11 +217,13 @@ private fun darkThemeColors(): TangemColors {
|
|||
)
|
||||
}
|
||||
|
||||
@Stable
|
||||
private val TangemTextSelectionColors = TextSelectionColors(
|
||||
handleColor = TangemColorPalette.Azure,
|
||||
backgroundColor = TangemColorPalette.Azure.copy(alpha = 0.4f),
|
||||
)
|
||||
private val TangemTextSelectionColors: TextSelectionColors
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
get() = TextSelectionColors(
|
||||
handleColor = TangemTheme.colors.text.accent,
|
||||
backgroundColor = TangemTheme.colors.text.accent.copy(alpha = 0.3f),
|
||||
)
|
||||
|
||||
private val LocalTangemColors = staticCompositionLocalOf<TangemColors> {
|
||||
error("No TangemColors provided")
|
||||
|
|
@ -246,4 +257,8 @@ val LocalWindowSize = staticCompositionLocalOf<WindowSize> {
|
|||
|
||||
val LocalTangemShimmer = staticCompositionLocalOf<Shimmer> {
|
||||
error("No TangemShimmer provided")
|
||||
}
|
||||
|
||||
val LocalMainBottomSheetColor = staticCompositionLocalOf<MutableState<Color>> {
|
||||
error("No MainBottomSheetColor provided")
|
||||
}
|
||||
|
|
@ -6,8 +6,6 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.ProvidableCompositionLocal
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import com.tangem.core.ui.haptic.MockHapticManager
|
||||
import com.tangem.core.ui.windowsize.rememberWindowSizePreview
|
||||
|
||||
@Composable
|
||||
|
|
@ -29,7 +27,6 @@ fun TangemThemePreview(
|
|||
typography = typography,
|
||||
dimens = dimens,
|
||||
windowSize = rememberWindowSizePreview(maxWidth, maxHeight),
|
||||
hapticManager = MockHapticManager(LocalHapticFeedback.current),
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ internal fun ComposeScreen.createComposeView(context: Context, activity: Activit
|
|||
TangemTheme(
|
||||
isDark = shouldUseDarkTheme(appThemeMode),
|
||||
windowSize = windowSize,
|
||||
hapticManager = uiDependencies.hapticManager,
|
||||
vibratorHapticManager = uiDependencies.vibratorHapticManager,
|
||||
snackbarHostState = uiDependencies.globalSnackbarHostState,
|
||||
) {
|
||||
ScreenContent(modifier = screenModifier)
|
||||
|
|
|
|||
|
|
@ -117,6 +117,7 @@ object BigDecimalFormatter {
|
|||
fiatAmount: BigDecimal?,
|
||||
fiatCurrencyCode: String,
|
||||
fiatCurrencySymbol: String,
|
||||
decimals: Int = FIAT_MARKET_DEFAULT_DIGITS,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
): String {
|
||||
if (fiatAmount == null) return EMPTY_BALANCE_SIGN
|
||||
|
|
@ -124,8 +125,8 @@ object BigDecimalFormatter {
|
|||
val formatterCurrency = getCurrency(fiatCurrencyCode)
|
||||
val formatter = NumberFormat.getCurrencyInstance(locale).apply {
|
||||
currency = formatterCurrency
|
||||
maximumFractionDigits = FIAT_MARKET_DEFAULT_DIGITS
|
||||
minimumFractionDigits = FIAT_MARKET_DEFAULT_DIGITS
|
||||
maximumFractionDigits = decimals
|
||||
minimumFractionDigits = decimals
|
||||
roundingMode = RoundingMode.HALF_UP
|
||||
}
|
||||
|
||||
|
|
@ -250,40 +251,27 @@ object BigDecimalFormatter {
|
|||
/**
|
||||
* "123456.6" -> "$123.457K"
|
||||
* "12345.6" -> "$123.046K"
|
||||
* Negative amount is not supported
|
||||
* @param threeDigitsMethod if true, will format the amount always with 3 significant digits
|
||||
* @param scale the number of digits to the right of the decimal point
|
||||
*/
|
||||
@Suppress("MagicNumber")
|
||||
fun formatCompactAmount(
|
||||
amount: BigDecimal,
|
||||
fun formatCompactFiatAmount(
|
||||
amount: BigDecimal?,
|
||||
fiatCurrencyCode: String,
|
||||
fiatCurrencySymbol: String,
|
||||
threeDigitsMethod: Boolean = false,
|
||||
scale: Int = 0,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
): String {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||
return BigDecimalFormatterCompat.formatCompactAmountNoLocaleContext(
|
||||
amount = amount,
|
||||
fiatCurrencyCode = fiatCurrencyCode,
|
||||
fiatCurrencySymbol = fiatCurrencySymbol,
|
||||
locale = locale,
|
||||
)
|
||||
}
|
||||
if (amount == null) return EMPTY_BALANCE_SIGN
|
||||
|
||||
val scaledAmount = amount.setScale(0, RoundingMode.HALF_UP)
|
||||
val digitsCount = scaledAmount.longValueExact().toString().count()
|
||||
val digitsToFormat = 6 - when (digitsCount % 3) {
|
||||
0 -> 0
|
||||
1 -> 2
|
||||
else -> 1
|
||||
}
|
||||
|
||||
val formatter = CompactDecimalFormat.getInstance(
|
||||
locale,
|
||||
CompactDecimalFormat.CompactStyle.SHORT,
|
||||
).apply {
|
||||
minimumSignificantDigits = 4
|
||||
maximumSignificantDigits = digitsToFormat
|
||||
}
|
||||
|
||||
val rawAmount = formatter.format(amount.setScale(0, RoundingMode.HALF_UP))
|
||||
val rawAmount = formatCompactAmount(
|
||||
amount = amount,
|
||||
locale = locale,
|
||||
threeDigitsMethod = threeDigitsMethod,
|
||||
scale = scale,
|
||||
)
|
||||
|
||||
return addCurrencySymbolToStringAmount(
|
||||
amount = rawAmount,
|
||||
|
|
@ -293,5 +281,53 @@ object BigDecimalFormatter {
|
|||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* "123456.6" -> "123.457K"
|
||||
* "12345.6" -> "123.046K"
|
||||
* Negative amount is not supported
|
||||
* @param threeDigitsMethod if true, will format the amount always with 3 significant digits
|
||||
* @param scale the number of digits to the right of the decimal point
|
||||
*/
|
||||
@Suppress("MagicNumber")
|
||||
fun formatCompactAmount(
|
||||
amount: BigDecimal,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
threeDigitsMethod: Boolean = false,
|
||||
scale: Int = 0,
|
||||
): String {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||
return BigDecimalFormatterCompat.formatCompactAmountNoLocaleContext(amount = amount)
|
||||
}
|
||||
|
||||
if (threeDigitsMethod) {
|
||||
val scaledAmount = amount.setScale(scale, RoundingMode.HALF_UP)
|
||||
val digitsCount = scaledAmount.longValueExact().toString().count()
|
||||
val digitsToFormat = 6 - when (digitsCount % 3) {
|
||||
0 -> 0
|
||||
1 -> 2
|
||||
else -> 1
|
||||
}
|
||||
|
||||
val formatter = CompactDecimalFormat.getInstance(
|
||||
locale,
|
||||
CompactDecimalFormat.CompactStyle.SHORT,
|
||||
).apply {
|
||||
minimumSignificantDigits = 4
|
||||
maximumSignificantDigits = digitsToFormat
|
||||
}
|
||||
|
||||
return formatter.format(amount.setScale(scale, RoundingMode.HALF_UP))
|
||||
} else {
|
||||
val value = amount.setScale(scale, RoundingMode.HALF_UP)
|
||||
|
||||
val formatter = CompactDecimalFormat.getInstance(
|
||||
locale,
|
||||
CompactDecimalFormat.CompactStyle.SHORT,
|
||||
)
|
||||
|
||||
return formatter.format(value)
|
||||
}
|
||||
}
|
||||
|
||||
private fun BigDecimal.isLessThanThreshold() = this > BigDecimal.ZERO && this < FIAT_FORMAT_THRESHOLD
|
||||
}
|
||||
|
|
@ -7,16 +7,32 @@ import java.util.Locale
|
|||
internal object BigDecimalFormatterCompat {
|
||||
|
||||
/**
|
||||
* Formats value as [BigDecimalFormatter.formatCompactAmount] does using only "T","B","M","K" suffixes
|
||||
* Formats value as [BigDecimalFormatter.formatCompactFiatAmount] does using only "T","B","M","K" suffixes
|
||||
* Used for < API24 compatibility
|
||||
*/
|
||||
@Suppress("MagicNumber", "UnnecessaryParentheses")
|
||||
fun formatCompactAmountNoLocaleContext(
|
||||
fun formatCompactFiatAmountNoLocaleContext(
|
||||
amount: BigDecimal,
|
||||
fiatCurrencyCode: String,
|
||||
fiatCurrencySymbol: String,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
): String {
|
||||
val formatted = formatCompactAmountNoLocaleContext(amount)
|
||||
|
||||
return BigDecimalFormatter.addCurrencySymbolToStringAmount(
|
||||
amount = formatted,
|
||||
fiatCurrencyCode = fiatCurrencyCode,
|
||||
fiatCurrencySymbol = fiatCurrencySymbol,
|
||||
locale = locale,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats value as [BigDecimalFormatter.formatCompactAmount] does using only "T","B","M","K" suffixes
|
||||
* Used for < API24 compatibility
|
||||
*/
|
||||
@Suppress("MagicNumber", "UnnecessaryParentheses")
|
||||
fun formatCompactAmountNoLocaleContext(amount: BigDecimal): String {
|
||||
val value = amount.setScale(0, RoundingMode.HALF_UP).longValueExact()
|
||||
|
||||
val formatted = when {
|
||||
|
|
@ -42,11 +58,6 @@ internal object BigDecimalFormatterCompat {
|
|||
else -> return value.toString()
|
||||
}
|
||||
|
||||
return BigDecimalFormatter.addCurrencySymbolToStringAmount(
|
||||
amount = formatted,
|
||||
fiatCurrencyCode = fiatCurrencyCode,
|
||||
fiatCurrencySymbol = fiatCurrencySymbol,
|
||||
locale = locale,
|
||||
)
|
||||
return formatted
|
||||
}
|
||||
}
|
||||
|
|
@ -61,7 +61,14 @@ object DateTimeFormatters {
|
|||
*/
|
||||
val dateMMMMd: DateTimeFormatter by lazy {
|
||||
DateTimeFormatterBuilder()
|
||||
.appendPattern(DateFormat.getBestDateTimePattern(Locale.getDefault(), "MMMM d"))
|
||||
.appendPattern(DateFormat.getBestDateTimePattern(Locale.getDefault(), "dd MMM"))
|
||||
.toFormatter()
|
||||
.withLocale(Locale.getDefault())
|
||||
}
|
||||
|
||||
val dateYYYY: DateTimeFormatter by lazy {
|
||||
DateTimeFormatterBuilder()
|
||||
.appendPattern(DateFormat.getBestDateTimePattern(Locale.getDefault(), "yyyy"))
|
||||
.toFormatter()
|
||||
.withLocale(Locale.getDefault())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
package com.tangem.core.ui.utils
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import com.tangem.core.ui.components.SpacerH4
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
/**
|
||||
* A container that shows a shimmer effect on top of the actual content.
|
||||
* The shimmer effect is toggled every 2 seconds.
|
||||
* Used for previewing components with shimmer effect and comparing their sizes with the actual content.
|
||||
*
|
||||
* If height is changing during the preview, it means that the actual content is not aligned with the shimmer effect.
|
||||
*
|
||||
* @param actualContent The actual content to be displayed.
|
||||
* @param shimmerContent The shimmer effect to be displayed.
|
||||
*/
|
||||
@Composable
|
||||
fun PreviewShimmerContainer(actualContent: @Composable () -> Unit, shimmerContent: @Composable () -> Unit) {
|
||||
Column {
|
||||
var height by remember { mutableIntStateOf(0) }
|
||||
Row {
|
||||
Text("height = $height")
|
||||
}
|
||||
SpacerH4()
|
||||
|
||||
var shimmerVisible by remember { mutableStateOf(true) }
|
||||
|
||||
TangemThemePreview {
|
||||
Box(
|
||||
Modifier.onGloballyPositioned {
|
||||
height = it.size.height
|
||||
},
|
||||
) {
|
||||
actualContent()
|
||||
if (shimmerVisible) {
|
||||
shimmerContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
while (true) {
|
||||
delay(timeMillis = 2000)
|
||||
shimmerVisible = !shimmerVisible
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.core.ui.utils
|
||||
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
|
||||
fun Modifier.disableNestedScroll(): Modifier = nestedScroll(DisableParentConnection)
|
||||
|
||||
private object DisableParentConnection : NestedScrollConnection {
|
||||
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
|
||||
return available.copy(x = 0f)
|
||||
}
|
||||
}
|
||||
12
core/ui/src/main/res/drawable/ic_blast_22.xml
Normal file
12
core/ui/src/main/res/drawable/ic_blast_22.xml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="22dp"
|
||||
android:height="22dp"
|
||||
android:viewportWidth="22"
|
||||
android:viewportHeight="22">
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="M15.257,11.048L17.71,9.826L18.555,7.231L16.864,6H5.604L3,7.934H16.236L15.533,10.111H10.225L9.714,11.701H15.022L13.532,16.281L16.019,15.05L16.906,12.304L15.24,11.082L15.257,11.048Z" />
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="M6.742,14.313L8.275,9.541L6.575,8.269L4.021,16.281H13.532L14.168,14.313H6.742Z" />
|
||||
</vector>
|
||||
10
core/ui/src/main/res/drawable/ic_filecoin_22.xml
Normal file
10
core/ui/src/main/res/drawable/ic_filecoin_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="M12.045,9.68L11.715,11.44L14.85,11.88L14.63,12.705L11.55,12.265C11.33,12.98 11.22,13.75 10.945,14.41C10.67,15.18 10.395,15.95 10.065,16.665C9.625,17.6 8.855,18.26 7.81,18.425C7.205,18.535 6.545,18.48 6.05,18.095C5.885,17.985 5.72,17.765 5.72,17.6C5.72,17.38 5.83,17.105 5.995,16.995C6.105,16.94 6.38,16.995 6.545,17.05C6.71,17.215 6.875,17.435 6.985,17.655C7.315,18.095 7.755,18.15 8.195,17.82C8.69,17.38 8.965,16.775 9.13,16.17C9.46,14.85 9.79,13.585 10.065,12.265V12.045L7.15,11.605L7.26,10.78L10.285,11.22L10.67,9.515L7.535,9.02L7.645,8.14L10.89,8.58C11,8.25 11.055,7.975 11.165,7.7C11.44,6.71 11.715,5.72 12.375,4.84C13.035,3.96 13.805,3.41 14.96,3.465C15.455,3.465 15.95,3.63 16.28,4.015C16.335,4.07 16.445,4.18 16.445,4.29C16.445,4.51 16.445,4.785 16.28,4.95C16.06,5.115 15.785,5.06 15.565,4.84C15.4,4.675 15.29,4.51 15.125,4.345C14.795,3.905 14.3,3.85 13.915,4.235C13.64,4.51 13.365,4.895 13.2,5.28C12.815,6.435 12.54,7.645 12.155,8.855L15.18,9.295L14.96,10.12L12.045,9.68Z"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="evenOdd" />
|
||||
</vector>
|
||||
15
core/ui/src/main/res/drawable/img_blast_22.xml
Normal file
15
core/ui/src/main/res/drawable/img_blast_22.xml
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="22dp"
|
||||
android:height="22dp"
|
||||
android:viewportWidth="22"
|
||||
android:viewportHeight="22">
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="M11,0L11,0A11,11 0,0 1,22 11L22,11A11,11 0,0 1,11 22L11,22A11,11 0,0 1,0 11L0,11A11,11 0,0 1,11 0z" />
|
||||
<path
|
||||
android:fillColor="#FCFC03"
|
||||
android:pathData="M15.257,11.048L17.71,9.826L18.555,7.231L16.864,6H5.604L3,7.934H16.236L15.533,10.111H10.225L9.714,11.701H15.022L13.532,16.281L16.019,15.05L16.906,12.304L15.24,11.082L15.257,11.048Z" />
|
||||
<path
|
||||
android:fillColor="#FCFC03"
|
||||
android:pathData="M6.742,14.313L8.275,9.541L6.575,8.269L4.021,16.281H13.532L14.168,14.313H6.742Z" />
|
||||
</vector>
|
||||
16
core/ui/src/main/res/drawable/img_filecoin_22.xml
Normal file
16
core/ui/src/main/res/drawable/img_filecoin_22.xml
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="22dp"
|
||||
android:height="22dp"
|
||||
android:viewportWidth="22"
|
||||
android:viewportHeight="22">
|
||||
<group>
|
||||
<clip-path android:pathData="M11,0L11,0A11,11 0,0 1,22 11L22,11A11,11 0,0 1,11 22L11,22A11,11 0,0 1,0 11L0,11A11,11 0,0 1,11 0z" />
|
||||
<path
|
||||
android:pathData="M11,0L11,0A11,11 0,0 1,22 11L22,11A11,11 0,0 1,11 22L11,22A11,11 0,0 1,0 11L0,11A11,11 0,0 1,11 0z"
|
||||
android:fillColor="#0090FF" />
|
||||
<path
|
||||
android:pathData="M12.045,9.68L11.715,11.44L14.85,11.88L14.63,12.705L11.55,12.265C11.33,12.98 11.22,13.75 10.945,14.41C10.67,15.18 10.395,15.95 10.065,16.665C9.625,17.6 8.855,18.26 7.81,18.425C7.205,18.535 6.545,18.48 6.05,18.095C5.885,17.985 5.72,17.765 5.72,17.6C5.72,17.38 5.83,17.105 5.995,16.995C6.105,16.94 6.38,16.995 6.545,17.05C6.71,17.215 6.875,17.435 6.985,17.655C7.315,18.095 7.755,18.15 8.195,17.82C8.69,17.38 8.965,16.775 9.13,16.17C9.46,14.85 9.79,13.585 10.065,12.265V12.045L7.15,11.605L7.26,10.78L10.285,11.22L10.67,9.515L7.535,9.02L7.645,8.14L10.89,8.58C11,8.25 11.055,7.975 11.165,7.7C11.44,6.71 11.715,5.72 12.375,4.84C13.035,3.96 13.805,3.41 14.96,3.465C15.455,3.465 15.95,3.63 16.28,4.015C16.335,4.07 16.445,4.18 16.445,4.29C16.445,4.51 16.445,4.785 16.28,4.95C16.06,5.115 15.785,5.06 15.565,4.84C15.4,4.675 15.29,4.51 15.125,4.345C14.795,3.905 14.3,3.85 13.915,4.235C13.64,4.51 13.365,4.895 13.2,5.28C12.815,6.435 12.54,7.645 12.155,8.855L15.18,9.295L14.96,10.12L12.045,9.68Z"
|
||||
android:fillColor="#ffffff"
|
||||
android:fillType="evenOdd" />
|
||||
</group>
|
||||
</vector>
|
||||
345
core/ui/src/main/res/drawable/img_staking_banner.xml
Normal file
345
core/ui/src/main/res/drawable/img_staking_banner.xml
Normal file
|
|
@ -0,0 +1,345 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="328dp"
|
||||
android:height="78dp"
|
||||
android:viewportWidth="328"
|
||||
android:viewportHeight="78">
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M14,0L314,0A14,14 0,0 1,328 14L328,64A14,14 0,0 1,314 78L14,78A14,14 0,0 1,0 64L0,14A14,14 0,0 1,14 0z"/>
|
||||
<path
|
||||
android:pathData="M14,0L314,0A14,14 0,0 1,328 14L328,64A14,14 0,0 1,314 78L14,78A14,14 0,0 1,0 64L0,14A14,14 0,0 1,14 0z"
|
||||
android:fillColor="#010101"/>
|
||||
<path
|
||||
android:pathData="M0,0h328v78h-328z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:startX="0"
|
||||
android:startY="0"
|
||||
android:endX="40.3"
|
||||
android:endY="159.38"
|
||||
android:type="linear">
|
||||
<item android:offset="0" android:color="#FF0099FF"/>
|
||||
<item android:offset="0.45" android:color="#00000000"/>
|
||||
<item android:offset="0.6" android:color="#00000000"/>
|
||||
<item android:offset="1" android:color="#FF0099FF"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M255.81,48.61h38.44v17.39h-38.44z"/>
|
||||
<path
|
||||
android:pathData="M294.04,55.66C294.04,52.02 285.53,49.07 275.03,49.07C264.54,49.07 256.03,52.02 256.03,55.66V59.19C256.03,62.83 264.54,65.78 275.03,65.78C285.53,65.78 294.04,62.83 294.04,59.19V55.66Z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:startX="255.89"
|
||||
android:startY="57.42"
|
||||
android:endX="294.04"
|
||||
android:endY="57.42"
|
||||
android:type="linear">
|
||||
<item android:offset="0" android:color="#FF8F929E"/>
|
||||
<item android:offset="0.26" android:color="#FF6A6D7A"/>
|
||||
<item android:offset="0.5" android:color="#FF5E616D"/>
|
||||
<item android:offset="0.74" android:color="#FF80828F"/>
|
||||
<item android:offset="1" android:color="#FF9295A1"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:pathData="M275.03,66C264.26,66 255.81,63.01 255.81,59.19V55.66C255.81,51.84 264.26,48.85 275.03,48.85C285.81,48.85 294.25,51.84 294.25,55.66V59.19C294.25,63.01 285.81,66 275.03,66ZM275.03,49.29C264.67,49.29 256.25,52.15 256.25,55.66V59.19C256.25,62.7 264.67,65.56 275.03,65.56C285.39,65.56 293.82,62.7 293.82,59.19V55.66C293.82,52.15 285.39,49.29 275.03,49.29V49.29Z"
|
||||
android:fillColor="#727481"/>
|
||||
<path
|
||||
android:pathData="M275.03,62.25C285.53,62.25 294.04,59.3 294.04,55.66C294.04,52.02 285.53,49.07 275.03,49.07C264.54,49.07 256.03,52.02 256.03,55.66C256.03,59.3 264.54,62.25 275.03,62.25Z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:startX="273.96"
|
||||
android:startY="49.68"
|
||||
android:endX="275.63"
|
||||
android:endY="58.7"
|
||||
android:type="linear">
|
||||
<item android:offset="0" android:color="#FFE1E3F0"/>
|
||||
<item android:offset="1" android:color="#FFA8AAB7"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:pathData="M275.03,62.48C264.26,62.48 255.81,59.49 255.81,55.66C255.81,51.84 264.26,48.85 275.03,48.85C285.81,48.85 294.25,51.84 294.25,55.66C294.25,59.48 285.81,62.47 275.03,62.47V62.48ZM275.03,49.29C264.67,49.29 256.25,52.15 256.25,55.66C256.25,59.17 264.67,62.03 275.03,62.03C285.39,62.03 293.82,59.18 293.82,55.66C293.82,52.15 285.39,49.29 275.03,49.29Z"
|
||||
android:fillColor="#F3F6FF"/>
|
||||
<path
|
||||
android:pathData="M293.95,55.02C293.01,51.68 284.9,49.07 275.03,49.07C265.17,49.07 257.06,51.68 256.12,55.02H293.95Z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:startX="271.42"
|
||||
android:startY="41.66"
|
||||
android:endX="276.67"
|
||||
android:endY="60.45"
|
||||
android:type="linear">
|
||||
<item android:offset="0.2" android:color="#FFFFFFFF"/>
|
||||
<item android:offset="1" android:color="#FFFFFFFF"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:pathData="M275.03,49.95C266.17,49.95 258.99,52.48 258.99,55.59C258.99,56.77 260.02,57.86 261.77,58.76C260.47,57.98 259.72,57.07 259.72,56.11C259.72,53.17 266.58,50.79 275.03,50.79C283.49,50.79 290.35,53.17 290.35,56.11C290.35,57.08 289.6,57.98 288.29,58.76C290.04,57.86 291.07,56.77 291.07,55.59C291.07,52.48 283.89,49.96 275.03,49.96V49.95Z"
|
||||
android:fillColor="#8D909C"/>
|
||||
<path
|
||||
android:pathData="M259.1,55.66C259.1,55.66 259.1,55.81 259.15,56.06C259.21,56.32 259.35,56.69 259.66,57.07C260.27,57.84 261.48,58.59 263.04,59.18C264.58,59.79 266.49,60.23 268.54,60.54C270.59,60.86 272.81,61.01 275.03,61C277.25,61 279.46,60.85 281.52,60.54C283.58,60.23 285.48,59.79 287.03,59.18C288.58,58.59 289.79,57.84 290.4,57.07C290.71,56.69 290.85,56.32 290.91,56.06C290.95,55.94 290.95,55.84 290.95,55.76C290.96,55.69 290.96,55.66 290.96,55.66C290.96,55.66 290.96,55.7 290.96,55.76C290.95,55.84 290.96,55.94 290.92,56.06C290.88,56.32 290.75,56.7 290.45,57.1C289.86,57.91 288.67,58.75 287.1,59.38C285.54,60.02 283.64,60.53 281.57,60.86C279.5,61.19 277.27,61.37 275.03,61.37C272.8,61.37 270.56,61.19 268.49,60.86C266.42,60.53 264.51,60.02 262.96,59.38C261.4,58.75 260.21,57.91 259.61,57.1C259.32,56.71 259.18,56.32 259.14,56.06C259.11,55.93 259.11,55.83 259.11,55.76C259.11,55.69 259.1,55.66 259.1,55.66Z"
|
||||
android:fillColor="#D4D6E3"/>
|
||||
<path
|
||||
android:pathData="M275.4,48.77C275.3,48.67 275.18,48.62 275.03,48.62C274.3,48.57 274.28,49.67 275.01,49.62C275.08,49.62 275.16,49.61 275.23,49.58C275.23,49.58 275.57,49.38 275.39,49.46C275.67,49.33 275.71,48.91 275.4,48.77Z"
|
||||
android:fillColor="#ffffff"/>
|
||||
</group>
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M254.9,44.03h38.44v17.39h-38.44z"/>
|
||||
<path
|
||||
android:pathData="M293.12,51.09C293.12,47.45 284.61,44.49 274.12,44.49C263.63,44.49 255.12,47.45 255.12,51.09V54.61C255.12,58.25 263.63,61.2 274.12,61.2C284.61,61.2 293.12,58.25 293.12,54.61V51.09Z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:startX="254.97"
|
||||
android:startY="52.85"
|
||||
android:endX="293.13"
|
||||
android:endY="52.85"
|
||||
android:type="linear">
|
||||
<item android:offset="0" android:color="#FF8F929E"/>
|
||||
<item android:offset="0.26" android:color="#FF6A6D7A"/>
|
||||
<item android:offset="0.5" android:color="#FF5E616D"/>
|
||||
<item android:offset="0.74" android:color="#FF80828F"/>
|
||||
<item android:offset="1" android:color="#FF9295A1"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:pathData="M274.12,61.42C263.34,61.42 254.9,58.43 254.9,54.61V51.09C254.9,47.27 263.34,44.27 274.12,44.27C284.89,44.27 293.34,47.27 293.34,51.09V54.61C293.34,58.43 284.89,61.42 274.12,61.42ZM274.12,44.72C263.76,44.72 255.33,47.58 255.33,51.09V54.61C255.33,58.13 263.76,60.98 274.12,60.98C284.48,60.98 292.91,58.13 292.91,54.61V51.09C292.91,47.57 284.48,44.71 274.12,44.71V44.72Z"
|
||||
android:fillColor="#727481"/>
|
||||
<path
|
||||
android:pathData="M274.12,57.68C284.61,57.68 293.12,54.72 293.12,51.09C293.12,47.45 284.61,44.49 274.12,44.49C263.62,44.49 255.12,47.45 255.12,51.09C255.12,54.72 263.62,57.68 274.12,57.68Z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:startX="273.04"
|
||||
android:startY="45.1"
|
||||
android:endX="274.72"
|
||||
android:endY="54.12"
|
||||
android:type="linear">
|
||||
<item android:offset="0" android:color="#FFE1E3F0"/>
|
||||
<item android:offset="1" android:color="#FFA8AAB7"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:pathData="M274.12,57.9C263.34,57.9 254.9,54.91 254.9,51.09C254.9,47.26 263.34,44.27 274.12,44.27C284.89,44.27 293.34,47.27 293.34,51.09C293.34,54.9 284.89,57.9 274.12,57.9V57.9ZM274.12,44.72C263.76,44.72 255.33,47.58 255.33,51.09C255.33,54.59 263.76,57.46 274.12,57.46C284.48,57.46 292.91,54.6 292.91,51.09C292.91,47.57 284.48,44.72 274.12,44.72Z"
|
||||
android:fillColor="#F3F6FF"/>
|
||||
<path
|
||||
android:pathData="M293.03,50.45C292.1,47.1 283.99,44.49 274.12,44.49C264.25,44.49 256.14,47.1 255.21,50.45H293.03Z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:startX="270.5"
|
||||
android:startY="37.09"
|
||||
android:endX="275.75"
|
||||
android:endY="55.87"
|
||||
android:type="linear">
|
||||
<item android:offset="0.2" android:color="#FFFFFFFF"/>
|
||||
<item android:offset="1" android:color="#FFFFFFFF"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:pathData="M274.12,45.37C265.26,45.37 258.08,47.9 258.08,51.01C258.08,52.19 259.1,53.28 260.86,54.19C259.55,53.41 258.8,52.5 258.8,51.53C258.8,48.6 265.66,46.21 274.12,46.21C282.57,46.21 289.43,48.6 289.43,51.53C289.43,52.5 288.68,53.41 287.37,54.19C289.13,53.28 290.16,52.19 290.16,51.01C290.16,47.9 282.98,45.38 274.12,45.38V45.37Z"
|
||||
android:fillColor="#8D909C"/>
|
||||
<path
|
||||
android:pathData="M258.19,51.09C258.19,51.09 258.19,51.23 258.24,51.49C258.29,51.74 258.44,52.11 258.75,52.49C259.36,53.27 260.57,54.01 262.12,54.61C263.67,55.21 265.57,55.66 267.62,55.97C269.68,56.28 271.9,56.43 274.11,56.42C276.33,56.43 278.55,56.28 280.61,55.97C282.66,55.65 284.56,55.21 286.11,54.61C287.66,54.01 288.88,53.27 289.48,52.49C289.8,52.11 289.94,51.74 289.99,51.49C290.03,51.36 290.03,51.26 290.04,51.19C290.04,51.12 290.05,51.09 290.05,51.09C290.05,51.09 290.05,51.12 290.04,51.19C290.04,51.26 290.04,51.36 290.01,51.49C289.97,51.75 289.83,52.13 289.53,52.53C288.94,53.34 287.75,54.17 286.19,54.8C284.63,55.44 282.73,55.96 280.66,56.29C278.59,56.62 276.35,56.79 274.12,56.8C271.88,56.79 269.65,56.62 267.58,56.29C265.5,55.96 263.6,55.44 262.05,54.8C260.49,54.17 259.29,53.34 258.7,52.53C258.4,52.13 258.27,51.75 258.22,51.49C258.19,51.36 258.2,51.25 258.19,51.19C258.19,51.12 258.19,51.09 258.19,51.09Z"
|
||||
android:fillColor="#D4D6E3"/>
|
||||
<path
|
||||
android:pathData="M274.49,44.2C274.39,44.09 274.27,44.04 274.11,44.04C273.38,43.99 273.37,45.09 274.09,45.04C274.17,45.04 274.24,45.03 274.32,45C274.32,45 274.65,44.8 274.47,44.89C274.75,44.76 274.79,44.33 274.49,44.2Z"
|
||||
android:fillColor="#ffffff"/>
|
||||
</group>
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M255.81,39.46h38.44v18.31h-38.44z"/>
|
||||
<path
|
||||
android:pathData="M294.04,46.88C294.04,43.05 285.53,39.94 275.03,39.94C264.54,39.94 256.03,43.05 256.03,46.88V50.59C256.03,54.42 264.54,57.53 275.03,57.53C285.53,57.53 294.04,54.42 294.04,50.59V46.88Z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:startX="255.89"
|
||||
android:startY="48.74"
|
||||
android:endX="294.04"
|
||||
android:endY="48.74"
|
||||
android:type="linear">
|
||||
<item android:offset="0" android:color="#FF8F929E"/>
|
||||
<item android:offset="0.26" android:color="#FF6A6D7A"/>
|
||||
<item android:offset="0.5" android:color="#FF5E616D"/>
|
||||
<item android:offset="0.74" android:color="#FF80828F"/>
|
||||
<item android:offset="1" android:color="#FF9295A1"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:pathData="M275.03,57.76C264.26,57.76 255.81,54.62 255.81,50.59V46.88C255.81,42.86 264.26,39.71 275.03,39.71C285.81,39.71 294.25,42.86 294.25,46.88V50.59C294.25,54.61 285.81,57.76 275.03,57.76ZM275.03,40.18C264.67,40.18 256.25,43.19 256.25,46.88V50.59C256.25,54.29 264.67,57.3 275.03,57.3C285.39,57.3 293.82,54.29 293.82,50.59V46.88C293.82,43.18 285.39,40.17 275.03,40.17V40.18Z"
|
||||
android:fillColor="#727481"/>
|
||||
<path
|
||||
android:pathData="M275.03,53.82C285.53,53.82 294.04,50.71 294.04,46.88C294.04,43.05 285.53,39.94 275.03,39.94C264.54,39.94 256.03,43.05 256.03,46.88C256.03,50.71 264.54,53.82 275.03,53.82Z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:startX="273.96"
|
||||
android:startY="40.58"
|
||||
android:endX="275.81"
|
||||
android:endY="50.04"
|
||||
android:type="linear">
|
||||
<item android:offset="0" android:color="#FFE1E3F0"/>
|
||||
<item android:offset="1" android:color="#FFA8AAB7"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:pathData="M275.03,54.06C264.26,54.06 255.81,50.91 255.81,46.88C255.81,42.86 264.26,39.71 275.03,39.71C285.81,39.71 294.25,42.86 294.25,46.88C294.25,50.9 285.81,54.05 275.03,54.05V54.06ZM275.03,40.18C264.67,40.18 256.25,43.19 256.25,46.88C256.25,50.57 264.67,53.59 275.03,53.59C285.39,53.59 293.82,50.58 293.82,46.88C293.82,43.18 285.39,40.18 275.03,40.18Z"
|
||||
android:fillColor="#F3F6FF"/>
|
||||
<path
|
||||
android:pathData="M293.95,46.21C293.01,42.69 284.9,39.94 275.03,39.94C265.17,39.94 257.06,42.69 256.12,46.21H293.95Z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:startX="271.42"
|
||||
android:startY="32.14"
|
||||
android:endX="277.19"
|
||||
android:endY="51.77"
|
||||
android:type="linear">
|
||||
<item android:offset="0.2" android:color="#FFFFFFFF"/>
|
||||
<item android:offset="1" android:color="#FFFFFFFF"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:pathData="M275.03,40.87C266.17,40.87 258.99,43.53 258.99,46.81C258.99,48.05 260.02,49.19 261.77,50.15C260.47,49.32 259.72,48.36 259.72,47.35C259.72,44.26 266.58,41.75 275.03,41.75C283.49,41.75 290.35,44.26 290.35,47.35C290.35,48.37 289.6,49.32 288.29,50.15C290.04,49.19 291.07,48.05 291.07,46.81C291.07,43.53 283.89,40.87 275.03,40.87V40.87Z"
|
||||
android:fillColor="#8D909C"/>
|
||||
<path
|
||||
android:pathData="M259.1,46.88C259.1,46.88 259.1,47.03 259.15,47.3C259.21,47.57 259.35,47.96 259.66,48.36C260.27,49.18 261.48,49.96 263.04,50.59C264.58,51.23 266.49,51.69 268.54,52.02C270.59,52.35 272.81,52.51 275.03,52.5C277.25,52.5 279.46,52.34 281.52,52.02C283.58,51.69 285.48,51.23 287.03,50.59C288.58,49.96 289.79,49.18 290.4,48.36C290.71,47.96 290.85,47.57 290.91,47.3C290.95,47.17 290.95,47.06 290.95,46.99C290.96,46.91 290.96,46.88 290.96,46.88C290.96,46.88 290.96,46.92 290.96,46.99C290.95,47.06 290.96,47.17 290.92,47.3C290.88,47.58 290.75,47.98 290.45,48.4C289.86,49.25 288.67,50.13 287.1,50.79C285.54,51.47 283.64,52.01 281.57,52.36C279.5,52.7 277.27,52.89 275.03,52.89C272.8,52.89 270.56,52.7 268.49,52.36C266.42,52.01 264.51,51.47 262.96,50.79C261.4,50.13 260.21,49.25 259.61,48.4C259.32,47.98 259.18,47.58 259.14,47.3C259.11,47.17 259.11,47.06 259.11,46.99C259.11,46.91 259.1,46.88 259.1,46.88Z"
|
||||
android:fillColor="#D4D6E3"/>
|
||||
</group>
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M248.49,25.73h37.53v22.88h-37.53z"/>
|
||||
<path
|
||||
android:pathData="M284.27,28.12C282.88,24.76 273.83,25.37 264.06,29.47C254.29,33.57 247.5,39.62 248.89,42.97C249.12,43.53 250,45.66 250.24,46.22C251.63,49.58 260.67,48.97 270.45,44.87C280.22,40.77 287.01,34.72 285.62,31.37C285.39,30.81 284.5,28.68 284.27,28.12Z"
|
||||
android:strokeAlpha="0.5"
|
||||
android:fillAlpha="0.5">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:startX="251.72"
|
||||
android:startY="43.7"
|
||||
android:endX="281.28"
|
||||
android:endY="31.45"
|
||||
android:type="linear">
|
||||
<item android:offset="0" android:color="#FF8F929E"/>
|
||||
<item android:offset="0.26" android:color="#FF767885"/>
|
||||
<item android:offset="0.5" android:color="#FF6C6F7C"/>
|
||||
<item android:offset="0.76" android:color="#FF767986"/>
|
||||
<item android:offset="0.77" android:color="#FF858794"/>
|
||||
<item android:offset="1" android:color="#FF9295A1"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:pathData="M255.83,48.61C252.66,48.61 250.67,47.81 250.04,46.3L248.69,43.06C247.98,41.34 249.21,38.95 252.15,36.33C255.03,33.76 259.23,31.26 263.98,29.26C269.33,27.02 274.69,25.73 278.68,25.73C281.85,25.73 283.85,26.53 284.48,28.04L285.82,31.29C286.53,33.01 285.31,35.39 282.37,38.01C279.48,40.58 275.28,43.09 270.53,45.08C265.18,47.33 259.82,48.62 255.83,48.62V48.61ZM278.68,26.17C274.75,26.17 269.45,27.45 264.15,29.67C259.44,31.64 255.29,34.13 252.44,36.66C249.69,39.11 248.47,41.38 249.1,42.88L250.44,46.13C250.99,47.45 252.9,48.17 255.83,48.17C259.77,48.17 265.06,46.9 270.37,44.67C275.07,42.7 279.23,40.21 282.08,37.68C284.83,35.23 286.04,32.96 285.42,31.45L284.07,28.2C283.53,26.89 281.62,26.16 278.68,26.16V26.17Z"
|
||||
android:strokeAlpha="0.5"
|
||||
android:fillColor="#727481"
|
||||
android:fillAlpha="0.5"/>
|
||||
<path
|
||||
android:pathData="M248.89,42.97C250.28,46.33 259.33,45.72 269.1,41.62C278.87,37.52 285.66,31.47 284.27,28.12C282.88,24.76 273.83,25.37 264.06,29.47C254.29,33.57 247.5,39.62 248.89,42.97Z"
|
||||
android:strokeAlpha="0.5"
|
||||
android:fillAlpha="0.5">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:startX="268.95"
|
||||
android:startY="41.25"
|
||||
android:endX="263.22"
|
||||
android:endY="27.6"
|
||||
android:type="linear">
|
||||
<item android:offset="0" android:color="#FFE1E3F0"/>
|
||||
<item android:offset="1" android:color="#FFA8AAB7"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:pathData="M254.48,45.36C251.31,45.36 249.32,44.56 248.68,43.05C247.97,41.33 249.2,38.94 252.14,36.33C255.02,33.76 259.23,31.25 263.98,29.26C269.33,27.02 274.69,25.73 278.68,25.73C281.85,25.73 283.85,26.53 284.48,28.04C285.19,29.75 283.96,32.14 281.02,34.76C278.14,37.33 273.93,39.84 269.19,41.83C263.83,44.08 258.47,45.36 254.48,45.36L254.48,45.36ZM278.68,26.17C274.75,26.17 269.45,27.45 264.15,29.67C259.44,31.64 255.29,34.13 252.44,36.66C249.69,39.11 248.47,41.38 249.09,42.88C249.64,44.19 251.55,44.92 254.48,44.92C258.42,44.92 263.72,43.65 269.02,41.42C273.73,39.45 277.88,36.96 280.73,34.42C283.48,31.98 284.7,29.71 284.07,28.2C283.53,26.89 281.62,26.16 278.68,26.16V26.17Z"
|
||||
android:strokeAlpha="0.5"
|
||||
android:fillColor="#F3F6FF"
|
||||
android:fillAlpha="0.5"/>
|
||||
<path
|
||||
android:pathData="M258.69,44.75C261.81,44.23 265.4,43.18 269.1,41.63C278.76,37.57 285.5,31.62 284.31,28.24C264.01,31.71 254,36.96 258.69,44.75Z"
|
||||
android:strokeAlpha="0.5"
|
||||
android:fillAlpha="0.5">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:startX="274.55"
|
||||
android:startY="43.99"
|
||||
android:endX="268.53"
|
||||
android:endY="29.66"
|
||||
android:type="linear">
|
||||
<item android:offset="0.2" android:color="#FFFFFFFF"/>
|
||||
<item android:offset="1" android:color="#FFFFFFFF"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:pathData="M264.4,30.28C256.15,33.74 250.43,38.87 251.62,41.75C252.07,42.83 253.45,43.44 255.42,43.58C253.91,43.37 252.86,42.83 252.49,41.93C251.37,39.23 256.85,34.35 264.72,31.05C272.6,27.74 279.89,27.26 281.01,29.97C281.38,30.86 281.02,31.99 280.11,33.22C281.39,31.7 281.94,30.29 281.48,29.21C280.29,26.34 272.64,26.82 264.4,30.27L264.4,30.28Z"
|
||||
android:strokeAlpha="0.5"
|
||||
android:fillColor="#8D909C"
|
||||
android:fillAlpha="0.5"/>
|
||||
</group>
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M243,12h30.2v32.95h-30.2z"/>
|
||||
<path
|
||||
android:pathData="M269.73,12.97C267.2,10.4 259.39,14.69 252.29,22.57C245.19,30.45 241.49,38.91 244.01,41.49C244.44,41.92 246.04,43.55 246.46,43.98C249,46.55 256.81,42.25 263.91,34.38C271.02,26.51 274.72,18.04 272.18,15.47C271.76,15.03 270.16,13.4 269.73,12.98V12.97Z"
|
||||
android:strokeAlpha="0.25"
|
||||
android:fillAlpha="0.25">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:startX="246.82"
|
||||
android:startY="41.01"
|
||||
android:endX="269.6"
|
||||
android:endY="18.59"
|
||||
android:type="linear">
|
||||
<item android:offset="0" android:color="#FFA6A8B5"/>
|
||||
<item android:offset="0.16" android:color="#FFD2D5E2"/>
|
||||
<item android:offset="0.17" android:color="#FFDEE1ED"/>
|
||||
<item android:offset="0.48" android:color="#FFFFFFFF"/>
|
||||
<item android:offset="0.8" android:color="#FFD6D9E6"/>
|
||||
<item android:offset="1" android:color="#FFA3A6B3"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:pathData="M248.53,44.95C247.59,44.95 246.85,44.68 246.32,44.14L243.87,41.65C241.21,38.95 244.85,30.5 252.14,22.41C257.76,16.19 264,12 267.68,12C268.61,12 269.35,12.27 269.88,12.81L272.33,15.3C274.99,18 271.36,26.45 264.06,34.54C258.44,40.76 252.2,44.95 248.52,44.95H248.53ZM267.68,12.45C264.11,12.45 257.99,16.58 252.45,22.72C245.43,30.5 241.72,38.84 244.16,41.32L246.61,43.81C247.05,44.27 247.7,44.49 248.52,44.49C252.09,44.49 258.21,40.36 263.75,34.22C270.76,26.45 274.48,18.1 272.03,15.62L269.58,13.13C269.14,12.68 268.49,12.45 267.67,12.45H267.68Z"
|
||||
android:strokeAlpha="0.25"
|
||||
android:fillColor="#9EA0AD"
|
||||
android:fillAlpha="0.25"/>
|
||||
<path
|
||||
android:pathData="M244.01,41.49C246.55,44.06 254.36,39.77 261.46,31.89C268.57,24.01 272.27,15.54 269.73,12.97C267.2,10.4 259.39,14.69 252.29,22.57C245.19,30.45 241.49,38.91 244.01,41.49Z"
|
||||
android:strokeAlpha="0.25"
|
||||
android:fillAlpha="0.25">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:startX="253.84"
|
||||
android:startY="24.14"
|
||||
android:endX="259.77"
|
||||
android:endY="29.49"
|
||||
android:type="linear">
|
||||
<item android:offset="0" android:color="#FF8A8C99"/>
|
||||
<item android:offset="1" android:color="#FFA8AAB7"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:pathData="M246.08,42.46C245.14,42.46 244.4,42.19 243.87,41.65C241.21,38.95 244.85,30.5 252.14,22.41C257.76,16.19 264,12 267.68,12C268.61,12 269.35,12.27 269.88,12.81C271.18,14.12 271.01,16.87 269.41,20.54C267.83,24.13 265.07,28.22 261.62,32.05C256,38.27 249.76,42.46 246.08,42.46ZM267.68,12.45C264.11,12.45 257.99,16.58 252.45,22.72C245.43,30.5 241.72,38.84 244.16,41.32C244.6,41.78 245.25,42.01 246.07,42.01C249.64,42.01 255.76,37.87 261.3,31.73C264.73,27.94 267.46,23.9 269.01,20.34C270.51,16.92 270.72,14.28 269.58,13.13C269.14,12.68 268.49,12.45 267.67,12.45H267.68Z"
|
||||
android:strokeAlpha="0.25"
|
||||
android:fillColor="#F3F6FF"
|
||||
android:fillAlpha="0.25"/>
|
||||
<path
|
||||
android:pathData="M252.9,23.19C246.91,29.84 243.8,37.01 245.97,39.21C246.79,40.04 248.24,40.04 250.06,39.37C248.64,39.79 247.49,39.72 246.82,39.03C244.78,36.95 247.76,30.13 253.49,23.78C259.21,17.44 265.51,13.97 267.55,16.05C268.22,16.73 268.35,17.94 268,19.47C268.56,17.52 268.5,15.98 267.68,15.14C265.51,12.94 258.9,16.55 252.9,23.19H252.9Z"
|
||||
android:strokeAlpha="0.25"
|
||||
android:fillColor="#D7DAE6"
|
||||
android:fillAlpha="0.25"/>
|
||||
</group>
|
||||
</group>
|
||||
</vector>
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package com.tangem.utils
|
||||
|
||||
import java.util.Locale
|
||||
|
||||
object SupportedLanguages {
|
||||
const val ENGLISH = "en"
|
||||
const val RUSSIAN = "ru"
|
||||
const val GERMAN = "de"
|
||||
const val FRANCH = "fr"
|
||||
const val ITALIAN = "it"
|
||||
const val JAPANESE = "ja"
|
||||
const val UKRAINIAN = "uk"
|
||||
const val CHINESE = "uk"
|
||||
|
||||
val supportedLangugeCodes = listOf(
|
||||
ENGLISH,
|
||||
RUSSIAN,
|
||||
GERMAN,
|
||||
FRANCH,
|
||||
ITALIAN,
|
||||
JAPANESE,
|
||||
UKRAINIAN,
|
||||
CHINESE,
|
||||
)
|
||||
|
||||
fun getCurrentSupportedLanguageCode(): String {
|
||||
val locale = Locale.getDefault()
|
||||
|
||||
return if (supportedLangugeCodes.contains(locale.language)) {
|
||||
locale.language
|
||||
} else {
|
||||
ENGLISH
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue