Updated on 2026-08-14
This commit is contained in:
commit
2a5e3fa7eb
220 changed files with 5932 additions and 1815 deletions
|
|
@ -688,6 +688,16 @@
|
|||
"networkId": "base/test"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "blast-ethereum",
|
||||
"name": "Blast",
|
||||
"symbol": "ETH",
|
||||
"networks": [
|
||||
{
|
||||
"networkId": "blast/test"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,12 +31,6 @@ class FeedbackDataBuilder(
|
|||
builder.appendDelimiter()
|
||||
builder.appendKeyValue("Blockchain", walletInfo.blockchain.fullName)
|
||||
builder.appendKeyValue("Derivation path", walletInfo.derivationPath)
|
||||
|
||||
// enable later
|
||||
// if (walletInfo.blockchain == Blockchain.Bitcoin) {
|
||||
// builder.appendKeyValue("XPUB", infoHolder.extendedPublicKey)
|
||||
// }
|
||||
|
||||
builder.appendKeyValue("Outputs count", walletInfo.outputsCount)
|
||||
|
||||
if (walletInfo.tokens.isNotEmpty()) {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import com.tangem.common.CompletionResult
|
|||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.datasource.config.ConfigManager
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.apptheme.model.AppThemeMode
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.redux.StateDialog
|
||||
import com.tangem.tap.common.feedback.FeedbackData
|
||||
|
|
@ -93,6 +92,4 @@ sealed class GlobalAction : Action {
|
|||
object FetchUserCountry : GlobalAction() {
|
||||
data class Success(val countryCode: String) : GlobalAction()
|
||||
}
|
||||
|
||||
data class ChangeAppThemeMode(val appThemeMode: AppThemeMode) : GlobalAction()
|
||||
}
|
||||
|
|
@ -92,9 +92,6 @@ fun globalReducer(action: Action, state: AppState, appStateHolder: AppStateHolde
|
|||
userCountryCode = action.countryCode,
|
||||
)
|
||||
}
|
||||
is GlobalAction.ChangeAppThemeMode -> globalState.copy(
|
||||
appThemeMode = action.appThemeMode,
|
||||
)
|
||||
else -> globalState
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import com.tangem.domain.card.repository.CardSdkConfigRepository
|
|||
import com.tangem.domain.card.repository.DerivationsRepository
|
||||
import com.tangem.domain.demo.DemoConfig
|
||||
import com.tangem.domain.demo.IsDemoCardUseCase
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase
|
||||
import com.tangem.tap.domain.card.DefaultDeleteSavedAccessCodesUseCase
|
||||
|
|
@ -69,8 +70,9 @@ internal object CardDomainModule {
|
|||
@Singleton
|
||||
fun provideGetExtendedPublicKeyForCurrencyUseCase(
|
||||
derivationsRepository: DerivationsRepository,
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
): GetExtendedPublicKeyForCurrencyUseCase {
|
||||
return GetExtendedPublicKeyForCurrencyUseCase(derivationsRepository)
|
||||
return GetExtendedPublicKeyForCurrencyUseCase(derivationsRepository, walletManagersFacade)
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.tap.di.domain
|
||||
|
||||
import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase
|
||||
import com.tangem.domain.markets.GetTokenPriceChartUseCase
|
||||
import com.tangem.domain.markets.repositories.MarketsTokenRepository
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
|
@ -19,4 +20,10 @@ object MarketsDomainModule {
|
|||
): GetMarketsTokenListFlowUseCase {
|
||||
return GetMarketsTokenListFlowUseCase(marketsTokenRepository = marketsTokenRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetTokenPriceChartUseCase(marketsTokenRepository: MarketsTokenRepository): GetTokenPriceChartUseCase {
|
||||
return GetTokenPriceChartUseCase(marketsTokenRepository = marketsTokenRepository)
|
||||
}
|
||||
}
|
||||
|
|
@ -197,5 +197,11 @@ internal object SettingsDomainModule {
|
|||
): NeverRequestPermissionUseCase {
|
||||
return NeverRequestPermissionUseCase(repository = permissionRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideShouldSaveAccessCodesUseCase(settingsRepository: SettingsRepository): ShouldSaveAccessCodesUseCase {
|
||||
return ShouldSaveAccessCodesUseCase(settingsRepository = settingsRepository)
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -8,12 +8,10 @@ import com.tangem.common.doOnSuccess
|
|||
import com.tangem.common.extensions.ByteArrayKey
|
||||
import com.tangem.common.extensions.toMapKey
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.card.repository.DerivationsRepository
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.operations.derivation.ExtendedPublicKeysMap
|
||||
|
|
@ -46,12 +44,16 @@ internal class DefaultDerivationsRepository(
|
|||
return
|
||||
}
|
||||
|
||||
derivePublicKeys(userWalletId = userWalletId, derivations = derivations)
|
||||
}
|
||||
|
||||
override suspend fun derivePublicKeys(userWalletId: UserWalletId, derivations: Derivations): DerivedKeys {
|
||||
tangemSdkManager.derivePublicKeys(cardId = null, derivations = derivations)
|
||||
.doOnSuccess { response ->
|
||||
updatePublicKeys(userWalletId = userWalletId, keys = response.entries)
|
||||
.doOnSuccess {
|
||||
validateDerivations(scanResponse = it.scanResponse, derivations = derivations)
|
||||
return
|
||||
return response.entries
|
||||
}
|
||||
.doOnFailure { throw it }
|
||||
}
|
||||
|
|
@ -60,28 +62,6 @@ internal class DefaultDerivationsRepository(
|
|||
error("This code should never be reached")
|
||||
}
|
||||
|
||||
override suspend fun deriveExtendedPublicKey(
|
||||
userWalletId: UserWalletId,
|
||||
derivation: DerivationPath,
|
||||
): ExtendedPublicKey? {
|
||||
val userWallet = userWalletsStore.getSyncOrNull(userWalletId) ?: error("User wallet not found")
|
||||
val walletCard = userWallet.scanResponse.card.wallets.firstOrNull {
|
||||
UserWalletIdBuilder.scanResponse(userWallet.scanResponse).build()?.value
|
||||
.contentEquals(userWallet.walletId.value)
|
||||
} ?: return null
|
||||
|
||||
val result = tangemSdkManager.deriveExtendedPublicKey(
|
||||
cardId = null,
|
||||
walletPublicKey = walletCard.publicKey,
|
||||
derivation = derivation,
|
||||
)
|
||||
|
||||
return when (result) {
|
||||
is CompletionResult.Failure -> throw result.error
|
||||
is CompletionResult.Success -> result.data
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* It throws an exception if any of the provided derivations are invalid
|
||||
* Validation for NonHardened moved to application layer, to avoid fails when derive multiple paths
|
||||
|
|
|
|||
|
|
@ -134,8 +134,6 @@ class DetailsMiddleware {
|
|||
|
||||
scope.launch {
|
||||
repository.changeAppThemeMode(appThemeMode)
|
||||
|
||||
store.dispatchWithMain(GlobalAction.ChangeAppThemeMode(appThemeMode))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,10 +6,10 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.core.analytics.Analytics
|
||||
|
||||
import com.tangem.core.ui.UiDependencies
|
||||
import com.tangem.core.ui.components.SystemBarsIconsDisposable
|
||||
import com.tangem.core.ui.screen.ComposeFragment
|
||||
|
|
@ -18,6 +18,7 @@ import com.tangem.tap.common.analytics.events.IntroductionProcess
|
|||
import com.tangem.tap.common.extensions.dispatchNavigationAction
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.features.home.compose.StoriesScreen
|
||||
import com.tangem.tap.features.home.featuretoggles.HomeFeatureToggles
|
||||
import com.tangem.tap.features.home.redux.HomeAction
|
||||
import com.tangem.tap.features.home.redux.HomeState
|
||||
import com.tangem.tap.store
|
||||
|
|
@ -26,17 +27,21 @@ import org.rekotlin.StoreSubscriber
|
|||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class HomeFragment : ComposeFragment(), StoreSubscriber<HomeState> {
|
||||
internal class HomeFragment : ComposeFragment(), StoreSubscriber<HomeState> {
|
||||
|
||||
@Inject
|
||||
override lateinit var uiDependencies: UiDependencies
|
||||
|
||||
@Inject
|
||||
lateinit var homeFeatureToggles: HomeFeatureToggles
|
||||
|
||||
private var homeState: MutableState<HomeState> = mutableStateOf(store.state.homeState)
|
||||
|
||||
private val viewModel by viewModels<HomeViewModel>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
store.dispatch(HomeAction.OnCreate)
|
||||
store.dispatch(HomeAction.Init)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
@ -73,17 +78,29 @@ class HomeFragment : ComposeFragment(), StoreSubscriber<HomeState> {
|
|||
StoriesScreen(
|
||||
homeState = homeState,
|
||||
onScanButtonClick = {
|
||||
Analytics.send(IntroductionProcess.ButtonScanCard())
|
||||
store.dispatch(action = HomeAction.ReadCard(scope = requireActivity().lifecycleScope))
|
||||
if (homeFeatureToggles.isCallbacksRefactoringEnabled) {
|
||||
viewModel.onScanClick()
|
||||
} else {
|
||||
Analytics.send(IntroductionProcess.ButtonScanCard())
|
||||
store.dispatch(action = HomeAction.ReadCard(scope = requireActivity().lifecycleScope))
|
||||
}
|
||||
},
|
||||
onShopButtonClick = {
|
||||
Analytics.send(IntroductionProcess.ButtonBuyCards())
|
||||
store.dispatch(HomeAction.GoToShop(store.state.globalState.userCountryCode))
|
||||
if (homeFeatureToggles.isCallbacksRefactoringEnabled) {
|
||||
viewModel.onShopClick()
|
||||
} else {
|
||||
Analytics.send(IntroductionProcess.ButtonBuyCards())
|
||||
store.dispatch(HomeAction.GoToShop)
|
||||
}
|
||||
},
|
||||
onSearchTokensClick = {
|
||||
Analytics.send(IntroductionProcess.ButtonTokensList())
|
||||
store.dispatchNavigationAction { push(AppRoute.ManageTokens) }
|
||||
store.dispatch(TokensAction.SetArgs.ReadAccess)
|
||||
if (homeFeatureToggles.isCallbacksRefactoringEnabled) {
|
||||
viewModel.onSearchClick()
|
||||
} else {
|
||||
Analytics.send(IntroductionProcess.ButtonTokensList())
|
||||
store.dispatchNavigationAction { push(AppRoute.ManageTokens(readOnlyContent = true)) }
|
||||
store.dispatch(TokensAction.SetArgs.ReadAccess)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
131
app/src/main/java/com/tangem/tap/features/home/HomeViewModel.kt
Normal file
131
app/src/main/java/com/tangem/tap/features/home/HomeViewModel.kt
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
package com.tangem.tap.features.home
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.domain.card.ScanCardProcessor
|
||||
import com.tangem.domain.card.SetAccessCodeRequestPolicyUseCase
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.settings.ShouldSaveAccessCodesUseCase
|
||||
import com.tangem.domain.tokens.TokensAction
|
||||
import com.tangem.domain.wallets.builder.UserWalletBuilder
|
||||
import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase
|
||||
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
|
||||
import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter
|
||||
import com.tangem.tap.common.analytics.events.IntroductionProcess
|
||||
import com.tangem.tap.common.analytics.events.Shop
|
||||
import com.tangem.tap.common.extensions.dispatchNavigationAction
|
||||
import com.tangem.tap.common.extensions.onUserWalletSelected
|
||||
import com.tangem.tap.features.home.redux.HIDE_PROGRESS_DELAY
|
||||
import com.tangem.tap.features.home.redux.HomeAction
|
||||
import com.tangem.tap.features.home.redux.HomeMiddleware.NEW_BUY_WALLET_URL
|
||||
import com.tangem.tap.store
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@HiltViewModel
|
||||
internal class HomeViewModel @Inject constructor(
|
||||
private val shouldSaveAccessCodesUseCase: ShouldSaveAccessCodesUseCase,
|
||||
private val setAccessCodeRequestPolicyUseCase: SetAccessCodeRequestPolicyUseCase,
|
||||
private val scanCardProcessor: ScanCardProcessor,
|
||||
private val generateWalletNameUseCase: GenerateWalletNameUseCase,
|
||||
private val saveWalletUseCase: SaveWalletUseCase,
|
||||
private val urlOpener: UrlOpener,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
) : ViewModel() {
|
||||
|
||||
fun onScanClick() {
|
||||
analyticsEventHandler.send(IntroductionProcess.ButtonScanCard())
|
||||
scanCard()
|
||||
}
|
||||
|
||||
fun onShopClick() {
|
||||
analyticsEventHandler.send(IntroductionProcess.ButtonBuyCards())
|
||||
analyticsEventHandler.send(Shop.ScreenOpened())
|
||||
|
||||
urlOpener.openUrl(NEW_BUY_WALLET_URL)
|
||||
}
|
||||
|
||||
fun onSearchClick() {
|
||||
analyticsEventHandler.send(IntroductionProcess.ButtonTokensList())
|
||||
|
||||
store.dispatch(TokensAction.SetArgs.ReadAccess)
|
||||
store.dispatchNavigationAction { push(AppRoute.ManageTokens(readOnlyContent = true)) }
|
||||
}
|
||||
|
||||
private fun scanCard() {
|
||||
viewModelScope.launch {
|
||||
setAccessCodeRequestPolicyUseCase(isBiometricsRequestPolicy = shouldSaveAccessCodesUseCase())
|
||||
|
||||
scanCardProcessor.scan(
|
||||
analyticsSource = AnalyticsParam.ScreensSources.Intro,
|
||||
onProgressStateChange = { showProgress ->
|
||||
if (showProgress) {
|
||||
store.dispatch(HomeAction.ScanInProgress(scanInProgress = true))
|
||||
} else {
|
||||
delay(HIDE_PROGRESS_DELAY)
|
||||
store.dispatch(HomeAction.ScanInProgress(scanInProgress = false))
|
||||
}
|
||||
},
|
||||
onFailure = {
|
||||
Timber.e(it, "Unable to scan card")
|
||||
delay(HIDE_PROGRESS_DELAY)
|
||||
store.dispatch(HomeAction.ScanInProgress(scanInProgress = false))
|
||||
},
|
||||
onSuccess = ::proceedWithScanResponse,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun proceedWithScanResponse(scanResponse: ScanResponse) {
|
||||
val userWallet = UserWalletBuilder(
|
||||
scanResponse = scanResponse,
|
||||
generateWalletNameUseCase = generateWalletNameUseCase,
|
||||
).build()
|
||||
|
||||
if (userWallet == null) {
|
||||
Timber.e("User wallet not created")
|
||||
return
|
||||
}
|
||||
|
||||
saveWalletUseCase(userWallet).fold(
|
||||
ifLeft = { Timber.e(it.toString(), "Unable to save user wallet") },
|
||||
ifRight = {
|
||||
sendSignedInCardAnalyticsEvent(scanResponse)
|
||||
coroutineScope { store.onUserWalletSelected(userWallet = userWallet) }
|
||||
},
|
||||
)
|
||||
|
||||
store.dispatch(HomeAction.ScanInProgress(scanInProgress = false))
|
||||
delay(HIDE_PROGRESS_DELAY)
|
||||
|
||||
store.dispatchNavigationAction { push(AppRoute.Wallet) }
|
||||
}
|
||||
|
||||
private fun sendSignedInCardAnalyticsEvent(scanResponse: ScanResponse) {
|
||||
val currency = ParamCardCurrencyConverter().convert(value = scanResponse.cardTypesResolver)
|
||||
|
||||
if (currency != null) {
|
||||
Analytics.send(
|
||||
event = Basic.SignedIn(
|
||||
currency = currency,
|
||||
batch = scanResponse.card.batchId,
|
||||
signInType = Basic.SignedIn.SignInType.Card,
|
||||
walletsCount = "1",
|
||||
hasBackup = scanResponse.card.backupStatus?.isActive,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -33,7 +33,7 @@ import com.tangem.wallet.R
|
|||
import kotlin.math.max
|
||||
|
||||
@Composable
|
||||
fun StoriesScreen(
|
||||
internal fun StoriesScreen(
|
||||
homeState: MutableState<HomeState>,
|
||||
onScanButtonClick: () -> Unit,
|
||||
onShopButtonClick: () -> Unit,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.tap.features.home.featuretoggles
|
||||
|
||||
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class HomeFeatureToggles @Inject constructor(
|
||||
private val featureTogglesManager: FeatureTogglesManager,
|
||||
) {
|
||||
|
||||
val isCallbacksRefactoringEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled(name = "HOME_SCREEN_CALLBACKS_REFACTORING_ENABLED")
|
||||
}
|
||||
|
|
@ -5,22 +5,17 @@ import org.rekotlin.Action
|
|||
|
||||
sealed class HomeAction : Action {
|
||||
|
||||
object OnCreate : HomeAction()
|
||||
object Init : HomeAction()
|
||||
|
||||
data class InsertStory(val position: Int, val story: Stories) : HomeAction()
|
||||
data object OnCreate : HomeAction()
|
||||
|
||||
/**
|
||||
* Action for scanning card
|
||||
*
|
||||
* @property scope lifecycle scope. It will be canceled when lifecycle-aware component is destroyed
|
||||
* @property scope lifecycle scope. It will be canceled when lifecycle-aware component is destroyed
|
||||
*/
|
||||
data class ReadCard(
|
||||
val scope: CoroutineScope,
|
||||
) : HomeAction()
|
||||
data class ReadCard(val scope: CoroutineScope) : HomeAction()
|
||||
|
||||
data class ScanInProgress(val scanInProgress: Boolean) : HomeAction()
|
||||
data class GoToShop(val userCountryCode: String?) : HomeAction()
|
||||
data object GoToShop : HomeAction()
|
||||
|
||||
data class UpdateCountryCode(val userCountryCode: String) : HomeAction()
|
||||
}
|
||||
|
|
@ -29,7 +29,7 @@ import org.rekotlin.Action
|
|||
import org.rekotlin.Middleware
|
||||
import timber.log.Timber
|
||||
|
||||
private const val HIDE_PROGRESS_DELAY = 400L
|
||||
internal const val HIDE_PROGRESS_DELAY = 400L
|
||||
|
||||
object HomeMiddleware {
|
||||
val handler = homeMiddleware
|
||||
|
|
@ -51,8 +51,7 @@ private fun handleHomeAction(action: Action) {
|
|||
is HomeAction.OnCreate -> {
|
||||
Analytics.eraseContext()
|
||||
Analytics.send(IntroductionProcess.ScreenOpened())
|
||||
}
|
||||
is HomeAction.Init -> {
|
||||
|
||||
store.dispatch(GlobalAction.RestoreAppCurrency)
|
||||
store.dispatch(GlobalAction.ExchangeManager.Init)
|
||||
store.dispatch(GlobalAction.FetchUserCountry)
|
||||
|
|
|
|||
|
|
@ -12,13 +12,6 @@ private fun internalReduce(action: Action, appState: AppState): HomeState {
|
|||
|
||||
var state = appState.homeState
|
||||
when (action) {
|
||||
is HomeAction.InsertStory -> {
|
||||
state = state.copy(
|
||||
stories = state.stories.toMutableList().apply {
|
||||
add(action.position, action.story)
|
||||
},
|
||||
)
|
||||
}
|
||||
is HomeAction.ScanInProgress -> {
|
||||
state = state.copy(scanInProgress = action.scanInProgress)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,18 +96,27 @@ class TransactionManagerImpl(
|
|||
// for not EVM blockchains set gasLimit ZERO for now
|
||||
when (fee.data) {
|
||||
is TransactionFee.Single -> {
|
||||
val normalFee = (fee.data as TransactionFee.Single).normal
|
||||
val singleFee = if (normalFee as? Fee.CardanoToken != null) {
|
||||
ProxyFee.CardanoToken(
|
||||
gasLimit = BigInteger.ZERO,
|
||||
fee = convertToProxyAmount(amount = normalFee.amount),
|
||||
minAdaValue = normalFee.minAdaValue,
|
||||
)
|
||||
} else {
|
||||
ProxyFee.Common(
|
||||
gasLimit = BigInteger.ZERO,
|
||||
fee = convertToProxyAmount(amount = normalFee.amount),
|
||||
)
|
||||
val singleFee = when (val normalFee = (fee.data as TransactionFee.Single).normal) {
|
||||
is Fee.CardanoToken -> {
|
||||
ProxyFee.CardanoToken(
|
||||
gasLimit = BigInteger.ZERO,
|
||||
fee = convertToProxyAmount(amount = normalFee.amount),
|
||||
minAdaValue = normalFee.minAdaValue,
|
||||
)
|
||||
}
|
||||
is Fee.Filecoin -> {
|
||||
ProxyFee.Filecoin(
|
||||
gasLimit = BigInteger.ZERO,
|
||||
fee = convertToProxyAmount(amount = normalFee.amount),
|
||||
gasPremium = normalFee.gasPremium,
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
ProxyFee.Common(
|
||||
gasLimit = BigInteger.ZERO,
|
||||
fee = convertToProxyAmount(amount = normalFee.amount),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ProxyFees.SingleFee(singleFee = singleFee)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import com.tangem.feature.walletsettings.component.WalletSettingsComponent
|
|||
import com.tangem.features.details.DetailsFeatureToggles
|
||||
import com.tangem.features.details.component.DetailsComponent
|
||||
import com.tangem.features.disclaimer.api.components.DisclaimerComponent
|
||||
import com.tangem.features.managetokens.ManageTokensToggles
|
||||
import com.tangem.features.managetokens.component.ManageTokensComponent
|
||||
import com.tangem.features.pushnotifications.api.featuretoggles.PushNotificationsFeatureToggles
|
||||
import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter
|
||||
import com.tangem.features.send.api.navigation.SendRouter
|
||||
|
|
@ -47,6 +49,7 @@ internal class ChildFactory @Inject constructor(
|
|||
private val detailsComponentFactory: DetailsComponent.Factory,
|
||||
private val walletSettingsComponentFactory: WalletSettingsComponent.Factory,
|
||||
private val disclaimerComponentFactory: DisclaimerComponent.Factory,
|
||||
private val manageTokensComponentFactory: ManageTokensComponent.Factory,
|
||||
private val sendRouter: SendRouter,
|
||||
private val tokenDetailsRouter: TokenDetailsRouter,
|
||||
private val walletRouter: WalletRouter,
|
||||
|
|
@ -55,6 +58,7 @@ internal class ChildFactory @Inject constructor(
|
|||
private val testerRouter: TesterRouter,
|
||||
private val detailsFeatureToggles: DetailsFeatureToggles,
|
||||
private val pushNotificationsFeatureToggles: PushNotificationsFeatureToggles,
|
||||
private val manageTokensToggles: ManageTokensToggles,
|
||||
private val pushNotificationRouter: PushNotificationsRouter,
|
||||
) {
|
||||
|
||||
|
|
@ -119,7 +123,21 @@ internal class ChildFactory @Inject constructor(
|
|||
route.asFragmentChild(Provider { HomeFragment() })
|
||||
}
|
||||
is AppRoute.ManageTokens -> {
|
||||
route.asFragmentChild(Provider { TokensListFragment() })
|
||||
if (manageTokensToggles.isFeatureEnabled) {
|
||||
route.asComponentChild(
|
||||
contextProvider = contextProvider(route, contextFactory),
|
||||
params = ManageTokensComponent.Params(
|
||||
mode = if (route.readOnlyContent) {
|
||||
ManageTokensComponent.Mode.READ_ONLY
|
||||
} else {
|
||||
ManageTokensComponent.Mode.MANAGE
|
||||
},
|
||||
),
|
||||
componentFactory = manageTokensComponentFactory,
|
||||
)
|
||||
} else {
|
||||
route.asFragmentChild(Provider { TokensListFragment() })
|
||||
}
|
||||
}
|
||||
is AppRoute.OnboardingNote -> {
|
||||
route.asFragmentChild(Provider { OnboardingNoteFragment() })
|
||||
|
|
|
|||
|
|
@ -94,6 +94,11 @@ val generateComposeMetrics by tasks.registering {
|
|||
"-P",
|
||||
"plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=$outputDirectory",
|
||||
)
|
||||
// Compose strong skipping mode
|
||||
// freeCompilerArgs.addAll(
|
||||
// "-P",
|
||||
// "plugin:androidx.compose.compiler.plugins.kotlin:experimentalStrongSkipping=true",
|
||||
// )
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -176,7 +176,11 @@ sealed class AppRoute(val path: String) : Route {
|
|||
}
|
||||
|
||||
@Serializable
|
||||
data object ManageTokens : AppRoute(path = "/manage_tokens")
|
||||
data class ManageTokens(
|
||||
val readOnlyContent: Boolean,
|
||||
) : AppRoute(path = "/manage_tokens/$readOnlyContent"), RouteBundleParams {
|
||||
override fun getBundle(): Bundle = bundle(serializer())
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data object AddCustomToken : AppRoute(path = "/add_custom_token")
|
||||
|
|
|
|||
|
|
@ -22,4 +22,5 @@ dependencies {
|
|||
implementation(deps.compose.material3)
|
||||
implementation(deps.compose.ui.tooling)
|
||||
implementation(deps.compose.ui.utils)
|
||||
implementation(deps.kotlin.immutable.collections)
|
||||
}
|
||||
|
|
@ -3,7 +3,6 @@ package com.tangem.common.ui.charts
|
|||
import android.content.res.Configuration
|
||||
import androidx.annotation.FloatRange
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
|
|
@ -12,10 +11,11 @@ import androidx.compose.material3.Text
|
|||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalFontFamilyResolver
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontSynthesis
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
|
|
@ -23,26 +23,32 @@ import androidx.compose.ui.text.font.resolveAsTypeface
|
|||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.patrykandpatrick.vico.compose.cartesian.*
|
||||
import com.patrykandpatrick.vico.compose.cartesian.CartesianChartHost
|
||||
import com.patrykandpatrick.vico.compose.cartesian.axis.rememberAxisGuidelineComponent
|
||||
import com.patrykandpatrick.vico.compose.cartesian.axis.rememberAxisLabelComponent
|
||||
import com.patrykandpatrick.vico.compose.cartesian.axis.rememberBottomAxis
|
||||
import com.patrykandpatrick.vico.compose.cartesian.axis.rememberCustomStartAxis
|
||||
import com.patrykandpatrick.vico.compose.cartesian.rememberCartesianChart
|
||||
import com.patrykandpatrick.vico.compose.cartesian.rememberVicoScrollState
|
||||
import com.patrykandpatrick.vico.compose.cartesian.rememberVicoZoomState
|
||||
import com.patrykandpatrick.vico.compose.common.of
|
||||
import com.patrykandpatrick.vico.core.cartesian.HorizontalLayout
|
||||
import com.patrykandpatrick.vico.core.cartesian.Zoom
|
||||
import com.patrykandpatrick.vico.core.cartesian.axis.*
|
||||
import com.patrykandpatrick.vico.core.cartesian.axis.AxisPosition
|
||||
import com.patrykandpatrick.vico.core.cartesian.axis.BaseAxis
|
||||
import com.patrykandpatrick.vico.core.cartesian.axis.HorizontalAxis
|
||||
import com.patrykandpatrick.vico.core.cartesian.axis.VerticalAxis
|
||||
import com.patrykandpatrick.vico.core.cartesian.data.AxisValueOverrider
|
||||
import com.patrykandpatrick.vico.core.cartesian.data.CartesianValueFormatter
|
||||
import com.patrykandpatrick.vico.core.cartesian.layer.LineCartesianLayer
|
||||
import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarker
|
||||
import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarkerVisibilityListener
|
||||
import com.patrykandpatrick.vico.core.cartesian.marker.LineCartesianLayerMarkerTarget
|
||||
import com.patrykandpatrick.vico.core.common.Dimensions
|
||||
import com.patrykandpatrick.vico.core.common.component.LineComponent
|
||||
import com.patrykandpatrick.vico.core.common.shape.Shape
|
||||
import com.tangem.common.ui.charts.layer.TimeItemPlacer
|
||||
import com.tangem.common.ui.charts.layer.rememberMarketChartLayer
|
||||
import com.tangem.common.ui.charts.marker.rememberTangemChartMarker
|
||||
import com.tangem.common.ui.charts.layer.rememberTangemChartMarker
|
||||
import com.tangem.common.ui.charts.preview.MarketChartPreviewDataProvider
|
||||
import com.tangem.common.ui.charts.state.*
|
||||
import com.tangem.core.ui.components.SpacerH16
|
||||
|
|
@ -50,6 +56,7 @@ import com.tangem.core.ui.res.TangemTheme
|
|||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.utils.DateTimeFormatters
|
||||
import com.tangem.core.ui.utils.toTimeFormat
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.launch
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
|
|
@ -70,73 +77,75 @@ private const val GUIDELINES_COUNT = 3
|
|||
fun MarketChart(
|
||||
modifier: Modifier = Modifier,
|
||||
state: MarketChartState = rememberMarketChartState(),
|
||||
splitChartSegmentColor: Color,
|
||||
@FloatRange(from = 0.0, to = 1.0) backgroundSplitChartSegmentColorAlpha: Float,
|
||||
@FloatRange(from = 0.0, to = 1.0) backgroundColorAlpha: Float,
|
||||
noChartContent: @Composable BoxScope.() -> Unit,
|
||||
splitChartSegmentColor: Color = TangemTheme.colors.icon.inactive,
|
||||
@FloatRange(from = 0.0, to = 1.0) backgroundSplitChartSegmentColorAlpha: Float = 0.24f,
|
||||
@FloatRange(from = 0.0, to = 1.0) backgroundColorAlpha: Float = 0.24f,
|
||||
) {
|
||||
var canvasWidth by remember { mutableIntStateOf(0) }
|
||||
var canvasHeight by remember { mutableIntStateOf(0) }
|
||||
var chartHeight by remember { mutableIntStateOf(0) }
|
||||
|
||||
val layer = rememberLayerFromState(
|
||||
state = state,
|
||||
splitChartSegmentColor = splitChartSegmentColor,
|
||||
backgroundColorAlpha = backgroundColorAlpha,
|
||||
backgroundSplitChartSegmentColorAlpha = backgroundSplitChartSegmentColorAlpha,
|
||||
canvasHeight = canvasHeight,
|
||||
val layer = rememberMarketChartLayer(
|
||||
lineColor = state.chartColor,
|
||||
backgroundLineColor = state.chartColor.copy(alpha = backgroundColorAlpha),
|
||||
secondLineColor = splitChartSegmentColor,
|
||||
backgroundSecondLineColor = splitChartSegmentColor.copy(alpha = backgroundSplitChartSegmentColorAlpha),
|
||||
secondColorOnTheRightSide = state.markerHighlightRightSide.not(),
|
||||
markerFraction = state.markerFraction,
|
||||
axisValueOverrider = AxisValueOverrider.fixed(),
|
||||
canvasHeight = chartHeight,
|
||||
)
|
||||
|
||||
val marker = rememberTangemChartMarker(color = state.chartColor)
|
||||
|
||||
val chart = rememberCartesianChart(
|
||||
layer,
|
||||
startAxis = rememberMarketChartStartAxis(
|
||||
yValueFormatter = state.yValueFormatter,
|
||||
),
|
||||
bottomAxis = rememberMarketChartBottomAxis(
|
||||
xValueFormatter = state.xValueFormatter,
|
||||
),
|
||||
startAxis = rememberMarketChartStartAxis(state.yValueFormatter),
|
||||
bottomAxis = rememberMarketChartBottomAxis(state.xValueFormatter),
|
||||
horizontalLayout = HorizontalLayout.FullWidth(),
|
||||
markerVisibilityListener = rememberMarketVisibilityListener(canvasWidth, state),
|
||||
marker = marker,
|
||||
)
|
||||
val marker = rememberTangemChartMarker(
|
||||
color = state.chartColor,
|
||||
innerCircleColor = Color.White,
|
||||
)
|
||||
val density = LocalDensity.current
|
||||
|
||||
// we need to calculate what the overall height should be in order to get the correct height of the graph
|
||||
val bottomAxisHeight = with(LocalDensity.current) {
|
||||
TangemTheme.typography.caption2.fontSize.toPx().toInt() + TangemTheme.dimens.spacing26.toPx().toInt()
|
||||
}
|
||||
|
||||
CartesianChartHost(
|
||||
modifier = modifier.onGloballyPositioned {
|
||||
with(density) {
|
||||
modifier = modifier
|
||||
.onGloballyPositioned {
|
||||
canvasWidth = it.size.width
|
||||
canvasHeight = if (it.size.height != 0) {
|
||||
// FIXME get height bounded to min max chart points
|
||||
it.size.height - 20.dp.toPx().toInt() - 27.dp.toPx().toInt()
|
||||
chartHeight = if (it.size.height != 0) {
|
||||
it.size.height - bottomAxisHeight
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
chart = chart,
|
||||
modelProducer = state.modelProducer,
|
||||
scrollState = rememberVicoScrollState(scrollEnabled = false),
|
||||
zoomState = rememberVicoZoomState(initialZoom = Zoom.Content, zoomEnabled = false),
|
||||
horizontalLayout = HorizontalLayout.fullWidth(),
|
||||
markerVisibilityListener = state.rememberMarketVisibilityListener(canvasWidth = canvasWidth),
|
||||
diffAnimationSpec = null,
|
||||
marker = marker,
|
||||
placeholder = noChartContent,
|
||||
animationSpec = null,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MarketChartState.rememberMarketVisibilityListener(canvasWidth: Int): CartesianMarkerVisibilityListener {
|
||||
val state = this
|
||||
return remember(state.markerVisibilityListener, canvasWidth) {
|
||||
private fun rememberMarketVisibilityListener(
|
||||
canvasWidth: Int,
|
||||
state: MarketChartState,
|
||||
): CartesianMarkerVisibilityListener {
|
||||
val haptic = LocalHapticFeedback.current
|
||||
return remember(state, canvasWidth) {
|
||||
val maxCanvasXFloat = canvasWidth.toFloat().takeIf { it != 0f }
|
||||
|
||||
object : CartesianMarkerVisibilityListener {
|
||||
override fun onShown(marker: CartesianMarker, targets: List<CartesianMarker.Target>) {
|
||||
state.stopDrawingAnimation()
|
||||
val xCanvas = (targets[0] as LineCartesianLayerMarkerTarget).canvasX
|
||||
|
||||
state.markerFraction = maxCanvasXFloat?.let { xCanvas / it }
|
||||
state.markerVisibilityListener.onShown(marker, targets)
|
||||
|
||||
haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove)
|
||||
}
|
||||
|
||||
override fun onHidden(marker: CartesianMarker) {
|
||||
|
|
@ -149,38 +158,29 @@ private fun MarketChartState.rememberMarketVisibilityListener(canvasWidth: Int):
|
|||
|
||||
state.markerFraction = maxCanvasXFloat?.let { xCanvas / it }
|
||||
state.markerVisibilityListener.onUpdated(marker, targets)
|
||||
haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun rememberLayerFromState(
|
||||
state: MarketChartState,
|
||||
splitChartSegmentColor: Color,
|
||||
@FloatRange(from = 0.0, to = 1.0) backgroundColorAlpha: Float,
|
||||
@FloatRange(from = 0.0, to = 1.0) backgroundSplitChartSegmentColorAlpha: Float,
|
||||
canvasHeight: Int,
|
||||
): LineCartesianLayer {
|
||||
return rememberMarketChartLayer(
|
||||
lineColor = state.chartColor,
|
||||
backgroundLineColor = state.chartColor.copy(alpha = backgroundColorAlpha),
|
||||
secondLineColor = splitChartSegmentColor,
|
||||
backgroundSecondLineColor = splitChartSegmentColor.copy(alpha = backgroundSplitChartSegmentColorAlpha),
|
||||
secondColorOnTheRightSide = state.markerHighlightRightSide.not(),
|
||||
startDrawingAnimation = state.startDrawingAnimationState,
|
||||
markerFraction = state.markerFraction,
|
||||
axisValueOverrider = AxisValueOverrider.adaptiveYValues(yFraction = 1.2f, round = true), // FIXME ?
|
||||
canvasHeight = canvasHeight,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun rememberMarketChartStartAxis(
|
||||
yValueFormatter: CartesianValueFormatter,
|
||||
): VerticalAxis<AxisPosition.Vertical.Start> {
|
||||
val textStyle = TangemTheme.typography.caption2
|
||||
val resolver = LocalFontFamilyResolver.current
|
||||
val typeface by remember(resolver, textStyle) {
|
||||
resolver.resolveAsTypeface(
|
||||
fontFamily = textStyle.fontFamily,
|
||||
fontWeight = textStyle.fontWeight ?: FontWeight.Normal,
|
||||
fontStyle = textStyle.fontStyle ?: FontStyle.Normal,
|
||||
fontSynthesis = textStyle.fontSynthesis ?: FontSynthesis.All,
|
||||
)
|
||||
}
|
||||
|
||||
return rememberCustomStartAxis(
|
||||
axis = null,
|
||||
line = null,
|
||||
tick = null,
|
||||
guideline = null,
|
||||
labelGuideline = rememberChartAxisGuidelineComponent(
|
||||
|
|
@ -194,38 +194,44 @@ private fun rememberMarketChartStartAxis(
|
|||
end = TangemTheme.dimens.spacing4,
|
||||
),
|
||||
textSize = TangemTheme.typography.caption2.fontSize,
|
||||
typeface = TangemTheme.typography.caption2.toGraphicsTypeFace(),
|
||||
typeface = typeface,
|
||||
),
|
||||
horizontalLabelPosition = VerticalAxis.HorizontalLabelPosition.Inside,
|
||||
verticalLabelPosition = VerticalAxis.VerticalLabelPosition.Center,
|
||||
itemPlacer = AxisItemPlacer.Vertical.count({ GUIDELINES_COUNT }, false),
|
||||
itemPlacer = VerticalAxis.ItemPlacer.count({ GUIDELINES_COUNT }, false),
|
||||
valueFormatter = yValueFormatter,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberMarketChartBottomAxis(
|
||||
private fun rememberMarketChartBottomAxis(
|
||||
xValueFormatter: CartesianValueFormatter,
|
||||
): HorizontalAxis<AxisPosition.Horizontal.Bottom> {
|
||||
val textStyle = TangemTheme.typography.caption2
|
||||
|
||||
val resolver = LocalFontFamilyResolver.current
|
||||
|
||||
val typeface by remember(resolver, textStyle) {
|
||||
resolver.resolveAsTypeface(
|
||||
fontFamily = textStyle.fontFamily,
|
||||
fontWeight = textStyle.fontWeight ?: FontWeight.Normal,
|
||||
fontStyle = textStyle.fontStyle ?: FontStyle.Normal,
|
||||
fontSynthesis = textStyle.fontSynthesis ?: FontSynthesis.All,
|
||||
)
|
||||
}
|
||||
|
||||
return rememberBottomAxis(
|
||||
label = rememberAxisLabelComponent(
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
textSize = TangemTheme.typography.caption2.fontSize,
|
||||
padding = Dimensions.of(top = TangemTheme.dimens.spacing20),
|
||||
typeface = TangemTheme.typography.caption2.toGraphicsTypeFace(),
|
||||
padding = Dimensions.of(top = TangemTheme.dimens.spacing26),
|
||||
typeface = typeface,
|
||||
),
|
||||
tick = null,
|
||||
axis = null,
|
||||
line = null,
|
||||
guideline = null,
|
||||
sizeConstraint = BaseAxis.SizeConstraint.Exact(sizeDp = 37f), // FIXME ?
|
||||
itemPlacer = remember {
|
||||
AxisItemPlacer.Horizontal.default(
|
||||
spacing = 25, // FIXME ?
|
||||
offset = 60, // FIXME ?
|
||||
shiftExtremeTicks = false,
|
||||
addExtremeLabelPadding = false,
|
||||
)
|
||||
},
|
||||
sizeConstraint = BaseAxis.SizeConstraint.Auto(),
|
||||
itemPlacer = remember { TimeItemPlacer() },
|
||||
valueFormatter = xValueFormatter,
|
||||
)
|
||||
}
|
||||
|
|
@ -245,19 +251,6 @@ private fun rememberChartAxisGuidelineComponent(color: Color): LineComponent {
|
|||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun TextStyle.toGraphicsTypeFace(): android.graphics.Typeface {
|
||||
val resolver = LocalFontFamilyResolver.current
|
||||
return remember(resolver, this) {
|
||||
resolver.resolveAsTypeface(
|
||||
fontFamily = this.fontFamily,
|
||||
fontWeight = this.fontWeight ?: FontWeight.Normal,
|
||||
fontStyle = this.fontStyle ?: FontStyle.Normal,
|
||||
fontSynthesis = this.fontSynthesis ?: FontSynthesis.All,
|
||||
)
|
||||
}.value
|
||||
}
|
||||
|
||||
// region Preview
|
||||
|
||||
@Suppress("LongMethod")
|
||||
|
|
@ -275,7 +268,6 @@ private fun MarketChartPreview(
|
|||
chartLook = MarketChartLook(
|
||||
type = MarketChartLook.Type.Growing,
|
||||
markerHighlightRightSide = true,
|
||||
animationOnDataChange = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -283,8 +275,8 @@ private fun MarketChartPreview(
|
|||
LaunchedEffect(key1 = Unit) {
|
||||
dataProducer.runTransactionSuspend {
|
||||
chartData = MarketChartData.Data(
|
||||
x = x,
|
||||
y = y,
|
||||
x = x.toImmutableList(),
|
||||
y = y.toImmutableList(),
|
||||
)
|
||||
updateLook {
|
||||
it.copy(
|
||||
|
|
@ -338,13 +330,9 @@ private fun MarketChartPreview(
|
|||
splitChartSegmentColor = TangemTheme.colors.icon.inactive,
|
||||
backgroundSplitChartSegmentColorAlpha = 0.24f,
|
||||
backgroundColorAlpha = 0.24f,
|
||||
noChartContent = { },
|
||||
)
|
||||
SpacerH16()
|
||||
|
||||
Button(onClick = { chartState.startDrawingAnimation() }) {
|
||||
Text("Start drawing animation")
|
||||
}
|
||||
Button(
|
||||
onClick = {
|
||||
dataProducer.runTransaction {
|
||||
|
|
@ -358,41 +346,38 @@ private fun MarketChartPreview(
|
|||
text = "Change marker highlight side",
|
||||
)
|
||||
}
|
||||
Button(onClick = {
|
||||
coroutineScope.launch {
|
||||
dataProducer.runTransactionSuspend {
|
||||
updateData {
|
||||
MarketChartData.Data(
|
||||
x = it.x,
|
||||
y = it.y.reversed(),
|
||||
Button(
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
dataProducer.runTransactionSuspend {
|
||||
updateData {
|
||||
MarketChartData.Data(
|
||||
x = it.x,
|
||||
y = it.y.reversed().toImmutableList(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
) {
|
||||
Text("Change Data")
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
dataProducer.runTransaction {
|
||||
updateLook {
|
||||
it.copy(
|
||||
type = if (it.type == MarketChartLook.Type.Growing) {
|
||||
MarketChartLook.Type.Falling
|
||||
} else {
|
||||
MarketChartLook.Type.Growing
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},) {
|
||||
Text("Change Data")
|
||||
}
|
||||
Button(onClick = {
|
||||
dataProducer.runTransaction {
|
||||
updateLook { it.copy(animationOnDataChange = it.animationOnDataChange.not()) }
|
||||
}
|
||||
},) {
|
||||
Text("Change animationOnDataChange = ${look.animationOnDataChange}")
|
||||
}
|
||||
|
||||
Button(onClick = {
|
||||
dataProducer.runTransaction {
|
||||
updateLook {
|
||||
it.copy(
|
||||
type = if (it.type == MarketChartLook.Type.Growing) {
|
||||
MarketChartLook.Type.Falling
|
||||
} else {
|
||||
MarketChartLook.Type.Growing
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
},) {
|
||||
},
|
||||
) {
|
||||
Text("Change color type")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,19 +12,21 @@ import androidx.compose.ui.graphics.toArgb
|
|||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.patrykandpatrick.vico.compose.cartesian.*
|
||||
import com.patrykandpatrick.vico.compose.cartesian.layer.rememberLine
|
||||
import com.patrykandpatrick.vico.compose.cartesian.layer.rememberLineCartesianLayer
|
||||
import com.patrykandpatrick.vico.compose.cartesian.layer.rememberLineSpec
|
||||
import com.patrykandpatrick.vico.compose.common.shader.BrushShader
|
||||
import com.patrykandpatrick.vico.compose.common.shader.toDynamicShader
|
||||
import com.patrykandpatrick.vico.core.cartesian.HorizontalLayout
|
||||
import com.patrykandpatrick.vico.core.cartesian.Zoom
|
||||
import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModel
|
||||
import com.patrykandpatrick.vico.core.cartesian.data.LineCartesianLayerModel
|
||||
import com.patrykandpatrick.vico.core.cartesian.layer.LineCartesianLayer
|
||||
import com.patrykandpatrick.vico.core.common.shader.ColorShader
|
||||
import com.tangem.common.ui.charts.state.MarketChartLook
|
||||
import com.tangem.common.ui.charts.state.MarketChartRawData
|
||||
import com.tangem.core.ui.components.SpacerH16
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlin.random.Random
|
||||
|
||||
@Composable
|
||||
|
|
@ -44,18 +46,19 @@ fun MarketChartMini(
|
|||
MarketChartLook.Type.Falling -> fallingColor
|
||||
}
|
||||
|
||||
val lineSpec = rememberLineSpec(
|
||||
val lineSpec = rememberLine(
|
||||
shader = ColorShader(lineColor.toArgb()),
|
||||
thickness = 1.dp,
|
||||
backgroundShader = BrushShader(
|
||||
brush = Brush.verticalGradient(
|
||||
colors = listOf(lineColor.copy(alpha = 0.22f), Color.Transparent),
|
||||
),
|
||||
),
|
||||
backgroundShader = Brush.verticalGradient(
|
||||
colors = listOf(lineColor.copy(alpha = 0.22f), Color.Transparent),
|
||||
).toDynamicShader(),
|
||||
)
|
||||
|
||||
val layer = rememberLineCartesianLayer(listOf(lineSpec))
|
||||
val chart = rememberCartesianChart(layer)
|
||||
val layer = rememberLineCartesianLayer(LineCartesianLayer.LineProvider.series(lineSpec))
|
||||
val chart = rememberCartesianChart(
|
||||
layer,
|
||||
horizontalLayout = HorizontalLayout.fullWidth(),
|
||||
)
|
||||
|
||||
CartesianChartHost(
|
||||
modifier = modifier,
|
||||
|
|
@ -63,7 +66,6 @@ fun MarketChartMini(
|
|||
model = model,
|
||||
zoomState = rememberVicoZoomState(initialZoom = Zoom.Content, zoomEnabled = false),
|
||||
scrollState = rememberVicoScrollState(scrollEnabled = false),
|
||||
horizontalLayout = HorizontalLayout.fullWidth(),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -74,8 +76,8 @@ fun MarketChartMini(
|
|||
@Composable
|
||||
private fun Preview() {
|
||||
val data = MarketChartRawData(
|
||||
x = List(20) { Random.nextFloat() },
|
||||
y = List(20) { Random.nextFloat() },
|
||||
x = List(20) { Random.nextFloat().toDouble() }.toImmutableList(),
|
||||
y = List(20) { Random.nextFloat().toDouble() }.toImmutableList(),
|
||||
)
|
||||
|
||||
TangemThemePreview {
|
||||
|
|
@ -92,8 +94,8 @@ private fun Preview() {
|
|||
@Composable
|
||||
private fun PreviewColumn() {
|
||||
val data = MarketChartRawData(
|
||||
x = List(20) { Random.nextFloat() },
|
||||
y = List(20) { Random.nextFloat() },
|
||||
x = List(20) { Random.nextFloat().toDouble() }.toImmutableList(),
|
||||
y = List(20) { Random.nextFloat().toDouble() }.toImmutableList(),
|
||||
)
|
||||
|
||||
TangemThemePreview {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,246 @@
|
|||
package com.tangem.common.ui.charts.downsample
|
||||
|
||||
import kotlin.math.max
|
||||
|
||||
/**
|
||||
* =========================================================
|
||||
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* =========================================================
|
||||
*
|
||||
* Downsamples the given data points to the desired number of buckets (points + 2).
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
object LTThreeBuckets {
|
||||
|
||||
fun downsample(x: List<Double>, y: List<Double>, desiredBuckets: Int): Result {
|
||||
require(x.size == y.size) { "X and Y must have the same size" }
|
||||
require(desiredBuckets > 0) { "Desired buckets must be greater than 0" }
|
||||
|
||||
val points = x.zip(y).mapIndexed { index, (x, y) -> Point(index, x, y) }
|
||||
val results = mutableListOf<Point>()
|
||||
|
||||
points.onPassBucketize(desiredBuckets)
|
||||
.sliding(size = 3, step = 1)
|
||||
.map { buckets -> Triangle.of(buckets) }
|
||||
.fastForEach { triangle ->
|
||||
if (results.isEmpty()) {
|
||||
results.add(triangle.getFirst())
|
||||
}
|
||||
|
||||
results.add(triangle.getResult())
|
||||
|
||||
if (results.size == desiredBuckets + 1) {
|
||||
results.add(triangle.getLast())
|
||||
}
|
||||
}
|
||||
|
||||
val xRes = ArrayList<Double>(points.size)
|
||||
val yRes = ArrayList<Double>(points.size)
|
||||
val indexesRes = ArrayList<Int>(points.size)
|
||||
|
||||
results.fastForEach {
|
||||
xRes.add(it.x)
|
||||
yRes.add(it.y)
|
||||
indexesRes.add(it.originalIndex!!)
|
||||
}
|
||||
|
||||
return Result(
|
||||
originalIndexes = indexesRes,
|
||||
x = xRes,
|
||||
y = yRes,
|
||||
)
|
||||
}
|
||||
|
||||
data class Result(
|
||||
val originalIndexes: List<Int>,
|
||||
val x: List<Double>,
|
||||
val y: List<Double>,
|
||||
)
|
||||
}
|
||||
|
||||
private fun List<Point>.onPassBucketize(desiredBucketsCount: Int): List<Bucket> {
|
||||
val middleSize = size - 2
|
||||
val bucketSize = middleSize / desiredBucketsCount
|
||||
val remainingElements = middleSize % desiredBucketsCount
|
||||
|
||||
require(bucketSize != 0) {
|
||||
"Can't produce $desiredBucketsCount buckets from an input series of ${middleSize + 2} elements"
|
||||
}
|
||||
|
||||
val buckets = mutableListOf<Bucket>()
|
||||
|
||||
// Add first point as the only point in the first bucket
|
||||
buckets.add(Bucket.of(this[0]))
|
||||
|
||||
var rest = this.subList(1, this.lastIndex)
|
||||
|
||||
// Add middle buckets.
|
||||
// When inputSize is not a multiple of desiredBuckets,
|
||||
// remaining elements are equally distributed on the first buckets.
|
||||
while (buckets.size < desiredBucketsCount + 1) {
|
||||
val size = if (buckets.size <= remainingElements) bucketSize + 1 else bucketSize
|
||||
buckets.add(Bucket.of(rest.subList(0, size)))
|
||||
rest = rest.subList(size, rest.size)
|
||||
}
|
||||
|
||||
// Add last point as the only point in the last bucket
|
||||
buckets.add(Bucket.of(this.last()))
|
||||
|
||||
return buckets
|
||||
}
|
||||
|
||||
private fun List<Bucket>.sliding(size: Int, step: Int): List<List<Bucket>> {
|
||||
val window = max(size, step)
|
||||
val buffer = ArrayDeque<Bucket>()
|
||||
var totalIn = 0
|
||||
|
||||
val lists = mutableListOf<List<Bucket>>()
|
||||
|
||||
fastForEach { p ->
|
||||
buffer.add(p)
|
||||
++totalIn
|
||||
if (buffer.size == window) {
|
||||
val batch = buffer.take(size)
|
||||
lists.add(batch)
|
||||
|
||||
repeat(step) {
|
||||
buffer.removeFirst()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (buffer.isNotEmpty()) {
|
||||
val totalOut = max(0, (totalIn + step - size - 1) / step) + 1
|
||||
if (totalOut > lists.size) {
|
||||
val batch = buffer.take(size)
|
||||
lists.add(batch)
|
||||
}
|
||||
}
|
||||
|
||||
return lists
|
||||
}
|
||||
|
||||
private data class Point(
|
||||
val originalIndex: Int? = null,
|
||||
val x: Double,
|
||||
val y: Double,
|
||||
)
|
||||
|
||||
private data class Bucket(
|
||||
val data: List<Point>,
|
||||
val center: Point,
|
||||
val result: Point,
|
||||
val first: Point,
|
||||
val last: Point,
|
||||
) {
|
||||
companion object {
|
||||
private fun centerBetweenPoints(a: Point, b: Point): Point {
|
||||
val vector = Point(
|
||||
x = b.x - a.x,
|
||||
y = b.y - a.y,
|
||||
)
|
||||
val halfVector = Point(
|
||||
x = vector.x / 2,
|
||||
y = vector.y / 2,
|
||||
)
|
||||
|
||||
return Point(
|
||||
x = a.x + halfVector.x,
|
||||
y = a.y + halfVector.y,
|
||||
)
|
||||
}
|
||||
|
||||
fun of(points: List<Point>): Bucket {
|
||||
val first = points.first()
|
||||
val last = points.last()
|
||||
|
||||
return Bucket(
|
||||
data = points,
|
||||
center = centerBetweenPoints(first, last),
|
||||
result = first,
|
||||
first = first,
|
||||
last = last,
|
||||
)
|
||||
}
|
||||
|
||||
fun of(point: Point): Bucket {
|
||||
return Bucket(
|
||||
data = listOf(point),
|
||||
center = point,
|
||||
result = point,
|
||||
first = point,
|
||||
last = point,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class Triangle(
|
||||
val left: Bucket,
|
||||
val center: Bucket,
|
||||
val right: Bucket,
|
||||
) {
|
||||
fun getResult(): Point {
|
||||
return center.data.map { Area.ofTriangle(left.result, it, right.center) }
|
||||
.maxByOrNull { it.value }
|
||||
?.generator
|
||||
?: error("Can't obtain max area triangle")
|
||||
}
|
||||
|
||||
fun getFirst(): Point {
|
||||
return left.first
|
||||
}
|
||||
|
||||
fun getLast(): Point {
|
||||
return right.last
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun of(buckets: List<Bucket>): Triangle {
|
||||
return Triangle(
|
||||
left = buckets[0],
|
||||
center = buckets[1],
|
||||
right = buckets[2],
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class Area(
|
||||
val generator: Point,
|
||||
val value: Double,
|
||||
) {
|
||||
companion object {
|
||||
fun ofTriangle(a: Point, b: Point, c: Point): Area {
|
||||
val addends = listOf(
|
||||
a.x * (b.y - c.y),
|
||||
b.x * (c.y - a.y),
|
||||
c.x * (a.y - b.y),
|
||||
)
|
||||
val sum = addends.sum()
|
||||
val value = kotlin.math.abs(sum / 2)
|
||||
|
||||
return Area(b, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <T> List<T>.fastForEach(action: (T) -> Unit) {
|
||||
for (index in indices) {
|
||||
val item = get(index)
|
||||
action(item)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
package com.tangem.common.ui.charts.layer
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.patrykandpatrick.vico.compose.common.component.rememberUnboundedLineComponent
|
||||
import com.patrykandpatrick.vico.compose.common.component.shapeComponent
|
||||
import com.patrykandpatrick.vico.compose.common.of
|
||||
import com.patrykandpatrick.vico.compose.common.shape.dashed
|
||||
import com.patrykandpatrick.vico.core.cartesian.*
|
||||
import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModel
|
||||
import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarker
|
||||
import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarkerValueFormatter
|
||||
import com.patrykandpatrick.vico.core.cartesian.marker.DefaultCartesianMarker
|
||||
import com.patrykandpatrick.vico.core.common.Dimensions
|
||||
import com.patrykandpatrick.vico.core.common.LayeredComponent
|
||||
import com.patrykandpatrick.vico.core.common.component.Component
|
||||
import com.patrykandpatrick.vico.core.common.component.TextComponent
|
||||
import com.patrykandpatrick.vico.core.common.shape.Shape
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
/**
|
||||
* @param color The color of the indicator and guideline.
|
||||
* @param innerCircleColor The color of the inner circle of the indicator.
|
||||
*
|
||||
* @return A [CartesianMarker] that consists of a dashed guideline and a layered indicator with a shadow effect.
|
||||
*/
|
||||
@Composable
|
||||
internal fun rememberTangemChartMarker(color: Color): CartesianMarker {
|
||||
val guideline = rememberUnboundedLineComponent(
|
||||
color = color,
|
||||
verticalAddDrawSpace = TangemTheme.dimens.spacing24,
|
||||
shape = remember { Shape.dashed(Shape.Rectangle, 4.dp, 4.dp) },
|
||||
)
|
||||
|
||||
return remember(guideline) {
|
||||
val outColor = guideline.color
|
||||
|
||||
object : DefaultCartesianMarker(
|
||||
label = TextComponent(textSizeSp = 0f),
|
||||
indicator = ::indicator,
|
||||
indicatorSizeDp = INDICATOR_SIZE_DP,
|
||||
guideline = guideline,
|
||||
valueFormatter = object : CartesianMarkerValueFormatter {
|
||||
override fun format(
|
||||
context: CartesianDrawContext,
|
||||
targets: List<CartesianMarker.Target>,
|
||||
): CharSequence = ""
|
||||
},
|
||||
) {
|
||||
override fun updateInsets(
|
||||
context: CartesianMeasureContext,
|
||||
horizontalDimensions: HorizontalDimensions,
|
||||
model: CartesianChartModel,
|
||||
insets: Insets,
|
||||
) {
|
||||
with(context) {
|
||||
super.updateInsets(context, horizontalDimensions, model, insets)
|
||||
val baseShadowInsetDp =
|
||||
CLIPPING_FREE_SHADOW_RADIUS_MULTIPLIER * LABEL_BACKGROUND_SHADOW_RADIUS_DP
|
||||
val topInset = (baseShadowInsetDp - LABEL_BACKGROUND_SHADOW_DY_DP).pixels
|
||||
val bottomInset = (baseShadowInsetDp + LABEL_BACKGROUND_SHADOW_DY_DP).pixels
|
||||
insets.ensureValuesAtLeast(top = topInset, bottom = bottomInset)
|
||||
}
|
||||
}
|
||||
|
||||
override fun CartesianDrawContext.drawIndicator(x: Float, y: Float, color: Int, halfIndicatorSize: Float) {
|
||||
val indicator = indicator ?: return
|
||||
cacheStore
|
||||
.getOrSet(keyNamespace, indicator, outColor) { indicator.invoke(outColor) }
|
||||
.draw(
|
||||
this,
|
||||
x - halfIndicatorSize,
|
||||
y - halfIndicatorSize,
|
||||
x + halfIndicatorSize,
|
||||
y + halfIndicatorSize,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun indicator(color: Int): Component {
|
||||
val composeColor = Color(color)
|
||||
|
||||
return LayeredComponent(
|
||||
rear = shapeComponent(
|
||||
color = composeColor.copy(alpha = INDICATOR_REAR_COLOR_ALPHA),
|
||||
shape = Shape.Pill,
|
||||
),
|
||||
front = LayeredComponent(
|
||||
rear = shapeComponent(
|
||||
color = composeColor,
|
||||
shape = Shape.Pill,
|
||||
),
|
||||
front = shapeComponent(
|
||||
color = Color.White,
|
||||
shape = Shape.Pill,
|
||||
),
|
||||
padding = indicatorPadding,
|
||||
),
|
||||
padding = indicatorPadding,
|
||||
)
|
||||
}
|
||||
|
||||
private val indicatorPadding = Dimensions.of(3.dp)
|
||||
private const val LABEL_BACKGROUND_SHADOW_RADIUS_DP = 4f
|
||||
private const val LABEL_BACKGROUND_SHADOW_DY_DP = 2f
|
||||
private const val CLIPPING_FREE_SHADOW_RADIUS_MULTIPLIER = 1.4f
|
||||
private const val INDICATOR_SIZE_DP = 16f
|
||||
private const val INDICATOR_REAR_COLOR_ALPHA = .24f
|
||||
|
|
@ -2,9 +2,6 @@ package com.tangem.common.ui.charts.layer
|
|||
|
||||
import android.content.res.Configuration
|
||||
import androidx.annotation.FloatRange
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.animate
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
|
@ -20,22 +17,22 @@ import androidx.compose.ui.unit.dp
|
|||
import com.patrykandpatrick.vico.compose.cartesian.CartesianChartHost
|
||||
import com.patrykandpatrick.vico.compose.cartesian.fullWidth
|
||||
import com.patrykandpatrick.vico.compose.cartesian.layer.rememberLineCartesianLayer
|
||||
import com.patrykandpatrick.vico.compose.cartesian.layer.rememberLineSpec
|
||||
import com.patrykandpatrick.vico.compose.cartesian.layer.rememberSplitLineSpec
|
||||
import com.patrykandpatrick.vico.compose.cartesian.layer.rememberSplitLine
|
||||
import com.patrykandpatrick.vico.compose.cartesian.rememberCartesianChart
|
||||
import com.patrykandpatrick.vico.compose.cartesian.rememberVicoZoomState
|
||||
import com.patrykandpatrick.vico.compose.common.shader.BrushShader
|
||||
import com.patrykandpatrick.vico.compose.common.shader.toDynamicShader
|
||||
import com.patrykandpatrick.vico.core.cartesian.HorizontalLayout
|
||||
import com.patrykandpatrick.vico.core.cartesian.Zoom
|
||||
import com.patrykandpatrick.vico.core.cartesian.data.AxisValueOverrider
|
||||
import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModel
|
||||
import com.patrykandpatrick.vico.core.cartesian.data.LineCartesianLayerModel
|
||||
import com.patrykandpatrick.vico.core.cartesian.layer.LineCartesianLayer
|
||||
import com.patrykandpatrick.vico.core.common.shader.ColorShader
|
||||
import com.patrykandpatrick.vico.core.common.shader.DynamicShader
|
||||
import com.tangem.common.ui.charts.preview.MarketChartPreviewDataProvider
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
|
|
@ -59,117 +56,50 @@ internal fun rememberMarketChartLayer(
|
|||
backgroundLineColor: Color,
|
||||
secondLineColor: Color,
|
||||
backgroundSecondLineColor: Color,
|
||||
startDrawingAnimation: MutableState<Boolean>,
|
||||
axisValueOverrider: AxisValueOverrider,
|
||||
secondColorOnTheRightSide: Boolean,
|
||||
@FloatRange(from = 0.0, to = 1.0) markerFraction: Float?,
|
||||
canvasHeight: Int,
|
||||
): LineCartesianLayer {
|
||||
var animationFraction: Float? by remember { mutableStateOf(null) }
|
||||
val backgroundColorLineGradient = persistentListOf(backgroundLineColor, Color.Transparent)
|
||||
val backgroundSecondLineColorGradient = persistentListOf(backgroundSecondLineColor, Color.Transparent)
|
||||
|
||||
LaunchedEffect(startDrawingAnimation.value) {
|
||||
animationFraction = null
|
||||
if (startDrawingAnimation.value) {
|
||||
animate(
|
||||
initialValue = 0f,
|
||||
targetValue = 1f,
|
||||
animationSpec = tween(easing = LinearEasing, durationMillis = 1000),
|
||||
) { start, _ ->
|
||||
if (start == 1f) {
|
||||
animationFraction = null
|
||||
startDrawingAnimation.value = false
|
||||
} else {
|
||||
animationFraction = start
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val markerSet = markerFraction != null
|
||||
|
||||
return rememberRawMarketChartLayer(
|
||||
lineColor = lineColor,
|
||||
backgroundLineColor = backgroundLineColor,
|
||||
secondLineColor = secondLineColor,
|
||||
backgroundSecondLineColor = backgroundSecondLineColor,
|
||||
return rememberLayer(
|
||||
fractionValue = markerFraction ?: 0f,
|
||||
axisValueOverrider = axisValueOverrider,
|
||||
secondColorOnTheRightSide = secondColorOnTheRightSide,
|
||||
markerFraction = markerFraction,
|
||||
animationFraction = animationFraction,
|
||||
canvasHeight = canvasHeight,
|
||||
lineColor = if (markerFraction != null) {
|
||||
secondLineColor
|
||||
} else {
|
||||
lineColor
|
||||
},
|
||||
backLineColor = if (markerSet && !secondColorOnTheRightSide) {
|
||||
backgroundSecondLineColorGradient
|
||||
} else {
|
||||
backgroundColorLineGradient
|
||||
},
|
||||
lineColorRight = when {
|
||||
markerSet && secondColorOnTheRightSide -> secondLineColor
|
||||
else -> lineColor
|
||||
},
|
||||
backLineColorRight = when {
|
||||
markerSet && secondColorOnTheRightSide -> backgroundSecondLineColorGradient
|
||||
else -> backgroundColorLineGradient
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
private fun rememberRawMarketChartLayer(
|
||||
lineColor: Color,
|
||||
backgroundLineColor: Color,
|
||||
secondLineColor: Color,
|
||||
backgroundSecondLineColor: Color,
|
||||
axisValueOverrider: AxisValueOverrider,
|
||||
canvasHeight: Int,
|
||||
secondColorOnTheRightSide: Boolean = false,
|
||||
@FloatRange(from = 0.0, to = 1.0) markerFraction: Float? = null,
|
||||
@FloatRange(from = 0.0, to = 1.0) animationFraction: Float? = null,
|
||||
): LineCartesianLayer {
|
||||
val backgroundColorLineGradient = listOf(backgroundLineColor, Color.Transparent)
|
||||
val backgroundSecondLineColorGradient = listOf(backgroundSecondLineColor, Color.Transparent)
|
||||
|
||||
val markerSet = markerFraction != null
|
||||
val animationRunning = animationFraction != null && animationFraction != 1f
|
||||
|
||||
val layerColors = when {
|
||||
!animationRunning && markerSet && secondColorOnTheRightSide -> {
|
||||
LayerColors(
|
||||
lineColor = lineColor,
|
||||
backLineColor = backgroundColorLineGradient,
|
||||
lineColorRight = secondLineColor,
|
||||
backLineColorRight = backgroundSecondLineColorGradient,
|
||||
)
|
||||
}
|
||||
!animationRunning && markerSet && !secondColorOnTheRightSide -> {
|
||||
LayerColors(
|
||||
lineColor = secondLineColor,
|
||||
backLineColor = backgroundSecondLineColorGradient,
|
||||
lineColorRight = lineColor,
|
||||
backLineColorRight = backgroundColorLineGradient,
|
||||
)
|
||||
}
|
||||
animationRunning -> {
|
||||
LayerColors(
|
||||
lineColor = lineColor,
|
||||
backLineColor = backgroundColorLineGradient,
|
||||
lineColorRight = Color.Transparent,
|
||||
backLineColorRight = listOf(Color.Transparent, Color.Transparent),
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
LayerColors(
|
||||
lineColor = lineColor,
|
||||
backLineColor = backgroundColorLineGradient,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return rememberLayer(
|
||||
fractionValue = animationFraction ?: markerFraction,
|
||||
axisValueOverrider = axisValueOverrider,
|
||||
layerColors = layerColors,
|
||||
canvasHeight = canvasHeight,
|
||||
)
|
||||
}
|
||||
|
||||
private data class LayerColors(
|
||||
val lineColor: Color,
|
||||
val backLineColor: List<Color>,
|
||||
val lineColorRight: Color? = null,
|
||||
val backLineColorRight: List<Color>? = null,
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun rememberLayer(
|
||||
fractionValue: Float?,
|
||||
fractionValue: Float,
|
||||
axisValueOverrider: AxisValueOverrider,
|
||||
layerColors: LayerColors,
|
||||
lineColor: Color,
|
||||
backLineColor: ImmutableList<Color>,
|
||||
lineColorRight: Color,
|
||||
backLineColorRight: ImmutableList<Color>,
|
||||
canvasHeight: Int,
|
||||
): LineCartesianLayer {
|
||||
val endGradientColorPosition = if (canvasHeight != 0) {
|
||||
|
|
@ -178,47 +108,27 @@ private fun rememberLayer(
|
|||
Float.POSITIVE_INFINITY
|
||||
}
|
||||
|
||||
val alineColor = remember(lineColor) { lineColor.toArgb() }
|
||||
val alineColorRight = remember(lineColorRight) { lineColorRight.toArgb() }
|
||||
|
||||
return rememberLineCartesianLayer(
|
||||
listOf(
|
||||
if (layerColors.lineColorRight == null || layerColors.backLineColorRight == null || fractionValue == null) {
|
||||
rememberLineSpec(
|
||||
shader = remember(layerColors.lineColor) { ColorShader(color = layerColors.lineColor.toArgb()) },
|
||||
backgroundShader = remember(layerColors.backLineColor, endGradientColorPosition) {
|
||||
BrushShader(
|
||||
brush = Brush.verticalGradient(
|
||||
colors = layerColors.backLineColor,
|
||||
endY = endGradientColorPosition,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
rememberSplitLineSpec(
|
||||
shader = remember(layerColors.lineColor, layerColors.lineColorRight, fractionValue) {
|
||||
DynamicShader.Companion.horizontalGradient(
|
||||
colors = intArrayOf(layerColors.lineColor.toArgb(), layerColors.lineColorRight.toArgb()),
|
||||
positions = floatArrayOf(fractionValue, fractionValue),
|
||||
)
|
||||
},
|
||||
backgroundShaderFirst = remember(layerColors.backLineColor, endGradientColorPosition) {
|
||||
BrushShader(
|
||||
brush = Brush.verticalGradient(
|
||||
colors = layerColors.backLineColor,
|
||||
endY = endGradientColorPosition,
|
||||
),
|
||||
)
|
||||
},
|
||||
backgroundShaderSecond = remember(layerColors.backLineColorRight, endGradientColorPosition) {
|
||||
BrushShader(
|
||||
brush = Brush.verticalGradient(
|
||||
colors = layerColors.backLineColorRight,
|
||||
endY = endGradientColorPosition,
|
||||
),
|
||||
)
|
||||
},
|
||||
xSplitFraction = fractionValue,
|
||||
)
|
||||
},
|
||||
LineCartesianLayer.LineProvider.series(
|
||||
rememberSplitLine(
|
||||
shader = DynamicShader.Companion.horizontalGradient(
|
||||
colors = intArrayOf(alineColor, alineColorRight),
|
||||
positions = floatArrayOf(fractionValue, fractionValue),
|
||||
),
|
||||
backgroundShaderFirst = Brush.verticalGradient(
|
||||
colors = backLineColor,
|
||||
endY = endGradientColorPosition,
|
||||
).toDynamicShader(),
|
||||
backgroundShaderSecond = Brush.verticalGradient(
|
||||
colors = backLineColorRight,
|
||||
endY = endGradientColorPosition,
|
||||
).toDynamicShader(),
|
||||
xSplitFraction = fractionValue,
|
||||
thickness = 1.dp,
|
||||
),
|
||||
),
|
||||
axisValueOverrider = axisValueOverrider,
|
||||
)
|
||||
|
|
@ -250,25 +160,26 @@ private fun LayerChartPreview(
|
|||
CartesianChartHost(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
chart = rememberCartesianChart(
|
||||
rememberRawMarketChartLayer(
|
||||
rememberMarketChartLayer(
|
||||
lineColor = lineColor,
|
||||
backgroundLineColor = lineColor.copy(alpha = 0.24f),
|
||||
secondLineColor = Color.Gray,
|
||||
backgroundSecondLineColor = Color.Gray.copy(alpha = 0.24f),
|
||||
secondColorOnTheRightSide = true,
|
||||
axisValueOverrider = AxisValueOverrider.fixed(minY = model.models[0].minY),
|
||||
markerFraction = 0.35f,
|
||||
canvasHeight = 495,
|
||||
),
|
||||
horizontalLayout = HorizontalLayout.fullWidth(),
|
||||
),
|
||||
zoomState = rememberVicoZoomState(initialZoom = Zoom.Content, zoomEnabled = false),
|
||||
horizontalLayout = HorizontalLayout.fullWidth(),
|
||||
model = model,
|
||||
)
|
||||
|
||||
CartesianChartHost(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
chart = rememberCartesianChart(
|
||||
rememberRawMarketChartLayer(
|
||||
rememberMarketChartLayer(
|
||||
lineColor = lineColor,
|
||||
backgroundLineColor = lineColor.copy(alpha = 0.24f),
|
||||
secondLineColor = Color.Gray,
|
||||
|
|
@ -278,16 +189,16 @@ private fun LayerChartPreview(
|
|||
axisValueOverrider = AxisValueOverrider.fixed(minY = model.models[0].minY),
|
||||
canvasHeight = 495,
|
||||
),
|
||||
horizontalLayout = HorizontalLayout.fullWidth(),
|
||||
),
|
||||
zoomState = rememberVicoZoomState(initialZoom = Zoom.Content, zoomEnabled = false),
|
||||
horizontalLayout = HorizontalLayout.fullWidth(),
|
||||
model = model,
|
||||
)
|
||||
|
||||
CartesianChartHost(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
chart = rememberCartesianChart(
|
||||
rememberRawMarketChartLayer(
|
||||
rememberMarketChartLayer(
|
||||
lineColor = lineColor,
|
||||
backgroundLineColor = lineColor.copy(alpha = 0.24f),
|
||||
secondLineColor = Color.Gray,
|
||||
|
|
@ -297,29 +208,9 @@ private fun LayerChartPreview(
|
|||
axisValueOverrider = AxisValueOverrider.fixed(minY = model.models[0].minY),
|
||||
canvasHeight = 495,
|
||||
),
|
||||
horizontalLayout = HorizontalLayout.fullWidth(),
|
||||
),
|
||||
zoomState = rememberVicoZoomState(initialZoom = Zoom.Content, zoomEnabled = false),
|
||||
horizontalLayout = HorizontalLayout.fullWidth(),
|
||||
model = model,
|
||||
)
|
||||
|
||||
CartesianChartHost(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
chart = rememberCartesianChart(
|
||||
rememberRawMarketChartLayer(
|
||||
lineColor = lineColor,
|
||||
backgroundLineColor = lineColor.copy(alpha = 0.24f),
|
||||
secondLineColor = lineColor,
|
||||
backgroundSecondLineColor = lineColor.copy(alpha = 0.24f),
|
||||
markerFraction = 0.35f,
|
||||
secondColorOnTheRightSide = true,
|
||||
animationFraction = 0.7f,
|
||||
axisValueOverrider = AxisValueOverrider.fixed(minY = model.models[0].minY),
|
||||
canvasHeight = 495,
|
||||
),
|
||||
),
|
||||
zoomState = rememberVicoZoomState(initialZoom = Zoom.Content, zoomEnabled = false),
|
||||
horizontalLayout = HorizontalLayout.fullWidth(),
|
||||
model = model,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
package com.tangem.common.ui.charts.layer
|
||||
|
||||
import com.patrykandpatrick.vico.core.cartesian.CartesianDrawContext
|
||||
import com.patrykandpatrick.vico.core.cartesian.CartesianMeasureContext
|
||||
import com.patrykandpatrick.vico.core.cartesian.HorizontalDimensions
|
||||
import com.patrykandpatrick.vico.core.cartesian.axis.HorizontalAxis
|
||||
import com.patrykandpatrick.vico.core.cartesian.data.ChartValues
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
class TimeItemPlacer : HorizontalAxis.ItemPlacer {
|
||||
|
||||
private val ChartValues.measuredLabelValues
|
||||
get() = buildList {
|
||||
// produce exactly 6 values distributed evenly
|
||||
val xLength = maxX - minX
|
||||
val xStep = xLength / 7
|
||||
|
||||
repeat(times = 6) {
|
||||
add(minX + xStep * (it + 1))
|
||||
}
|
||||
}
|
||||
|
||||
override fun getEndHorizontalAxisInset(
|
||||
context: CartesianMeasureContext,
|
||||
horizontalDimensions: HorizontalDimensions,
|
||||
tickThickness: Float,
|
||||
maxLabelWidth: Float,
|
||||
): Float = 0f
|
||||
|
||||
override fun getStartHorizontalAxisInset(
|
||||
context: CartesianMeasureContext,
|
||||
horizontalDimensions: HorizontalDimensions,
|
||||
tickThickness: Float,
|
||||
maxLabelWidth: Float,
|
||||
): Float = 0f
|
||||
|
||||
override fun getHeightMeasurementLabelValues(
|
||||
context: CartesianMeasureContext,
|
||||
horizontalDimensions: HorizontalDimensions,
|
||||
fullXRange: ClosedFloatingPointRange<Double>,
|
||||
maxLabelWidth: Float,
|
||||
): List<Double> = context.chartValues.measuredLabelValues
|
||||
|
||||
override fun getLabelValues(
|
||||
context: CartesianDrawContext,
|
||||
visibleXRange: ClosedFloatingPointRange<Double>,
|
||||
fullXRange: ClosedFloatingPointRange<Double>,
|
||||
maxLabelWidth: Float,
|
||||
): List<Double> = context.chartValues.measuredLabelValues
|
||||
|
||||
override fun getWidthMeasurementLabelValues(
|
||||
context: CartesianMeasureContext,
|
||||
horizontalDimensions: HorizontalDimensions,
|
||||
fullXRange: ClosedFloatingPointRange<Double>,
|
||||
): List<Double> = context.chartValues.measuredLabelValues
|
||||
}
|
||||
|
|
@ -1,146 +0,0 @@
|
|||
package com.tangem.common.ui.charts.marker
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.patrykandpatrick.vico.compose.cartesian.CartesianChartHost
|
||||
import com.patrykandpatrick.vico.compose.cartesian.fullWidth
|
||||
import com.patrykandpatrick.vico.compose.cartesian.layer.rememberLineCartesianLayer
|
||||
import com.patrykandpatrick.vico.compose.cartesian.layer.rememberLineSpec
|
||||
import com.patrykandpatrick.vico.compose.cartesian.rememberCartesianChart
|
||||
import com.patrykandpatrick.vico.compose.cartesian.rememberVicoZoomState
|
||||
import com.patrykandpatrick.vico.compose.common.component.rememberLayeredComponent
|
||||
import com.patrykandpatrick.vico.compose.common.component.rememberShapeComponent
|
||||
import com.patrykandpatrick.vico.compose.common.component.rememberUnboundedLineComponent
|
||||
import com.patrykandpatrick.vico.compose.common.of
|
||||
import com.patrykandpatrick.vico.compose.common.shader.color
|
||||
import com.patrykandpatrick.vico.compose.common.shape.dashed
|
||||
import com.patrykandpatrick.vico.core.cartesian.*
|
||||
import com.patrykandpatrick.vico.core.cartesian.data.AxisValueOverrider
|
||||
import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModel
|
||||
import com.patrykandpatrick.vico.core.cartesian.data.LineCartesianLayerModel
|
||||
import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarker
|
||||
import com.patrykandpatrick.vico.core.cartesian.marker.DefaultCartesianMarker
|
||||
import com.patrykandpatrick.vico.core.common.Dimensions
|
||||
import com.patrykandpatrick.vico.core.common.component.TextComponent
|
||||
import com.patrykandpatrick.vico.core.common.shader.DynamicShader
|
||||
import com.patrykandpatrick.vico.core.common.shape.Shape
|
||||
import com.tangem.common.ui.charts.preview.MarketChartPreviewDataProvider
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* @param color The color of the indicator and guideline.
|
||||
* @param innerCircleColor The color of the inner circle of the indicator.
|
||||
*
|
||||
* @return A [CartesianMarker] that consists of a dashed guideline and a layered indicator with a shadow effect.
|
||||
*/
|
||||
@Composable
|
||||
internal fun rememberTangemChartMarker(color: Color, innerCircleColor: Color): CartesianMarker {
|
||||
val indicatorFrontComponent = rememberShapeComponent(
|
||||
shape = Shape.Pill,
|
||||
color = innerCircleColor,
|
||||
)
|
||||
val indicatorCenterComponent = rememberShapeComponent(
|
||||
shape = Shape.Pill,
|
||||
color = color,
|
||||
)
|
||||
val indicatorRearComponent = rememberShapeComponent(
|
||||
shape = Shape.Pill,
|
||||
color = if (color == Color.Transparent) {
|
||||
Color.Transparent
|
||||
} else {
|
||||
color.copy(alpha = INDICATOR_REAR_COLOR_ALPHA)
|
||||
},
|
||||
)
|
||||
val indicator = rememberLayeredComponent(
|
||||
rear = indicatorRearComponent,
|
||||
front = rememberLayeredComponent(
|
||||
rear = indicatorCenterComponent,
|
||||
front = indicatorFrontComponent,
|
||||
padding = indicatorPadding,
|
||||
),
|
||||
padding = indicatorPadding,
|
||||
)
|
||||
val guideline = rememberUnboundedLineComponent(
|
||||
color = color,
|
||||
verticalAddDrawSpace = TangemTheme.dimens.spacing24,
|
||||
shape = remember { Shape.dashed(Shape.Rectangle, 4.dp, 4.dp) },
|
||||
)
|
||||
return remember(indicator, guideline) {
|
||||
object : DefaultCartesianMarker(
|
||||
label = TextComponent.build { textSizeSp = 0f },
|
||||
indicator = indicator,
|
||||
indicatorSizeDp = INDICATOR_SIZE_DP,
|
||||
guideline = guideline,
|
||||
) {
|
||||
override fun getInsets(
|
||||
context: CartesianMeasureContext,
|
||||
outInsets: Insets,
|
||||
horizontalDimensions: HorizontalDimensions,
|
||||
) {
|
||||
with(context) {
|
||||
super.getInsets(context, outInsets, horizontalDimensions)
|
||||
val baseShadowInsetDp =
|
||||
CLIPPING_FREE_SHADOW_RADIUS_MULTIPLIER * LABEL_BACKGROUND_SHADOW_RADIUS_DP
|
||||
outInsets.top += (baseShadowInsetDp - LABEL_BACKGROUND_SHADOW_DY_DP).pixels
|
||||
outInsets.bottom += (baseShadowInsetDp + LABEL_BACKGROUND_SHADOW_DY_DP).pixels
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val indicatorPadding = Dimensions.of(3.dp)
|
||||
private const val LABEL_BACKGROUND_SHADOW_RADIUS_DP = 4f
|
||||
private const val LABEL_BACKGROUND_SHADOW_DY_DP = 2f
|
||||
private const val CLIPPING_FREE_SHADOW_RADIUS_MULTIPLIER = 1.4f
|
||||
private const val INDICATOR_SIZE_DP = 16f
|
||||
private const val INDICATOR_REAR_COLOR_ALPHA = .24f
|
||||
|
||||
// region Preview
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun TangemChartMarkerPreview(
|
||||
@PreviewParameter(MarketChartPreviewDataProvider::class) previewData: Pair<List<BigDecimal>, List<BigDecimal>>,
|
||||
) {
|
||||
val marker = rememberTangemChartMarker(Color.Red, Color.White)
|
||||
val y = previewData.second.map { it.toFloat() }
|
||||
val x = List(y.size) { it.toFloat() }
|
||||
val model = CartesianChartModel(LineCartesianLayerModel.build { series(x, y) })
|
||||
|
||||
val centerAprx = (model.models[0].minX + model.models[0].maxX) / 2f
|
||||
val center = model.models[0].getXDeltaGcd().let { centerAprx - centerAprx % it }
|
||||
|
||||
TangemThemePreview {
|
||||
Box(modifier = Modifier.background(TangemTheme.colors.background.primary)) {
|
||||
CartesianChartHost(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
chart = rememberCartesianChart(
|
||||
rememberLineCartesianLayer(
|
||||
listOf(rememberLineSpec(shader = DynamicShader.color(Color.Blue))),
|
||||
axisValueOverrider = AxisValueOverrider.fixed(minY = model.models[0].minY),
|
||||
),
|
||||
persistentMarkers = mapOf(center to marker),
|
||||
),
|
||||
model = model,
|
||||
marker = marker,
|
||||
zoomState = rememberVicoZoomState(initialZoom = Zoom.Content, zoomEnabled = false),
|
||||
horizontalLayout = HorizontalLayout.fullWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// endregion Preview
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
package com.tangem.common.ui.charts.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Immutable
|
||||
|
|
@ -30,7 +32,7 @@ sealed interface MarketChartData {
|
|||
*/
|
||||
@Immutable
|
||||
data class Data(
|
||||
val x: List<BigDecimal> = listOf(),
|
||||
val y: List<BigDecimal> = listOf(),
|
||||
val x: ImmutableList<BigDecimal> = persistentListOf(),
|
||||
val y: ImmutableList<BigDecimal> = persistentListOf(),
|
||||
) : MarketChartData
|
||||
}
|
||||
|
|
@ -3,13 +3,13 @@ package com.tangem.common.ui.charts.state
|
|||
import androidx.compose.runtime.Stable
|
||||
import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModelProducer
|
||||
import com.patrykandpatrick.vico.core.cartesian.data.LineCartesianLayerModel
|
||||
import com.patrykandpatrick.vico.core.common.data.ExtraStore
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import com.tangem.common.ui.charts.state.converter.PointValuesConverter
|
||||
import com.tangem.common.ui.charts.state.converter.PriceAndTimePointValuesConverter
|
||||
import com.tangem.common.ui.charts.state.formatter.FormatterWrapWithCache
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.math.BigDecimal
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
/**
|
||||
* This class represents a transaction for updating the state and look of a Market Chart.
|
||||
|
|
@ -25,7 +25,12 @@ class Transaction(
|
|||
var chartData: MarketChartData.NoData? = null
|
||||
|
||||
fun updateLook(block: (prev: MarketChartLook) -> MarketChartLook) {
|
||||
chartLook = block(currentLook)
|
||||
val newLook = block(currentLook)
|
||||
|
||||
chartLook = newLook.copy(
|
||||
xAxisFormatter = FormatterWrapWithCache(newLook.xAxisFormatter),
|
||||
yAxisFormatter = FormatterWrapWithCache(newLook.yAxisFormatter),
|
||||
)
|
||||
}
|
||||
|
||||
fun updateState(block: (prev: MarketChartData) -> MarketChartData.NoData) {
|
||||
|
|
@ -56,7 +61,12 @@ class TransactionSuspend(
|
|||
}
|
||||
|
||||
fun updateLook(block: (prev: MarketChartLook) -> MarketChartLook) {
|
||||
chartLook = block(currentLook)
|
||||
val newLook = block(currentLook)
|
||||
|
||||
chartLook = newLook.copy(
|
||||
xAxisFormatter = FormatterWrapWithCache(newLook.xAxisFormatter),
|
||||
yAxisFormatter = FormatterWrapWithCache(newLook.yAxisFormatter),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun updateState(block: (prev: MarketChartData) -> MarketChartData) {
|
||||
|
|
@ -75,21 +85,24 @@ class TransactionSuspend(
|
|||
class MarketChartDataProducer private constructor(
|
||||
initialData: MarketChartData,
|
||||
initialLook: MarketChartLook,
|
||||
val pointsValuesConverter: PointValuesConverter = DefaultPointValuesConverter,
|
||||
val pointsValuesConverter: PointValuesConverter = PriceAndTimePointValuesConverter(needToFormatAxis = true),
|
||||
private val dispatcher: CoroutineDispatcher = Dispatchers.Default,
|
||||
) {
|
||||
internal val startDrawingAnimation = MutableSharedFlow<Unit>()
|
||||
internal val dataState = MutableStateFlow(initialData)
|
||||
internal val lookState = MutableStateFlow(initialLook)
|
||||
internal val entries = MutableStateFlow<List<LineCartesianLayerModel.Entry>>(emptyList())
|
||||
|
||||
internal val modelProducer = CartesianChartModelProducer.build(dispatcher = dispatcher)
|
||||
internal val modelProducer = CartesianChartModelProducer(dispatcher = dispatcher)
|
||||
internal val rawData = MutableStateFlow<MarketChartRawData?>(null)
|
||||
private val mutex = Mutex()
|
||||
|
||||
/**
|
||||
* This function runs a suspending transaction block to update the state and look of the Market Chart.
|
||||
*/
|
||||
suspend fun runTransactionSuspend(block: TransactionSuspend.() -> Unit) =
|
||||
handleTransactionSuspend(transaction = TransactionSuspend(dataState.value, lookState.value).apply(block))
|
||||
suspend fun runTransactionSuspend(block: TransactionSuspend.() -> Unit) = withContext(dispatcher) {
|
||||
mutex.withLock {
|
||||
handleTransactionSuspend(transaction = TransactionSuspend(dataState.value, lookState.value).apply(block))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function runs a non-suspending transaction block to update the state and look of the Market Chart.
|
||||
|
|
@ -102,32 +115,30 @@ class MarketChartDataProducer private constructor(
|
|||
val chartData = transaction.chartData
|
||||
val oldData = dataState.value
|
||||
|
||||
if (chartData != null) {
|
||||
dataState.value = chartData
|
||||
}
|
||||
|
||||
if (chartData is MarketChartData.Data && (oldData !is MarketChartData.Data || oldData != chartData)) {
|
||||
if (lookState.value.animationOnDataChange) {
|
||||
startDrawingAnimation.emit(Unit)
|
||||
}
|
||||
withContext(dispatcher) {
|
||||
val rawData = pointsValuesConverter.convert(chartData)
|
||||
(lookState.value.xAxisFormatter as? FormatterWrapWithCache)?.clearCache()
|
||||
(lookState.value.yAxisFormatter as? FormatterWrapWithCache)?.clearCache()
|
||||
|
||||
val entriesLocal =
|
||||
rawData.x.mapIndexed { index, fl -> LineCartesianLayerModel.Entry(fl, rawData.y[index]) }
|
||||
val rawData = pointsValuesConverter.convert(chartData)
|
||||
|
||||
entries.value = entriesLocal
|
||||
val entriesLocal =
|
||||
rawData.x.mapIndexed { index, fl -> LineCartesianLayerModel.Entry(fl, rawData.y[index]) }
|
||||
|
||||
currentCoroutineContext().ensureActive()
|
||||
|
||||
runCatching {
|
||||
modelProducer.runTransaction {
|
||||
add(LineCartesianLayerModel.Partial(series = listOf(entriesLocal)))
|
||||
|
||||
updateExtras {
|
||||
it[entriesKey] = entriesLocal
|
||||
it[xKey] = chartData.x
|
||||
it[yKey] = chartData.y
|
||||
}
|
||||
}.await()
|
||||
}
|
||||
}
|
||||
|
||||
entries.value = entriesLocal
|
||||
dataState.value = chartData
|
||||
this.rawData.value = rawData
|
||||
|
||||
delay(timeMillis = 200)
|
||||
} else if (chartData != null) {
|
||||
dataState.value = chartData
|
||||
}
|
||||
|
||||
nonSuspendTransaction?.let { handleTransaction(it) }
|
||||
|
|
@ -143,10 +154,6 @@ class MarketChartDataProducer private constructor(
|
|||
}
|
||||
|
||||
companion object {
|
||||
internal val entriesKey = ExtraStore.Key<List<LineCartesianLayerModel.Entry>>()
|
||||
internal val xKey = ExtraStore.Key<List<BigDecimal>>()
|
||||
internal val yKey = ExtraStore.Key<List<BigDecimal>>()
|
||||
|
||||
private val initialData: MarketChartData = MarketChartData.NoData.Empty
|
||||
private val initialLook: MarketChartLook = MarketChartLook()
|
||||
|
||||
|
|
@ -159,7 +166,7 @@ class MarketChartDataProducer private constructor(
|
|||
* @return A MarketChartDataProducer.
|
||||
*/
|
||||
suspend fun buildSuspend(
|
||||
pointsValuesConverter: PointValuesConverter = DefaultPointValuesConverter,
|
||||
pointsValuesConverter: PointValuesConverter = PriceAndTimePointValuesConverter(needToFormatAxis = true),
|
||||
dispatcher: CoroutineDispatcher = Dispatchers.Default,
|
||||
block: TransactionSuspend.() -> Unit,
|
||||
): MarketChartDataProducer {
|
||||
|
|
@ -184,7 +191,7 @@ class MarketChartDataProducer private constructor(
|
|||
* @return A MarketChartDataProducer.
|
||||
*/
|
||||
fun build(
|
||||
pointsValuesConverter: PointValuesConverter = DefaultPointValuesConverter,
|
||||
pointsValuesConverter: PointValuesConverter = PriceAndTimePointValuesConverter(needToFormatAxis = true),
|
||||
dispatcher: CoroutineDispatcher = Dispatchers.Default,
|
||||
block: Transaction.() -> Unit,
|
||||
): MarketChartDataProducer {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
package com.tangem.common.ui.charts.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.common.ui.charts.state.formatter.AxisLabelFormatter
|
||||
|
||||
/**
|
||||
* This class represents the look and feel of a Market Chart.
|
||||
* It includes properties for type, marker highlight, animation on data change, animate data appearance,
|
||||
|
|
@ -7,16 +10,13 @@ package com.tangem.common.ui.charts.state
|
|||
*
|
||||
* @property type The type of the chart, can be either Growing or Falling.
|
||||
* @property markerHighlightRightSide A boolean indicating whether the marker highlights the right side of the chart.
|
||||
* @property animationOnDataChange A boolean indicating whether to animate on data change.
|
||||
* @property animateDataAppearance A boolean indicating whether to animate data appearance.
|
||||
* @property xAxisFormatter A formatter for the x-axis labels.
|
||||
* @property yAxisFormatter A formatter for the y-axis labels.
|
||||
*/
|
||||
@Immutable
|
||||
data class MarketChartLook(
|
||||
val type: Type = Type.Growing,
|
||||
val markerHighlightRightSide: Boolean = true,
|
||||
val animationOnDataChange: Boolean = false,
|
||||
val animateDataAppearance: Boolean = false,
|
||||
val xAxisFormatter: AxisLabelFormatter = AxisLabelFormatter { it.toString() },
|
||||
val yAxisFormatter: AxisLabelFormatter = AxisLabelFormatter { it.toString() },
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,20 @@
|
|||
package com.tangem.common.ui.charts.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
/**
|
||||
* This class represents raw data for a Market Chart. Used for drawing the chart.
|
||||
*
|
||||
* @property originalIndexes If the source data has the original representation (due to reduced sampling),
|
||||
* this list contains the original indexes of the data points.
|
||||
* @property y The list of y-values.
|
||||
* @property x The list of x-values.
|
||||
*/
|
||||
@Immutable
|
||||
data class MarketChartRawData(
|
||||
val y: List<Float>,
|
||||
val x: List<Float> = List(y.size) { 1f },
|
||||
val originalIndexes: ImmutableList<Int>? = null,
|
||||
val y: ImmutableList<Double>,
|
||||
val x: ImmutableList<Double> = List(y.size) { 1.0 }.toImmutableList(),
|
||||
)
|
||||
|
|
@ -2,7 +2,6 @@ package com.tangem.common.ui.charts.state
|
|||
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.patrykandpatrick.vico.core.cartesian.data.CartesianValueFormatter
|
||||
import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarker
|
||||
import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarkerVisibilityListener
|
||||
|
|
@ -20,26 +19,22 @@ import java.math.BigDecimal
|
|||
@Composable
|
||||
fun rememberMarketChartState(
|
||||
dataProducer: MarketChartDataProducer = remember { MarketChartDataProducer.build {} },
|
||||
colorMapper: (MarketChartLook.Type) -> Color = {
|
||||
when (it) {
|
||||
MarketChartLook.Type.Growing -> Color.Green
|
||||
MarketChartLook.Type.Falling -> Color.Red
|
||||
colorMapper: (MarketChartLook.Type) -> Color = remember {
|
||||
{
|
||||
when (it) {
|
||||
MarketChartLook.Type.Growing -> Color.Green
|
||||
MarketChartLook.Type.Falling -> Color.Red
|
||||
}
|
||||
}
|
||||
},
|
||||
onMarkerShown: (x: BigDecimal?, y: BigDecimal?) -> Unit = { _, _ -> },
|
||||
): MarketChartState {
|
||||
val lookState = dataProducer.lookState.collectAsStateWithLifecycle()
|
||||
val lookState = dataProducer.lookState.collectAsState()
|
||||
|
||||
val state = remember(dataProducer, lookState, colorMapper, onMarkerShown) {
|
||||
MarketChartState(dataProducer, lookState, colorMapper, onMarkerShown)
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
dataProducer.startDrawingAnimation.collect {
|
||||
state.startDrawingAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
|
|
@ -59,7 +54,6 @@ class MarketChartState internal constructor(
|
|||
private val colorMapper: (MarketChartLook.Type) -> Color,
|
||||
private val markerCallback: (x: BigDecimal?, y: BigDecimal?) -> Unit,
|
||||
) {
|
||||
internal val startDrawingAnimationState = mutableStateOf(false)
|
||||
internal val modelProducer = dataProducer.modelProducer
|
||||
|
||||
internal val chartColor by derivedStateOf {
|
||||
|
|
@ -70,29 +64,29 @@ class MarketChartState internal constructor(
|
|||
lookState.value.markerHighlightRightSide
|
||||
}
|
||||
|
||||
internal val xValueFormatter by derivedStateOf {
|
||||
CartesianValueFormatter { value, _, _ ->
|
||||
val state = dataProducer.dataState.value as? MarketChartData.Data
|
||||
?: return@CartesianValueFormatter value.toString()
|
||||
internal val xValueFormatter = CartesianValueFormatter { value, _, _ ->
|
||||
val formatter = dataProducer.lookState.value.xAxisFormatter
|
||||
|
||||
lookState.value.xAxisFormatter.format(
|
||||
value = dataProducer.pointsValuesConverter.prepareRawXForFormat(value, state),
|
||||
)
|
||||
}
|
||||
val state = dataProducer.dataState.value as? MarketChartData.Data
|
||||
?: return@CartesianValueFormatter value.toString()
|
||||
|
||||
formatter.format(
|
||||
value = dataProducer.pointsValuesConverter.prepareRawXForFormat(value, state),
|
||||
)
|
||||
}
|
||||
|
||||
internal val yValueFormatter by derivedStateOf {
|
||||
CartesianValueFormatter { value, _, _ ->
|
||||
val state = dataProducer.dataState.value as? MarketChartData.Data
|
||||
?: return@CartesianValueFormatter value.toString()
|
||||
internal val yValueFormatter = CartesianValueFormatter { value, _, _ ->
|
||||
val formatter = dataProducer.lookState.value.yAxisFormatter
|
||||
|
||||
lookState.value.yAxisFormatter.format(
|
||||
value = dataProducer.pointsValuesConverter.prepareRawYForFormat(value, state),
|
||||
)
|
||||
}
|
||||
val state = dataProducer.dataState.value as? MarketChartData.Data
|
||||
?: return@CartesianValueFormatter value.toString()
|
||||
|
||||
formatter.format(
|
||||
value = dataProducer.pointsValuesConverter.prepareRawYForFormat(value, state),
|
||||
)
|
||||
}
|
||||
|
||||
internal var markerFraction: Float? by mutableStateOf(null)
|
||||
internal var markerFraction by mutableStateOf<Float?>(null)
|
||||
|
||||
internal val markerVisibilityListener = object : CartesianMarkerVisibilityListener {
|
||||
override fun onShown(marker: CartesianMarker, targets: List<CartesianMarker.Target>) {
|
||||
|
|
@ -116,24 +110,17 @@ class MarketChartState internal constructor(
|
|||
}
|
||||
}
|
||||
|
||||
val isDrawingAnimationInProgress: Boolean by derivedStateOf {
|
||||
startDrawingAnimationState.value
|
||||
}
|
||||
|
||||
private fun getPoint(targets: List<CartesianMarker.Target>): Pair<BigDecimal, BigDecimal>? {
|
||||
val entry = (targets[0] as LineCartesianLayerMarkerTarget).points[0].entry
|
||||
val entryIndex = dataProducer.entries.value.indexOf(entry).takeIf { it != -1 } ?: return null
|
||||
val state = dataProducer.dataState.value as? MarketChartData.Data ?: return null
|
||||
val x = state.x.getOrNull(entryIndex) ?: return null
|
||||
val y = state.y.getOrNull(entryIndex) ?: return null
|
||||
val rawData = dataProducer.rawData.value ?: return null
|
||||
|
||||
val originalIndex = rawData.originalIndexes?.getOrNull(entryIndex)
|
||||
val index = originalIndex ?: entryIndex
|
||||
|
||||
val x = state.x.getOrNull(index) ?: return null
|
||||
val y = state.y.getOrNull(index) ?: return null
|
||||
return x to y
|
||||
}
|
||||
|
||||
fun startDrawingAnimation() {
|
||||
startDrawingAnimationState.value = true
|
||||
}
|
||||
|
||||
fun stopDrawingAnimation() {
|
||||
startDrawingAnimationState.value = false
|
||||
}
|
||||
}
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
package com.tangem.common.ui.charts.state
|
||||
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Interface to convert chart data values to Floats and backwards.
|
||||
*
|
||||
* We need to convert the values on the graph to floating point values in order to display them correctly on the canvas.
|
||||
* We also need to determine exactly which floating point value on the graph corresponds to the decimal point,
|
||||
* so that we can format the actual value and display on the x/y axis.
|
||||
*/
|
||||
interface PointValuesConverter {
|
||||
|
||||
fun convert(data: MarketChartData.Data): MarketChartRawData
|
||||
|
||||
fun prepareRawXForFormat(rawX: Float, data: MarketChartData.Data): BigDecimal
|
||||
|
||||
fun prepareRawYForFormat(rawY: Float, data: MarketChartData.Data): BigDecimal
|
||||
}
|
||||
|
||||
object DefaultPointValuesConverter : PointValuesConverter {
|
||||
|
||||
override fun convert(data: MarketChartData.Data): MarketChartRawData {
|
||||
val minX = data.x.min()
|
||||
val minY = data.y.min()
|
||||
|
||||
val normY = data.y.map { normalize(it, minY) }
|
||||
val normX = data.x.map { normalize(it, minX) }
|
||||
|
||||
return MarketChartRawData(
|
||||
x = normX,
|
||||
y = normY,
|
||||
)
|
||||
}
|
||||
|
||||
override fun prepareRawXForFormat(rawX: Float, data: MarketChartData.Data): BigDecimal {
|
||||
val dataMin = data.x.min()
|
||||
val scale = dataMin.scale()
|
||||
val bVal = if (scale > 2) {
|
||||
rawX.toBigDecimal().movePointLeft(scale - 2) + dataMin
|
||||
} else {
|
||||
rawX.toBigDecimal() + dataMin
|
||||
}
|
||||
|
||||
return bVal
|
||||
}
|
||||
|
||||
override fun prepareRawYForFormat(rawY: Float, data: MarketChartData.Data): BigDecimal {
|
||||
val dataMin = data.y.min()
|
||||
val scale = dataMin.scale()
|
||||
val bVal = if (scale > 2) {
|
||||
rawY.toBigDecimal().movePointLeft(scale - 2) + dataMin
|
||||
} else {
|
||||
rawY.toBigDecimal() + dataMin
|
||||
}
|
||||
|
||||
return bVal
|
||||
}
|
||||
|
||||
// TODO enhance algorithm for values with big difference between min and max, which cannot fit in Float
|
||||
private fun normalize(value: BigDecimal, min: BigDecimal, scale: Int = min.scale()): Float {
|
||||
val n = value - min
|
||||
return if (scale > 2) {
|
||||
n.movePointRight(scale - 2).toFloat()
|
||||
} else {
|
||||
n.toFloat()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package com.tangem.common.ui.charts.state.converter
|
||||
|
||||
import com.tangem.common.ui.charts.state.MarketChartData
|
||||
import com.tangem.common.ui.charts.state.MarketChartRawData
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Interface to convert chart data values to Floats and backwards.
|
||||
*
|
||||
* We need to convert the values on the graph to floating point values in order to display them correctly on the canvas.
|
||||
* We also need to determine exactly which floating point value on the graph corresponds to the decimal point,
|
||||
* so that we can format the actual value and display on the x/y axis.
|
||||
*
|
||||
* **[prepareRawXForFormat] and [prepareRawYForFormat] must be very fast because they are called in the onDraw method**
|
||||
*/
|
||||
interface PointValuesConverter {
|
||||
|
||||
fun convert(data: MarketChartData.Data): MarketChartRawData
|
||||
|
||||
fun prepareRawXForFormat(rawX: Double, data: MarketChartData.Data): BigDecimal
|
||||
|
||||
fun prepareRawYForFormat(rawY: Double, data: MarketChartData.Data): BigDecimal
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
package com.tangem.common.ui.charts.state.converter
|
||||
|
||||
import com.tangem.common.ui.charts.downsample.LTThreeBuckets
|
||||
import com.tangem.common.ui.charts.state.MarketChartData
|
||||
import com.tangem.common.ui.charts.state.MarketChartRawData
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
class PriceAndTimePointValuesConverter(
|
||||
private val needToFormatAxis: Boolean,
|
||||
) : PointValuesConverter {
|
||||
|
||||
private data class MinMaxCache(
|
||||
val minX: BigDecimal,
|
||||
val maxX: BigDecimal,
|
||||
val minY: BigDecimal,
|
||||
val maxY: BigDecimal,
|
||||
)
|
||||
|
||||
private var minMaxCache = MinMaxCache(BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO)
|
||||
private val formatYValuesCache = mutableMapOf<Double, BigDecimal>()
|
||||
private val formatXValuesCache = mutableMapOf<Double, BigDecimal>()
|
||||
|
||||
override fun convert(data: MarketChartData.Data): MarketChartRawData {
|
||||
formatYValuesCache.clear()
|
||||
formatXValuesCache.clear()
|
||||
val cache = MinMaxCache(
|
||||
minY = data.y.minOrNull() ?: BigDecimal.ZERO,
|
||||
maxY = data.y.maxOrNull() ?: BigDecimal.ZERO,
|
||||
minX = data.x.minOrNull() ?: BigDecimal.ZERO,
|
||||
maxX = data.x.maxOrNull() ?: BigDecimal.ZERO,
|
||||
)
|
||||
minMaxCache = cache
|
||||
|
||||
val normY = data.y.normalizeToDouble(min = cache.minY, max = cache.maxY)
|
||||
val normX = data.x.normalizeTime(min = cache.minX, max = cache.maxX)
|
||||
|
||||
return if (normX.size > MAX_POINTS) {
|
||||
LTThreeBuckets
|
||||
.downsample(normX, normY, MAX_POINTS - 2)
|
||||
.let {
|
||||
MarketChartRawData(
|
||||
originalIndexes = it.originalIndexes.toImmutableList(),
|
||||
x = it.x.toImmutableList(),
|
||||
y = it.y.toImmutableList(),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
MarketChartRawData(
|
||||
x = normX.toImmutableList(),
|
||||
y = normY.toImmutableList(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun prepareRawXForFormat(rawX: Double, data: MarketChartData.Data): BigDecimal {
|
||||
if (!needToFormatAxis) return BigDecimal.ZERO
|
||||
if (formatXValuesCache.containsKey(rawX)) return formatXValuesCache[rawX]!!
|
||||
|
||||
val result = (rawX * MINUTE).toBigDecimal()
|
||||
|
||||
formatXValuesCache[rawX] = result
|
||||
return result
|
||||
}
|
||||
|
||||
override fun prepareRawYForFormat(rawY: Double, data: MarketChartData.Data): BigDecimal {
|
||||
if (!needToFormatAxis) return BigDecimal.ZERO
|
||||
if (formatYValuesCache.containsKey(rawY)) return formatYValuesCache[rawY]!!
|
||||
|
||||
val min = minMaxCache.minY
|
||||
val max = minMaxCache.maxY
|
||||
val length = max - min
|
||||
|
||||
val result = when {
|
||||
rawY < 0.01f -> min
|
||||
rawY < 0.55f && rawY > 0.45f -> min + length / 2.toBigDecimal()
|
||||
rawY > 0.97f && rawY < 1.01f -> max
|
||||
else -> length * rawY.toBigDecimal() + min
|
||||
}
|
||||
formatYValuesCache[rawY] = result
|
||||
return result
|
||||
}
|
||||
|
||||
private fun List<BigDecimal>.normalizeToDouble(min: BigDecimal, max: BigDecimal): List<Double> {
|
||||
if (min == max) {
|
||||
return List(size) { 0.5 }
|
||||
}
|
||||
|
||||
return map { ((it - min) / (max - min)).toDouble() }
|
||||
}
|
||||
|
||||
private fun List<BigDecimal>.normalizeTime(min: BigDecimal, max: BigDecimal): List<Double> {
|
||||
if (min == max) {
|
||||
return List(size) { 0.5 }
|
||||
}
|
||||
|
||||
return map {
|
||||
(it / MINUTE_BIG).toDouble()
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private const val MAX_POINTS = 502
|
||||
private const val MINUTE = 60000L
|
||||
private val MINUTE_BIG = 60000L.toBigDecimal()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.common.ui.charts.state
|
||||
package com.tangem.common.ui.charts.state.formatter
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import java.math.BigDecimal
|
||||
|
|
@ -7,6 +7,8 @@ import java.math.BigDecimal
|
|||
* Used for formatting the axis labels in a chart.
|
||||
* It takes a BigDecimal value and returns a CharSequence that represents the formatted label.
|
||||
*
|
||||
* [format] has to be very fast because it is called in the onDraw method.
|
||||
*
|
||||
* @param value The value to be formatted.
|
||||
* @return The formatted label as a CharSequence.
|
||||
*/
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.common.ui.charts.state.formatter
|
||||
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class FormatterWrapWithCache(private val formatter: AxisLabelFormatter) : AxisLabelFormatter {
|
||||
private val cache = mutableMapOf<BigDecimal, CharSequence>()
|
||||
|
||||
override fun format(value: BigDecimal): CharSequence {
|
||||
return cache.getOrPut(value) { formatter.format(value) }
|
||||
}
|
||||
|
||||
fun clearCache() {
|
||||
cache.clear()
|
||||
}
|
||||
}
|
||||
|
|
@ -2,10 +2,12 @@ package com.tangem.common.ui.amountScreen.converters
|
|||
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import com.tangem.common.ui.R
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.common.ui.amountScreen.utils.checkExceedBalance
|
||||
import com.tangem.common.ui.amountScreen.utils.getFiatValue
|
||||
import com.tangem.common.ui.amountScreen.utils.getKeyboardAction
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.utils.parseBigDecimal
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.utils.isNullOrZero
|
||||
|
|
@ -51,6 +53,7 @@ class AmountReduceByTransformer(
|
|||
value = cryptoValue,
|
||||
fiatValue = fiatValue,
|
||||
isError = isExceedBalance,
|
||||
error = resourceReference(R.string.send_validation_amount_exceeds_balance),
|
||||
cryptoAmount = amountTextField.cryptoAmount.copy(value = decimalCryptoValue),
|
||||
fiatAmount = amountTextField.fiatAmount.copy(value = decimalFiatValue),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
|
|
|
|||
|
|
@ -2,10 +2,12 @@ package com.tangem.common.ui.amountScreen.converters
|
|||
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import com.tangem.common.ui.R
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.common.ui.amountScreen.utils.checkExceedBalance
|
||||
import com.tangem.common.ui.amountScreen.utils.getFiatValue
|
||||
import com.tangem.common.ui.amountScreen.utils.getKeyboardAction
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.utils.parseBigDecimal
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.utils.isNullOrZero
|
||||
|
|
@ -45,6 +47,7 @@ class AmountReduceToTransformer(
|
|||
value = cryptoValue,
|
||||
fiatValue = fiatValue,
|
||||
isError = isExceedBalance,
|
||||
error = resourceReference(R.string.send_validation_amount_exceeds_balance),
|
||||
cryptoAmount = amountTextField.cryptoAmount.copy(value = value),
|
||||
fiatAmount = amountTextField.fiatAmount.copy(value = decimalFiatValue),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
|
|
|
|||
|
|
@ -2,11 +2,13 @@ package com.tangem.common.ui.amountScreen.converters.field
|
|||
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import com.tangem.common.ui.R
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.common.ui.amountScreen.utils.checkExceedBalance
|
||||
import com.tangem.common.ui.amountScreen.utils.getCryptoValue
|
||||
import com.tangem.common.ui.amountScreen.utils.getFiatValue
|
||||
import com.tangem.common.ui.amountScreen.utils.getKeyboardAction
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.utils.parseToBigDecimal
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.utils.isNullOrZero
|
||||
|
|
@ -59,6 +61,7 @@ class AmountFieldChangeTransformer(
|
|||
value = cryptoValue,
|
||||
fiatValue = fiatValue,
|
||||
isError = isExceedBalance,
|
||||
error = resourceReference(R.string.send_validation_amount_exceeds_balance),
|
||||
cryptoAmount = amountTextField.cryptoAmount.copy(value = decimalCryptoValue),
|
||||
fiatAmount = amountTextField.fiatAmount.copy(value = decimalFiatValue),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
package com.tangem.common.ui.amountScreen.ui
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.requiredHeightIn
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment.Companion.BottomCenter
|
||||
import androidx.compose.ui.Alignment.Companion.TopCenter
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
|
|
@ -80,6 +80,8 @@ private fun AmountSecondary(amountField: AmountFieldModel, appCurrencyCode: Stri
|
|||
val secondaryAmount = if (amountField.isFiatValue) amountField.cryptoAmount else amountField.fiatAmount
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.animateContentSize()
|
||||
.padding(
|
||||
top = TangemTheme.dimens.spacing8,
|
||||
start = TangemTheme.dimens.spacing12,
|
||||
|
|
@ -105,7 +107,7 @@ private fun AmountSecondary(amountField: AmountFieldModel, appCurrencyCode: Stri
|
|||
color = TangemTheme.colors.text.tertiary,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.align(BottomCenter)
|
||||
.align(TopCenter)
|
||||
.padding(bottom = TangemTheme.dimens.spacing32),
|
||||
)
|
||||
AmountFieldError(
|
||||
|
|
@ -113,7 +115,10 @@ private fun AmountSecondary(amountField: AmountFieldModel, appCurrencyCode: Stri
|
|||
error = amountField.error,
|
||||
modifier = Modifier
|
||||
.align(BottomCenter)
|
||||
.padding(bottom = TangemTheme.dimens.spacing12),
|
||||
.padding(
|
||||
top = TangemTheme.dimens.spacing20,
|
||||
bottom = TangemTheme.dimens.spacing12,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,202 @@
|
|||
package com.tangem.common.ui.navigationButtons
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import com.tangem.common.ui.navigationButtons.preview.NavigationButtonsPreview
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButton
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.rememberHapticFeedback
|
||||
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
|
||||
|
||||
@Composable
|
||||
fun NavigationButtonsBlock(buttonState: NavigationButtonsState, modifier: Modifier = Modifier) {
|
||||
val state = buttonState as? NavigationButtonsState.Data
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
) {
|
||||
ExtraButtons(state?.extraButtons, state?.txUrl)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
PreviousButton(state?.prevButton)
|
||||
PrimaryButton(state?.primaryButton, modifier = Modifier.weight(1f))
|
||||
}
|
||||
|
||||
SecondaryButton(state?.secondaryButton)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PrimaryButton(primaryButton: NavigationButton?, modifier: Modifier = Modifier) {
|
||||
AnimatedContent(
|
||||
targetState = primaryButton,
|
||||
transitionSpec = {
|
||||
val isPrimaryToHide = targetState != null && initialState == null
|
||||
val isPrimaryWasVisible = targetState == null && initialState != null
|
||||
if (isPrimaryToHide || isPrimaryWasVisible) {
|
||||
slideInVertically(initialOffsetY = { it / 2 }).plus(fadeIn())
|
||||
.togetherWith(slideOutVertically(targetOffsetY = { it / 2 }).plus(fadeOut()))
|
||||
} else {
|
||||
fadeIn().togetherWith(fadeOut())
|
||||
}
|
||||
},
|
||||
contentAlignment = Alignment.Center,
|
||||
label = "Animate show primary button",
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
) { button ->
|
||||
if (button != null && button.textReference != TextReference.EMPTY) {
|
||||
val icon = if (button.iconRes != null && button.isIconVisible) {
|
||||
TangemButtonIconPosition.End(iconResId = button.iconRes)
|
||||
} else {
|
||||
TangemButtonIconPosition.None
|
||||
}
|
||||
TangemButton(
|
||||
text = button.textReference.resolveReference(),
|
||||
enabled = button.isEnabled,
|
||||
onClick = button.onClick,
|
||||
showProgress = button.showProgress,
|
||||
colors = TangemButtonsDefaults.primaryButtonColors,
|
||||
icon = icon,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
} else {
|
||||
Spacer(modifier = Modifier.fillMaxWidth())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SecondaryButton(secondaryButton: NavigationButton?) {
|
||||
AnimatedContent(
|
||||
targetState = secondaryButton,
|
||||
transitionSpec = {
|
||||
val isPrimaryToHide = targetState != null && initialState == null
|
||||
val isPrimaryWasVisible = targetState == null && initialState != null
|
||||
if (isPrimaryToHide || isPrimaryWasVisible) {
|
||||
slideInVertically(initialOffsetY = { it / 2 }).plus(fadeIn())
|
||||
.togetherWith(slideOutVertically(targetOffsetY = { it / 2 }).plus(fadeOut()))
|
||||
} else {
|
||||
fadeIn().togetherWith(fadeOut())
|
||||
}
|
||||
},
|
||||
contentAlignment = Alignment.Center,
|
||||
label = "Animate show secondary button",
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) { button ->
|
||||
if (button != null && button.textReference != TextReference.EMPTY) {
|
||||
val icon = button.iconRes?.let { TangemButtonIconPosition.End(iconResId = it) }
|
||||
?: TangemButtonIconPosition.None
|
||||
|
||||
TangemButton(
|
||||
text = button.textReference.resolveReference(),
|
||||
enabled = button.isEnabled,
|
||||
onClick = button.onClick,
|
||||
icon = icon,
|
||||
showProgress = button.showProgress,
|
||||
colors = TangemButtonsDefaults.secondaryButtonColors,
|
||||
modifier = Modifier.padding(top = TangemTheme.dimens.spacing12),
|
||||
)
|
||||
} else {
|
||||
Spacer(modifier = Modifier.fillMaxWidth())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PreviousButton(prevButton: NavigationButton?) {
|
||||
AnimatedVisibility(
|
||||
visible = prevButton != null,
|
||||
enter = expandHorizontally(expandFrom = Alignment.End),
|
||||
exit = shrinkHorizontally(shrinkTowards = Alignment.End),
|
||||
label = "Animate show prev button",
|
||||
) {
|
||||
val button = remember(this) { requireNotNull(prevButton) }
|
||||
if (button.iconRes != null && button.isIconVisible) {
|
||||
Icon(
|
||||
painter = rememberVectorPainter(
|
||||
image = ImageVector.vectorResource(button.iconRes),
|
||||
),
|
||||
tint = TangemTheme.colors.icon.primary1,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(TangemTheme.dimens.radius16))
|
||||
.background(TangemTheme.colors.button.secondary)
|
||||
.clickable(onClick = button.onClick)
|
||||
.padding(TangemTheme.dimens.spacing12),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExtraButtons(extraButtons: ImmutableList<NavigationButton>?, txUrl: String?) {
|
||||
AnimatedVisibility(
|
||||
visible = !txUrl.isNullOrBlank() && extraButtons != null,
|
||||
enter = slideInVertically(initialOffsetY = { it / 2 }).plus(fadeIn()),
|
||||
exit = slideOutVertically(targetOffsetY = { it / 2 }).plus(fadeOut()),
|
||||
label = "Animate show sent state buttons",
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
val buttons = remember(this) { requireNotNull(extraButtons) }
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
buttons.forEach { button ->
|
||||
val icon = button.iconRes?.let { TangemButtonIconPosition.Start(iconResId = it) }
|
||||
?: TangemButtonIconPosition.None
|
||||
TangemButton(
|
||||
text = button.textReference.resolveReference(),
|
||||
icon = icon,
|
||||
onClick = rememberHapticFeedback(state = button, onAction = button.onClick),
|
||||
modifier = Modifier.weight(1f),
|
||||
enabled = button.isEnabled,
|
||||
showProgress = false,
|
||||
colors = TangemButtonsDefaults.secondaryButtonColors,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun NavigationButtonsBlock_Preview(
|
||||
@PreviewParameter(NavigationButtonsBlockDataProvider::class) navigationButtonsState: NavigationButtonsState,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
NavigationButtonsBlock(navigationButtonsState)
|
||||
}
|
||||
}
|
||||
|
||||
private class NavigationButtonsBlockDataProvider : PreviewParameterProvider<NavigationButtonsState> {
|
||||
override val values: Sequence<NavigationButtonsState>
|
||||
get() = sequenceOf(NavigationButtonsPreview.allButtons)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.tangem.common.ui.navigationButtons
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
sealed class NavigationButtonsState {
|
||||
data object Empty : NavigationButtonsState()
|
||||
|
||||
data class Data(
|
||||
val primaryButton: NavigationButton,
|
||||
val prevButton: NavigationButton?,
|
||||
val secondaryButton: NavigationButton?,
|
||||
val extraButtons: ImmutableList<NavigationButton>,
|
||||
val txUrl: String? = null,
|
||||
) : NavigationButtonsState()
|
||||
}
|
||||
|
||||
data class NavigationButton(
|
||||
val textReference: TextReference,
|
||||
@DrawableRes val iconRes: Int? = null,
|
||||
val isSecondary: Boolean,
|
||||
val isIconVisible: Boolean,
|
||||
val showProgress: Boolean,
|
||||
val isEnabled: Boolean,
|
||||
val onClick: () -> Unit,
|
||||
)
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
package com.tangem.common.ui.navigationButtons.preview
|
||||
|
||||
import com.tangem.common.ui.R
|
||||
import com.tangem.common.ui.navigationButtons.NavigationButton
|
||||
import com.tangem.common.ui.navigationButtons.NavigationButtonsState
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
internal object NavigationButtonsPreview {
|
||||
|
||||
private val extraButtons = persistentListOf(
|
||||
NavigationButton(
|
||||
textReference = resourceReference(R.string.common_explore),
|
||||
iconRes = R.drawable.ic_tangem_24,
|
||||
isSecondary = true,
|
||||
isIconVisible = true,
|
||||
showProgress = false,
|
||||
isEnabled = true,
|
||||
onClick = {},
|
||||
),
|
||||
NavigationButton(
|
||||
textReference = resourceReference(R.string.common_share),
|
||||
iconRes = R.drawable.ic_tangem_24,
|
||||
isSecondary = true,
|
||||
isIconVisible = true,
|
||||
showProgress = false,
|
||||
isEnabled = true,
|
||||
onClick = {},
|
||||
),
|
||||
)
|
||||
|
||||
private val next = NavigationButton(
|
||||
textReference = resourceReference(R.string.common_next),
|
||||
isSecondary = false,
|
||||
isIconVisible = false,
|
||||
showProgress = false,
|
||||
isEnabled = true,
|
||||
onClick = {},
|
||||
)
|
||||
private val prev = NavigationButton(
|
||||
textReference = TextReference.EMPTY,
|
||||
iconRes = R.drawable.ic_back_24,
|
||||
isSecondary = true,
|
||||
isIconVisible = true,
|
||||
showProgress = false,
|
||||
isEnabled = true,
|
||||
onClick = {},
|
||||
)
|
||||
|
||||
private val finished = NavigationButton(
|
||||
textReference = resourceReference(R.string.common_close),
|
||||
isSecondary = false,
|
||||
isIconVisible = false,
|
||||
showProgress = false,
|
||||
isEnabled = true,
|
||||
onClick = {},
|
||||
)
|
||||
|
||||
val allButtons = NavigationButtonsState.Data(
|
||||
primaryButton = finished,
|
||||
prevButton = prev,
|
||||
secondaryButton = next,
|
||||
extraButtons = extraButtons,
|
||||
txUrl = "https://tangem.com",
|
||||
)
|
||||
}
|
||||
|
|
@ -16,8 +16,8 @@ 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}")
|
||||
|
|
@ -28,6 +28,7 @@ interface TangemTechMarketsApi {
|
|||
|
||||
@GET("coins/{coin_id}/history")
|
||||
suspend fun getCoinChart(
|
||||
@Path("coin_id") coinId: String,
|
||||
@Query("currency") currency: String,
|
||||
@Query("interval") interval: String,
|
||||
): ApiResponse<TokenMarketChartResponse>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -38,5 +38,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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import androidx.compose.runtime.*
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.res.LocalBottomSheetAlwaysVisible
|
||||
|
|
@ -114,6 +115,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> PreviewBottomSheet(
|
|||
crossinline content: @Composable (ColumnScope.(T) -> Unit),
|
||||
) {
|
||||
BasicBottomSheet<T>(
|
||||
modifier = Modifier.width(360.dp),
|
||||
config = config,
|
||||
sheetState = SheetState(
|
||||
skipPartiallyExpanded = true,
|
||||
|
|
@ -137,6 +139,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicBottomSheet(
|
|||
addBottomInsets: Boolean,
|
||||
crossinline title: @Composable (BoxScope.(T) -> Unit),
|
||||
crossinline content: @Composable (ColumnScope.(T) -> Unit),
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val model = config.content as? T ?: return
|
||||
|
||||
|
|
@ -145,7 +148,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicBottomSheet(
|
|||
|
||||
ModalBottomSheet(
|
||||
// FIXME temporary solution to fix height of the bottom sheet
|
||||
modifier = Modifier.sizeIn(maxHeight = LocalWindowSize.current.height - statusBarHeight),
|
||||
modifier = modifier.heightIn(max = LocalWindowSize.current.height - statusBarHeight),
|
||||
onDismissRequest = config.onDismissRequest,
|
||||
sheetState = sheetState,
|
||||
containerColor = containerColor,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -52,10 +52,11 @@ fun TangemTheme(
|
|||
LocalHapticManager provides hapticManager,
|
||||
LocalSnackbarHostState provides snackbarHostState,
|
||||
LocalWindowSize provides windowSize,
|
||||
LocalTextSelectionColors provides TangemTextSelectionColors,
|
||||
) {
|
||||
CompositionLocalProvider(
|
||||
LocalTangemShimmer provides TangemShimmer,
|
||||
LocalMainBottomSheetColor provides remember { mutableStateOf(Color.Unspecified) },
|
||||
LocalTextSelectionColors provides TangemTextSelectionColors,
|
||||
) {
|
||||
ProvideTextStyle(
|
||||
value = TangemTheme.typography.body1,
|
||||
|
|
@ -208,11 +209,13 @@ private fun darkThemeColors(): TangemColors {
|
|||
)
|
||||
}
|
||||
|
||||
@Stable
|
||||
private val TangemTextSelectionColors = TextSelectionColors(
|
||||
handleColor = TangemColorPalette.Azure,
|
||||
backgroundColor = TangemColorPalette.Azure.copy(alpha = 0.4f),
|
||||
)
|
||||
private val TangemTextSelectionColors: TextSelectionColors
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
get() = TextSelectionColors(
|
||||
handleColor = TangemTheme.colors.text.accent,
|
||||
backgroundColor = TangemTheme.colors.text.accent.copy(alpha = 0.3f),
|
||||
)
|
||||
|
||||
private val LocalTangemColors = staticCompositionLocalOf<TangemColors> {
|
||||
error("No TangemColors provided")
|
||||
|
|
@ -247,3 +250,7 @@ val LocalWindowSize = staticCompositionLocalOf<WindowSize> {
|
|||
val LocalTangemShimmer = staticCompositionLocalOf<Shimmer> {
|
||||
error("No TangemShimmer provided")
|
||||
}
|
||||
|
||||
val LocalMainBottomSheetColor = staticCompositionLocalOf<MutableState<Color>> {
|
||||
error("No MainBottomSheetColor provided")
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
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>
|
||||
|
|
@ -24,7 +24,12 @@ internal object BlockchainInfoConverter : Converter<WalletManager, BlockchainInf
|
|||
addresses = value.wallet.mapAddresses(Address::value),
|
||||
explorerLinks = value.wallet.mapAddresses { value.wallet.getExploreUrl(it.value) },
|
||||
tokens = value.cardTokens.map { token ->
|
||||
BlockchainInfo.TokenInfo(id = token.id, name = token.name, contractAddress = token.contractAddress)
|
||||
BlockchainInfo.TokenInfo(
|
||||
id = token.id,
|
||||
name = token.name,
|
||||
contractAddress = token.contractAddress,
|
||||
decimals = token.decimals.toString(),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,10 @@ internal object CardInfoConverter : Converter<ScanResponse, CardInfo> {
|
|||
CardInfo(
|
||||
userWalletId = createUserWalletId(scanResponse = value),
|
||||
cardId = card.cardId,
|
||||
cardsCount = when (val status = value.card.backupStatus) {
|
||||
is CardDTO.BackupStatus.Active -> status.cardCount.toString()
|
||||
else -> "0"
|
||||
},
|
||||
firmwareVersion = card.firmwareVersion.stringValue,
|
||||
cardBlockchain = walletData?.blockchain,
|
||||
signedHashesList = card.wallets.map {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import com.tangem.domain.markets.repositories.MarketsTokenRepository
|
|||
import com.tangem.pagination.*
|
||||
import com.tangem.pagination.fetcher.LimitOffsetBatchFetcher
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
|
||||
internal class DefaultMarketsTokenRepository(
|
||||
|
|
@ -19,6 +20,7 @@ internal class DefaultMarketsTokenRepository(
|
|||
) : MarketsTokenRepository {
|
||||
|
||||
private val tokenListConverter = TokenMarketListConverter()
|
||||
private val tokenChartConverter = TokenChartConverter()
|
||||
|
||||
private fun createTokenMarketsFetcher(firstBatchSize: Int, nextBatchSize: Int) = LimitOffsetBatchFetcher(
|
||||
prefetchDistance = firstBatchSize,
|
||||
|
|
@ -41,9 +43,9 @@ internal class DefaultMarketsTokenRepository(
|
|||
interval = request.params.priceChangeInterval.toRequestParam(),
|
||||
order = request.params.order.toRequestParam(),
|
||||
search = searchText,
|
||||
generalCoins = request.params.showUnder100kMarketCapTokens.not(),
|
||||
offset = request.offset,
|
||||
limit = request.limit,
|
||||
timestamp = if (isFirstBatchFetching) null else requestTimeStamp.get(),
|
||||
).getOrThrow()
|
||||
}
|
||||
|
||||
|
|
@ -57,7 +59,7 @@ internal class DefaultMarketsTokenRepository(
|
|||
}
|
||||
|
||||
if (isFirstBatchFetching) {
|
||||
requestTimeStamp.set(0) // TODO when backend is ready
|
||||
requestTimeStamp.set(res.timestamp ?: 0)
|
||||
}
|
||||
|
||||
val last = res.tokens.size < request.limit
|
||||
|
|
@ -81,12 +83,28 @@ internal class DefaultMarketsTokenRepository(
|
|||
marketsApi = marketsApi,
|
||||
)
|
||||
|
||||
val atomicInteger = AtomicInteger(0)
|
||||
|
||||
return BatchListSource(
|
||||
fetchDispatcher = dispatcherProvider.io,
|
||||
context = batchingContext,
|
||||
generateNewKey = { it.size },
|
||||
generateNewKey = { atomicInteger.getAndIncrement() },
|
||||
batchFetcher = createTokenMarketsFetcher(firstBatchSize = firstBatchSize, nextBatchSize = nextBatchSize),
|
||||
updateFetcher = tokenMarketsUpdateFetcher,
|
||||
).toBatchFlow()
|
||||
}
|
||||
|
||||
override suspend fun getChart(
|
||||
fiatCurrencyCode: String,
|
||||
interval: PriceChangeInterval,
|
||||
tokenId: String,
|
||||
): TokenChart {
|
||||
val response = marketsApi.getCoinChart(
|
||||
currency = fiatCurrencyCode,
|
||||
coinId = tokenId,
|
||||
interval = interval.toRequestParam(),
|
||||
)
|
||||
|
||||
return tokenChartConverter.convert(interval, response.getOrThrow())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.data.markets
|
||||
|
||||
import com.tangem.data.markets.converters.TokenListChartConverter
|
||||
import com.tangem.data.markets.converters.TokenChartConverter
|
||||
import com.tangem.data.markets.converters.TokenMarketChartsConverter
|
||||
import com.tangem.data.markets.converters.TokenQuotesConverter
|
||||
import com.tangem.data.markets.converters.toRequestParam
|
||||
|
|
@ -21,7 +21,7 @@ internal class MarketsBatchUpdateFetcher(
|
|||
private val tangemTechApi: TangemTechApi,
|
||||
) : BatchUpdateFetcher<Int, List<TokenMarket>, TokenMarketUpdateRequest> {
|
||||
|
||||
private val tokenListChartsConverter = TokenMarketChartsConverter(TokenListChartConverter())
|
||||
private val tokenListChartsConverter = TokenMarketChartsConverter(TokenChartConverter())
|
||||
private val tokenQuotesConverter = TokenQuotesConverter()
|
||||
|
||||
override suspend fun BatchUpdateFetcher.UpdateContext<Int, List<TokenMarket>>.fetchUpdateAsync(
|
||||
|
|
|
|||
|
|
@ -4,13 +4,13 @@ import com.tangem.datasource.api.markets.models.response.TokenMarketChartRespons
|
|||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import com.tangem.domain.markets.TokenChart
|
||||
|
||||
class TokenListChartConverter {
|
||||
class TokenChartConverter {
|
||||
|
||||
fun convert(interval: PriceChangeInterval, value: TokenMarketChartResponse): TokenChart {
|
||||
return TokenChart(
|
||||
interval = interval,
|
||||
priceY = value.prices.values.toList(),
|
||||
timeStamp = value.prices.keys.toList(),
|
||||
timeStamps = value.prices.keys.toList(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -20,7 +20,7 @@ fun TokenMarketListConfig.Order.toRequestParam(): String = when (this) {
|
|||
fun PriceChangeInterval.toRequestParam(): String = when (this) {
|
||||
PriceChangeInterval.H24 -> "24h"
|
||||
PriceChangeInterval.WEEK -> "1w"
|
||||
PriceChangeInterval.MONTH -> "30d"
|
||||
PriceChangeInterval.MONTH -> "1m"
|
||||
PriceChangeInterval.MONTH3 -> "3m"
|
||||
PriceChangeInterval.MONTH6 -> "6m"
|
||||
PriceChangeInterval.YEAR -> "1y"
|
||||
|
|
|
|||
|
|
@ -3,31 +3,38 @@ package com.tangem.data.markets.converters
|
|||
import com.tangem.datasource.api.markets.models.response.TokenMarketChartListResponse
|
||||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import com.tangem.domain.markets.TokenMarket
|
||||
import com.tangem.domain.markets.TokenMarketListConfig
|
||||
|
||||
class TokenMarketChartsConverter(
|
||||
private val tokenListChartConverter: TokenListChartConverter,
|
||||
private val tokenChartConverter: TokenChartConverter,
|
||||
) {
|
||||
|
||||
fun convert(
|
||||
chartsToCopy: TokenMarket.Charts,
|
||||
tokenId: String,
|
||||
interval: PriceChangeInterval,
|
||||
interval: TokenMarketListConfig.Interval,
|
||||
value: TokenMarketChartListResponse,
|
||||
): TokenMarket.Charts {
|
||||
val prices = requireNotNull(value[tokenId]) {
|
||||
"$tokenId is not found in the response. This shouldn't have happened."
|
||||
}
|
||||
return when (interval) {
|
||||
PriceChangeInterval.H24 -> chartsToCopy.copy(
|
||||
h24 = tokenListChartConverter.convert(interval, prices),
|
||||
TokenMarketListConfig.Interval.H24 -> chartsToCopy.copy(
|
||||
h24 = tokenChartConverter.convert(interval.toPriceChangeInterval(), prices),
|
||||
)
|
||||
PriceChangeInterval.WEEK -> chartsToCopy.copy(
|
||||
week = tokenListChartConverter.convert(interval, prices),
|
||||
TokenMarketListConfig.Interval.WEEK -> chartsToCopy.copy(
|
||||
week = tokenChartConverter.convert(interval.toPriceChangeInterval(), prices),
|
||||
)
|
||||
PriceChangeInterval.MONTH -> chartsToCopy.copy(
|
||||
month = tokenListChartConverter.convert(interval, prices),
|
||||
TokenMarketListConfig.Interval.MONTH -> chartsToCopy.copy(
|
||||
month = tokenChartConverter.convert(interval.toPriceChangeInterval(), prices),
|
||||
)
|
||||
else -> error("unsupported interval=$interval. This shouldn't have happened.")
|
||||
}
|
||||
}
|
||||
|
||||
private fun TokenMarketListConfig.Interval.toPriceChangeInterval(): PriceChangeInterval = when (this) {
|
||||
TokenMarketListConfig.Interval.H24 -> PriceChangeInterval.H24
|
||||
TokenMarketListConfig.Interval.WEEK -> PriceChangeInterval.WEEK
|
||||
TokenMarketListConfig.Interval.MONTH -> PriceChangeInterval.MONTH
|
||||
}
|
||||
}
|
||||
|
|
@ -22,6 +22,7 @@ dependencies {
|
|||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.staking)
|
||||
implementation(projects.domain.wallets.models)
|
||||
implementation(projects.domain.legacy)
|
||||
|
||||
/** Feature Api modules */
|
||||
implementation(projects.features.staking.api)
|
||||
|
|
@ -40,6 +41,8 @@ dependencies {
|
|||
|
||||
|
||||
implementation(projects.libs.blockchainSdk)
|
||||
|
||||
implementation(deps.tangem.card.core)
|
||||
implementation(deps.tangem.blockchain) {
|
||||
exclude(module = "joda-time")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
package com.tangem.data.staking
|
||||
|
||||
import android.util.Base64
|
||||
import arrow.core.raise.catch
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchainsdk.utils.toCoinId
|
||||
import com.tangem.common.extensions.toCompressedPublicKey
|
||||
import com.tangem.data.common.cache.CacheRegistry
|
||||
import com.tangem.data.staking.converters.*
|
||||
import com.tangem.data.staking.converters.action.ActionStatusConverter
|
||||
|
|
@ -23,7 +25,10 @@ import com.tangem.datasource.local.token.StakingBalanceStore
|
|||
import com.tangem.datasource.local.token.StakingYieldsStore
|
||||
import com.tangem.domain.core.lce.LceFlow
|
||||
import com.tangem.domain.core.lce.lceFlow
|
||||
import com.tangem.domain.staking.model.*
|
||||
import com.tangem.domain.staking.model.StakingAvailability
|
||||
import com.tangem.domain.staking.model.StakingEntryInfo
|
||||
import com.tangem.domain.staking.model.UnsubmittedTransactionMetadata
|
||||
import com.tangem.domain.staking.model.stakekit.NetworkType
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import com.tangem.domain.staking.model.stakekit.YieldBalance
|
||||
import com.tangem.domain.staking.model.stakekit.YieldBalanceList
|
||||
|
|
@ -37,10 +42,11 @@ import com.tangem.domain.staking.repositories.StakingRepository
|
|||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyAddress
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.toFormattedString
|
||||
import com.tangem.utils.extensions.orZero
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -55,6 +61,7 @@ internal class DefaultStakingRepository(
|
|||
private val cacheRegistry: CacheRegistry,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val stakingFeatureToggle: StakingFeatureToggles,
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
) : StakingRepository {
|
||||
|
||||
private val stakingNetworkTypeConverter = StakingNetworkTypeConverter()
|
||||
|
|
@ -129,7 +136,7 @@ internal class DefaultStakingRepository(
|
|||
val yield = getYield(cryptoCurrencyId, symbol)
|
||||
|
||||
StakingEntryInfo(
|
||||
interestRate = yield.apy,
|
||||
interestRate = requireNotNull(yield.validators.maxByOrNull { it.apr.orZero() }?.apr),
|
||||
periodInDays = yield.metadata.cooldownPeriod.days,
|
||||
tokenSymbol = yield.token.symbol,
|
||||
)
|
||||
|
|
@ -160,12 +167,30 @@ internal class DefaultStakingRepository(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun createAction(params: ActionParams): StakingAction {
|
||||
override suspend fun createAction(
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
params: ActionParams,
|
||||
): StakingAction {
|
||||
return withContext(dispatchers.io) {
|
||||
val response = when (params.actionCommonType) {
|
||||
StakingActionCommonType.ENTER -> stakeKitApi.createEnterAction(createActionRequestBody(params))
|
||||
StakingActionCommonType.EXIT -> stakeKitApi.createExitAction(createActionRequestBody(params))
|
||||
StakingActionCommonType.PENDING -> stakeKitApi.createPendingAction(
|
||||
StakingActionCommonType.ENTER -> stakeKitApi.createEnterAction(
|
||||
createActionRequestBody(
|
||||
userWalletId,
|
||||
network,
|
||||
params,
|
||||
),
|
||||
)
|
||||
StakingActionCommonType.EXIT -> stakeKitApi.createExitAction(
|
||||
createActionRequestBody(
|
||||
userWalletId,
|
||||
network,
|
||||
params,
|
||||
),
|
||||
)
|
||||
StakingActionCommonType.PENDING_OTHER,
|
||||
StakingActionCommonType.PENDING_REWARDS,
|
||||
-> stakeKitApi.createPendingAction(
|
||||
createPendingActionRequestBody(params),
|
||||
)
|
||||
}
|
||||
|
|
@ -174,12 +199,30 @@ internal class DefaultStakingRepository(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun estimateGas(params: ActionParams): StakingGasEstimate {
|
||||
override suspend fun estimateGas(
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
params: ActionParams,
|
||||
): StakingGasEstimate {
|
||||
return withContext(dispatchers.io) {
|
||||
val gasEstimateDTO = when (params.actionCommonType) {
|
||||
StakingActionCommonType.ENTER -> stakeKitApi.estimateGasOnEnter(createActionRequestBody(params))
|
||||
StakingActionCommonType.EXIT -> stakeKitApi.estimateGasOnExit(createActionRequestBody(params))
|
||||
StakingActionCommonType.PENDING -> stakeKitApi.estimateGasOnPending(
|
||||
StakingActionCommonType.ENTER -> stakeKitApi.estimateGasOnEnter(
|
||||
createActionRequestBody(
|
||||
userWalletId,
|
||||
network,
|
||||
params,
|
||||
),
|
||||
)
|
||||
StakingActionCommonType.EXIT -> stakeKitApi.estimateGasOnExit(
|
||||
createActionRequestBody(
|
||||
userWalletId,
|
||||
network,
|
||||
params,
|
||||
),
|
||||
)
|
||||
StakingActionCommonType.PENDING_REWARDS,
|
||||
StakingActionCommonType.PENDING_OTHER,
|
||||
-> stakeKitApi.estimateGasOnPending(
|
||||
createPendingActionRequestBody(params),
|
||||
)
|
||||
}
|
||||
|
|
@ -435,12 +478,19 @@ internal class DefaultStakingRepository(
|
|||
}
|
||||
}
|
||||
|
||||
private fun createActionRequestBody(params: ActionParams): ActionRequestBody {
|
||||
private suspend fun createActionRequestBody(
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
params: ActionParams,
|
||||
): ActionRequestBody {
|
||||
return ActionRequestBody(
|
||||
integrationId = params.integrationId,
|
||||
addresses = Address(params.address),
|
||||
addresses = Address(
|
||||
address = params.address,
|
||||
additionalAddresses = createAdditionalAddresses(userWalletId, network, params),
|
||||
),
|
||||
args = ActionRequestBodyArgs(
|
||||
amount = params.amount.toFormattedString(params.token.decimals),
|
||||
amount = params.amount.toPlainString(),
|
||||
inputToken = tokenConverter.convertBack(params.token),
|
||||
validatorAddress = params.validatorAddress,
|
||||
),
|
||||
|
|
@ -453,12 +503,29 @@ internal class DefaultStakingRepository(
|
|||
type = params.type ?: StakingActionType.UNKNOWN,
|
||||
passthrough = params.passthrough.orEmpty(),
|
||||
args = ActionRequestBodyArgs(
|
||||
amount = params.amount.toFormattedString(params.token.decimals),
|
||||
amount = params.amount.toPlainString(),
|
||||
validatorAddress = params.validatorAddress,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun createAdditionalAddresses(
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
params: ActionParams,
|
||||
): Address.AdditionalAddresses? {
|
||||
val selectedWallet = walletManagersFacade.getOrCreateWalletManager(userWalletId, network)
|
||||
return when (params.token.network) {
|
||||
NetworkType.COSMOS -> Address.AdditionalAddresses(
|
||||
cosmosPubKey = Base64.encodeToString(
|
||||
/* input = */ selectedWallet?.wallet?.publicKey?.blockchainKey?.toCompressedPublicKey(),
|
||||
/* flags = */ Base64.NO_WRAP,
|
||||
),
|
||||
)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
override fun isStakeMoreAvailable(networkId: Network.ID): Boolean {
|
||||
val blockchain = Blockchain.fromId(networkId.value)
|
||||
return when (blockchain) {
|
||||
|
|
@ -502,23 +569,24 @@ internal class DefaultStakingRepository(
|
|||
const val AVALANCHE_INTEGRATION_ID = "avalanche-avax-native-staking"
|
||||
const val TRON_INTEGRATION_ID = "tron-trx-native-staking"
|
||||
const val CRONOS_INTEGRATION_ID = "cronos-cro-native-staking"
|
||||
const val BINANCE_INTEGRATION_ID = "binance-bnb-native-staking"
|
||||
const val BINANCE_INTEGRATION_ID = "bsc-bnb-native-staking"
|
||||
const val KAVA_INTEGRATION_ID = "kava-kava-native-staking"
|
||||
const val NEAR_INTEGRATION_ID = "near-near-native-staking"
|
||||
const val TEZOS_INTEGRATION_ID = "tezos-xtz-native-staking"
|
||||
|
||||
// uncomment items as implementation is ready
|
||||
val integrationIdMap = mapOf(
|
||||
Blockchain.Solana.toCoinId() to SOLANA_INTEGRATION_ID,
|
||||
Blockchain.Cosmos.toCoinId() to COSMOS_INTEGRATION_ID,
|
||||
Blockchain.Polkadot.toCoinId() to POLKADOT_INTEGRATION_ID,
|
||||
Blockchain.Polygon.toCoinId() to ETHEREUM_INTEGRATION_ID,
|
||||
Blockchain.Avalanche.toCoinId() to AVALANCHE_INTEGRATION_ID,
|
||||
Blockchain.Tron.toCoinId() to TRON_INTEGRATION_ID,
|
||||
Blockchain.Cronos.toCoinId() to CRONOS_INTEGRATION_ID,
|
||||
Blockchain.Binance.toCoinId() to BINANCE_INTEGRATION_ID,
|
||||
Blockchain.Kava.toCoinId() to KAVA_INTEGRATION_ID,
|
||||
Blockchain.Near.toCoinId() to NEAR_INTEGRATION_ID,
|
||||
Blockchain.Tezos.toCoinId() to TEZOS_INTEGRATION_ID,
|
||||
// Blockchain.Polkadot.toCoinId() to POLKADOT_INTEGRATION_ID,
|
||||
// Blockchain.Polygon.toCoinId() to ETHEREUM_INTEGRATION_ID,
|
||||
// Blockchain.Avalanche.toCoinId() to AVALANCHE_INTEGRATION_ID,
|
||||
// Blockchain.Tron.toCoinId() to TRON_INTEGRATION_ID,
|
||||
// Blockchain.Cronos.toCoinId() to CRONOS_INTEGRATION_ID,
|
||||
// Blockchain.BSC.toCoinId() to BINANCE_INTEGRATION_ID,
|
||||
// Blockchain.Kava.toCoinId() to KAVA_INTEGRATION_ID,
|
||||
// Blockchain.Near.toCoinId() to NEAR_INTEGRATION_ID,
|
||||
// Blockchain.Tezos.toCoinId() to TEZOS_INTEGRATION_ID,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -39,14 +39,18 @@ class YieldConverter(
|
|||
private fun convertEnter(enterDTO: YieldDTO.ArgsDTO.Enter): Yield.Args.Enter {
|
||||
return Yield.Args.Enter(
|
||||
addresses = convertAddresses(enterDTO.addresses),
|
||||
args = enterDTO.args.mapValues { convertAddressArgument(it.value) },
|
||||
args = enterDTO.args
|
||||
.mapKeys { convertArgType(it.key) }
|
||||
.mapValues { convertAddressArgument(it.value) },
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertAddresses(addressesDTO: YieldDTO.ArgsDTO.Enter.Addresses): Yield.Args.Enter.Addresses {
|
||||
return Yield.Args.Enter.Addresses(
|
||||
address = convertAddressArgument(addressesDTO.address),
|
||||
additionalAddresses = addressesDTO.additionalAddresses?.mapValues { convertAddressArgument(it.value) },
|
||||
additionalAddresses = addressesDTO.additionalAddresses
|
||||
?.mapKeys { convertArgType(it.key) }
|
||||
?.mapValues { convertAddressArgument(it.value) },
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -122,4 +126,12 @@ class YieldConverter(
|
|||
else -> Yield.RewardType.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
private fun convertArgType(value: String): Yield.Args.ArgType {
|
||||
return when (value) {
|
||||
"address" -> Yield.Args.ArgType.ADDRESS
|
||||
"amount" -> Yield.Args.ArgType.AMOUNT
|
||||
else -> Yield.Args.ArgType.UNKNOWN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ import com.tangem.datasource.local.token.StakingBalanceStore
|
|||
import com.tangem.datasource.local.token.StakingYieldsStore
|
||||
import com.tangem.domain.staking.repositories.StakingErrorResolver
|
||||
import com.tangem.domain.staking.repositories.StakingRepository
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
|
|
@ -35,6 +36,7 @@ internal object StakingDataModule {
|
|||
dispatchers: CoroutineDispatcherProvider,
|
||||
stakingFeatureToggle: StakingFeatureToggles,
|
||||
cacheRegistry: CacheRegistry,
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
): StakingRepository {
|
||||
return DefaultStakingRepository(
|
||||
stakeKitApi = stakeKitApi,
|
||||
|
|
@ -44,6 +46,7 @@ internal object StakingDataModule {
|
|||
dispatchers = dispatchers,
|
||||
cacheRegistry = cacheRegistry,
|
||||
stakingFeatureToggle = stakingFeatureToggle,
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ internal class DefaultNetworksRepository(
|
|||
|
||||
override fun isNeedToCreateAccountWithoutReserve(network: Network): Boolean {
|
||||
val blockchain = Blockchain.fromNetworkId(network.id.value)
|
||||
return blockchain == Blockchain.Aptos
|
||||
return REQUIRED_ACCOUNT_WITHOUT_RESERVE_BLOCKCHAINS.contains(blockchain)
|
||||
}
|
||||
|
||||
override suspend fun getNetworkAddresses(
|
||||
|
|
@ -345,4 +345,9 @@ internal class DefaultNetworksRepository(
|
|||
private fun getNetworksStatusesCacheKey(userWalletId: UserWalletId, network: Network): String {
|
||||
return "network_status_${userWalletId}_${network.id.value}_${network.derivationPath.value}"
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
val REQUIRED_ACCOUNT_WITHOUT_RESERVE_BLOCKCHAINS = listOf(Blockchain.Aptos, Blockchain.Filecoin)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,9 @@
|
|||
plugins {
|
||||
alias(deps.plugins.kotlin.jvm)
|
||||
alias(deps.plugins.kotlin.serialization)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(deps.kotlin.serialization)
|
||||
}
|
||||
|
|
@ -1,5 +1,8 @@
|
|||
package com.tangem.domain.appcurrency.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class AppCurrency(
|
||||
val code: String,
|
||||
val name: String,
|
||||
|
|
|
|||
|
|
@ -1,30 +1,140 @@
|
|||
package com.tangem.domain.card
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.extensions.ByteArrayKey
|
||||
import com.tangem.common.extensions.calculateRipemd160
|
||||
import com.tangem.common.extensions.calculateSha256
|
||||
import com.tangem.crypto.NetworkType
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
|
||||
import com.tangem.domain.card.repository.DerivationsRepository
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.operations.derivation.ExtendedPublicKeysMap
|
||||
|
||||
/**
|
||||
* Derivates an exteneded public key (xpub) based on blockchain hardened derivation
|
||||
*/
|
||||
class GetExtendedPublicKeyForCurrencyUseCase(
|
||||
private val derivationsRepository: DerivationsRepository,
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
) {
|
||||
suspend operator fun invoke(
|
||||
userWalletId: UserWalletId,
|
||||
derivation: Network.DerivationPath,
|
||||
): Either<Throwable, String> {
|
||||
suspend operator fun invoke(userWalletId: UserWalletId, network: Network): Either<Throwable, String> {
|
||||
return Either.catch {
|
||||
val derivationPath = requireNotNull(derivation.value?.let { DerivationPath(it) }) {
|
||||
error("Derivation is null")
|
||||
val userWallet = walletManagersFacade.getOrCreateWalletManager(userWalletId, network)
|
||||
?: error("Wallet not found")
|
||||
|
||||
val blockchain = Blockchain.fromId(network.id.value)
|
||||
val isSecp256k1Blockchain = Blockchain.secp256k1Blockchains(network.isTestnet).contains(blockchain)
|
||||
|
||||
val hdKey = if (isSecp256k1Blockchain) {
|
||||
userWallet.wallet.publicKey.derivationType?.hdKey ?: error("No derivation found")
|
||||
} else {
|
||||
error("No derivation found")
|
||||
}
|
||||
val hardenedNodes = derivationPath.nodes.filter { it.isHardened }
|
||||
val hardenedDerivation = DerivationPath(hardenedNodes)
|
||||
derivationsRepository.deriveExtendedPublicKey(userWalletId, hardenedDerivation)
|
||||
?.serialize(NetworkType.Mainnet).orEmpty()
|
||||
|
||||
var childKey = makeChildKey(
|
||||
isBip44DerivationStyleXPUB = blockchain.isBip44DerivationStyleXPUB(),
|
||||
extendedPublicKey = hdKey.extendedPublicKey,
|
||||
derivationPath = hdKey.path,
|
||||
)
|
||||
|
||||
var parentKey = Key(
|
||||
derivationPath = childKey.derivationPath.dropLastNodes(1),
|
||||
extendedPublicKey = null,
|
||||
)
|
||||
|
||||
val pendingDerivations = getPendingDerivations(childKey, parentKey)
|
||||
val derivedKeys = deriveKeys(
|
||||
userWalletId = userWalletId,
|
||||
seedKey = userWallet.wallet.publicKey.seedKey,
|
||||
paths = pendingDerivations,
|
||||
)
|
||||
|
||||
if (childKey.extendedPublicKey == null) {
|
||||
childKey = childKey.copy(
|
||||
extendedPublicKey = derivedKeys[childKey.derivationPath] ?: error("Failed to derive child key"),
|
||||
)
|
||||
}
|
||||
|
||||
if (parentKey.extendedPublicKey == null) {
|
||||
parentKey = parentKey.copy(
|
||||
extendedPublicKey = derivedKeys[parentKey.derivationPath] ?: error("Failed to derive parent key"),
|
||||
)
|
||||
}
|
||||
|
||||
makeExtendedKey(childKey, parentKey, network.isTestnet)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun deriveKeys(
|
||||
userWalletId: UserWalletId,
|
||||
seedKey: ByteArray,
|
||||
paths: MutableList<DerivationPath>,
|
||||
): ExtendedPublicKeysMap {
|
||||
val result = derivationsRepository.derivePublicKeys(userWalletId, mapOf(ByteArrayKey(seedKey) to paths))
|
||||
return result.getValue(ByteArrayKey(seedKey))
|
||||
}
|
||||
|
||||
private fun makeExtendedKey(childKey: Key, parentKey: Key, isTestnet: Boolean): String {
|
||||
val publicKey = childKey.extendedPublicKey?.publicKey ?: error("No public key found")
|
||||
val chainCode = childKey.extendedPublicKey.chainCode
|
||||
val lastChildNode = childKey.derivationPath.nodes.last()
|
||||
val parentPublicKey = parentKey.extendedPublicKey?.publicKey
|
||||
|
||||
val depth = childKey.derivationPath.nodes.size
|
||||
val childNumber = lastChildNode.index
|
||||
val parentFingerprint = parentPublicKey
|
||||
?.calculateSha256()?.calculateRipemd160()
|
||||
?.take(PARENT_FINGERPRINT_SIZE)?.toByteArray()
|
||||
?: error("No parent fingerprint found")
|
||||
|
||||
val net = if (isTestnet) NetworkType.Testnet else NetworkType.Mainnet
|
||||
return ExtendedPublicKey(
|
||||
publicKey = publicKey,
|
||||
chainCode = chainCode,
|
||||
depth = depth,
|
||||
parentFingerprint = parentFingerprint,
|
||||
childNumber = childNumber,
|
||||
).serialize(net)
|
||||
}
|
||||
|
||||
private fun getPendingDerivations(childKey: Key, parentKey: Key): MutableList<DerivationPath> {
|
||||
val pendingDerivations = mutableListOf<DerivationPath>()
|
||||
|
||||
if (childKey.extendedPublicKey == null) {
|
||||
pendingDerivations.add(childKey.derivationPath)
|
||||
}
|
||||
|
||||
if (parentKey.extendedPublicKey == null) {
|
||||
pendingDerivations.add(parentKey.derivationPath)
|
||||
}
|
||||
|
||||
return pendingDerivations
|
||||
}
|
||||
|
||||
private fun makeChildKey(
|
||||
isBip44DerivationStyleXPUB: Boolean,
|
||||
extendedPublicKey: ExtendedPublicKey,
|
||||
derivationPath: DerivationPath,
|
||||
): Key = if (isBip44DerivationStyleXPUB) {
|
||||
Key(derivationPath.dropLastNodes(2), null)
|
||||
} else {
|
||||
Key(derivationPath, extendedPublicKey)
|
||||
}
|
||||
|
||||
private fun DerivationPath.dropLastNodes(count: Int): DerivationPath {
|
||||
return DerivationPath(nodes.dropLast(count))
|
||||
}
|
||||
|
||||
private data class Key(
|
||||
val derivationPath: DerivationPath,
|
||||
val extendedPublicKey: ExtendedPublicKey?,
|
||||
)
|
||||
|
||||
private companion object {
|
||||
const val PARENT_FINGERPRINT_SIZE = 4
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
package com.tangem.domain.card.repository
|
||||
|
||||
import com.tangem.common.extensions.ByteArrayKey
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.operations.derivation.ExtendedPublicKeysMap
|
||||
|
||||
interface DerivationsRepository {
|
||||
|
||||
|
|
@ -11,5 +12,8 @@ interface DerivationsRepository {
|
|||
suspend fun derivePublicKeys(userWalletId: UserWalletId, currencies: List<CryptoCurrency>)
|
||||
|
||||
@Throws
|
||||
suspend fun deriveExtendedPublicKey(userWalletId: UserWalletId, derivation: DerivationPath): ExtendedPublicKey?
|
||||
suspend fun derivePublicKeys(
|
||||
userWalletId: UserWalletId,
|
||||
derivations: Map<ByteArrayKey, List<DerivationPath>>,
|
||||
): Map<ByteArrayKey, ExtendedPublicKeysMap>
|
||||
}
|
||||
|
|
@ -16,7 +16,8 @@ internal class FeedbackDataBuilder {
|
|||
fun addCardInfo(cardInfo: CardInfo) {
|
||||
builder.appendKeyValue("Card ID", cardInfo.cardId)
|
||||
builder.appendKeyValue("Firmware version", cardInfo.firmwareVersion)
|
||||
builder.appendKeyValue("Imported wallet", if (cardInfo.isImported) "yes" else "no")
|
||||
builder.appendKeyValue("Linked cards count:", cardInfo.cardsCount)
|
||||
builder.appendKeyValue("Has seed phrase:", cardInfo.isImported.toString())
|
||||
builder.appendKeyValue("Card Blockchain", cardInfo.cardBlockchain)
|
||||
builder.appendSignedHashes(cardInfo.signedHashesList)
|
||||
}
|
||||
|
|
@ -34,12 +35,14 @@ internal class FeedbackDataBuilder {
|
|||
builder.appendKeyValue("Outputs count", outputsCount)
|
||||
|
||||
if (tokens.isNotEmpty()) {
|
||||
builder.append("Tokens:")
|
||||
builder.breakLine()
|
||||
tokens.forEach { token ->
|
||||
builder.appendKeyValue("ID", token.id ?: "[custom token]")
|
||||
builder.appendKeyValue("Token ID", token.id ?: "[custom token]")
|
||||
builder.appendKeyValue("Name", token.name)
|
||||
builder.appendKeyValue("Contract address", token.contractAddress)
|
||||
builder.appendKeyValue("Decimals", token.decimals)
|
||||
|
||||
builder.breakLine()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,5 +24,6 @@ data class BlockchainInfo(
|
|||
val id: String?,
|
||||
val name: String,
|
||||
val contractAddress: String,
|
||||
val decimals: String,
|
||||
)
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ data class CardInfo(
|
|||
val userWalletId: UserWalletId?,
|
||||
val cardId: String,
|
||||
val firmwareVersion: String,
|
||||
val cardsCount: String,
|
||||
val cardBlockchain: String?,
|
||||
val signedHashesList: List<SignedHashes>,
|
||||
val isImported: Boolean,
|
||||
|
|
|
|||
|
|
@ -11,9 +11,10 @@ android {
|
|||
|
||||
|
||||
dependencies {
|
||||
api(projects.domain.markets.models)
|
||||
api(projects.domain.appCurrency.models)
|
||||
api(projects.domain.core)
|
||||
api(projects.core.pagination)
|
||||
api(projects.domain.markets.models)
|
||||
|
||||
implementation(deps.kotlin.serialization)
|
||||
implementation(projects.domain.tokens.models)
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ import java.math.BigDecimal
|
|||
data class TokenChart(
|
||||
val interval: PriceChangeInterval,
|
||||
val priceY: List<BigDecimal>,
|
||||
val timeStamp: List<Long>,
|
||||
val timeStamps: List<Long>,
|
||||
) {
|
||||
init {
|
||||
require(priceY.size == timeStamp.size)
|
||||
require(priceY.size == timeStamps.size)
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,6 @@ package com.tangem.domain.markets
|
|||
data class TokenMarketListConfig(
|
||||
val fiatPriceCurrency: String,
|
||||
val searchText: String?,
|
||||
val showUnder100kMarketCapTokens: Boolean,
|
||||
val priceChangeInterval: Interval,
|
||||
val order: Order,
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ sealed class TokenMarketUpdateRequest {
|
|||
) : TokenMarketUpdateRequest()
|
||||
|
||||
data class UpdateChart(
|
||||
val interval: PriceChangeInterval,
|
||||
val interval: TokenMarketListConfig.Interval,
|
||||
val currency: String,
|
||||
) : TokenMarketUpdateRequest()
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ class GetMarketsTokenListFlowUseCase(
|
|||
firstBatchSize = batchFlowType.firstBatchSize,
|
||||
nextBatchSize = batchFlowType.nextBatchSize,
|
||||
)
|
||||
// TODO listen quotes updates flow and update them in other parts of the application
|
||||
}
|
||||
|
||||
enum class BatchFlowType(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
package com.tangem.domain.markets
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.repositories.MarketsTokenRepository
|
||||
|
||||
class GetTokenPriceChartUseCase(
|
||||
private val marketsTokenRepository: MarketsTokenRepository,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(
|
||||
appCurrency: AppCurrency,
|
||||
interval: PriceChangeInterval,
|
||||
tokenId: String,
|
||||
): Either<Unit, TokenChart> {
|
||||
return Either.catch {
|
||||
marketsTokenRepository.getChart(
|
||||
fiatCurrencyCode = appCurrency.code,
|
||||
interval = interval,
|
||||
tokenId = tokenId,
|
||||
)
|
||||
}.mapLeft {}
|
||||
}
|
||||
}
|
||||
|
|
@ -9,4 +9,6 @@ interface MarketsTokenRepository {
|
|||
firstBatchSize: Int,
|
||||
nextBatchSize: Int,
|
||||
): TokenListBatchFlow
|
||||
|
||||
suspend fun getChart(fiatCurrencyCode: String, interval: PriceChangeInterval, tokenId: String): TokenChart
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue