Updated on 2026-08-14
This commit is contained in:
commit
ff09b9f79b
361 changed files with 10298 additions and 8064 deletions
|
|
@ -15,4 +15,7 @@ dependencies {
|
|||
/* Project - Core */
|
||||
implementation(projects.core.ui)
|
||||
implementation(projects.core.decompose)
|
||||
|
||||
/* Compose */
|
||||
implementation(deps.compose.runtime)
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.tangem.features.managetokens
|
||||
|
||||
interface ManageTokensToggles {
|
||||
val isFeatureEnabled: Boolean
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.features.managetokens.component
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
|
||||
interface AddCustomTokenComponent {
|
||||
|
||||
@Composable
|
||||
fun BottomSheet(isVisible: Boolean, onDismiss: () -> Unit)
|
||||
|
||||
data class Params(
|
||||
val userWalletId: UserWalletId,
|
||||
)
|
||||
|
||||
interface Factory {
|
||||
fun create(params: Params): AddCustomTokenComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -2,13 +2,11 @@ package com.tangem.features.managetokens.component
|
|||
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
|
||||
interface ManageTokensComponent : ComposableContentComponent {
|
||||
|
||||
data class Params(
|
||||
val userWalletId: UserWalletId,
|
||||
)
|
||||
data class Params(val mode: Mode)
|
||||
|
||||
enum class Mode { READ_ONLY, MANAGE, }
|
||||
interface Factory : ComponentFactory<Params, ManageTokensComponent>
|
||||
}
|
||||
|
|
@ -19,6 +19,11 @@ dependencies {
|
|||
implementation(projects.core.decompose)
|
||||
implementation(projects.core.ui)
|
||||
implementation(projects.common.routing)
|
||||
implementation(projects.core.featuretoggles)
|
||||
|
||||
/* Project - Domain */
|
||||
implementation(projects.domain.wallets.models)
|
||||
implementation(projects.domain.tokens.models)
|
||||
|
||||
/* AndroidX */
|
||||
implementation(deps.androidx.activity.compose)
|
||||
|
|
@ -28,6 +33,7 @@ dependencies {
|
|||
implementation(deps.compose.ui)
|
||||
implementation(deps.compose.ui.tooling)
|
||||
implementation(deps.compose.foundation)
|
||||
implementation(deps.compose.material) // For button colors
|
||||
implementation(deps.compose.material3)
|
||||
implementation(deps.compose.shimmer)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.features.managetokens
|
||||
|
||||
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
|
||||
|
||||
internal class DefaultManageTokensToggles(
|
||||
private val featureTogglesManager: FeatureTogglesManager,
|
||||
) : ManageTokensToggles {
|
||||
|
||||
override val isFeatureEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled("NEW_MANAGE_TOKENS")
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.tangem.features.managetokens.component
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
|
||||
internal interface CustomTokenFormComponent {
|
||||
|
||||
fun content(scope: LazyListScope)
|
||||
|
||||
data class Params(
|
||||
val userWalletId: UserWalletId,
|
||||
val networkId: Network.ID,
|
||||
)
|
||||
|
||||
interface Factory {
|
||||
fun create(params: Params): CustomTokenFormComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.features.managetokens.component
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.features.managetokens.entity.SelectedNetworkUM
|
||||
|
||||
internal interface CustomTokenNetworkSelectorComponent {
|
||||
|
||||
fun content(scope: LazyListScope)
|
||||
|
||||
data class Params(
|
||||
val userWalletId: UserWalletId,
|
||||
val selectedNetwork: SelectedNetworkUM?,
|
||||
val onNetworkSelected: (SelectedNetworkUM) -> Unit,
|
||||
)
|
||||
|
||||
interface Factory {
|
||||
fun create(params: Params): CustomTokenNetworkSelectorComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package com.tangem.features.managetokens.component.impl
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.features.managetokens.component.ManageTokensComponent
|
||||
import com.tangem.features.managetokens.model.ManageTokensModel
|
||||
import com.tangem.features.managetokens.ui.ManageTokensScreen
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
internal class DefaultManageTokensComponent @AssistedInject constructor(
|
||||
@Assisted context: AppComponentContext,
|
||||
@Assisted params: ManageTokensComponent.Params,
|
||||
) : ManageTokensComponent, AppComponentContext by context {
|
||||
|
||||
private val model: ManageTokensModel = getOrCreateModel(params)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.state.collectAsStateWithLifecycle()
|
||||
|
||||
ManageTokensScreen(
|
||||
modifier = modifier,
|
||||
state = state,
|
||||
)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : ManageTokensComponent.Factory {
|
||||
override fun create(
|
||||
context: AppComponentContext,
|
||||
params: ManageTokensComponent.Params,
|
||||
): DefaultManageTokensComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
package com.tangem.features.managetokens.component.preview
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.features.managetokens.component.AddCustomTokenComponent
|
||||
import com.tangem.features.managetokens.component.CustomTokenNetworkSelectorComponent
|
||||
import com.tangem.features.managetokens.entity.AddCustomTokenButtonUM
|
||||
import com.tangem.features.managetokens.entity.AddCustomTokenUM
|
||||
import com.tangem.features.managetokens.entity.ClickableFieldUM
|
||||
import com.tangem.features.managetokens.entity.SelectedNetworkUM
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
import com.tangem.features.managetokens.ui.AddCustomTokenBottomSheet
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
internal class PreviewAddCustomTokenComponent(
|
||||
initialState: AddCustomTokenUM = AddCustomTokenUM.NetworkSelector(popBack = {}),
|
||||
) : AddCustomTokenComponent {
|
||||
|
||||
private val userWalletId = UserWalletId(stringValue = "321")
|
||||
|
||||
private val previewState: MutableStateFlow<AddCustomTokenUM> = MutableStateFlow(initialState)
|
||||
|
||||
@Composable
|
||||
override fun BottomSheet(isVisible: Boolean, onDismiss: () -> Unit) {
|
||||
val state by previewState.collectAsStateWithLifecycle()
|
||||
val config = TangemBottomSheetConfig(
|
||||
isShow = isVisible,
|
||||
onDismissRequest = onDismiss,
|
||||
content = state,
|
||||
)
|
||||
|
||||
AddCustomTokenBottomSheet(
|
||||
config = config,
|
||||
content = {
|
||||
when (val s = state) {
|
||||
is AddCustomTokenUM.Form -> {
|
||||
PreviewCustomTokenFormComponent(
|
||||
networkName = ClickableFieldUM(
|
||||
label = resourceReference(R.string.custom_token_network_input_title),
|
||||
value = stringReference(s.selectedNetwork.name),
|
||||
onClick = { showNetworkSelector(s.selectedNetwork) },
|
||||
),
|
||||
).content(this)
|
||||
}
|
||||
is AddCustomTokenUM.NetworkSelector -> {
|
||||
PreviewCustomTokenNetworkSelectorComponent(
|
||||
params = CustomTokenNetworkSelectorComponent.Params(
|
||||
userWalletId = userWalletId,
|
||||
selectedNetwork = s.selectedNetwork,
|
||||
onNetworkSelected = ::showForm,
|
||||
),
|
||||
networksSize = 20,
|
||||
).content(this)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun showNetworkSelector(selectedNetwork: SelectedNetworkUM) {
|
||||
previewState.update {
|
||||
AddCustomTokenUM.NetworkSelector(selectedNetwork, popBack = { showForm(selectedNetwork) })
|
||||
}
|
||||
}
|
||||
|
||||
private fun showForm(network: SelectedNetworkUM) {
|
||||
previewState.update {
|
||||
AddCustomTokenUM.Form(
|
||||
popBack = {},
|
||||
selectedNetwork = network,
|
||||
addTokenButton = AddCustomTokenButtonUM.Visible(
|
||||
isEnabled = false,
|
||||
onClick = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
package com.tangem.features.managetokens.component.preview
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import com.tangem.core.ui.components.notifications.NotificationConfig
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.features.managetokens.component.CustomTokenFormComponent
|
||||
import com.tangem.features.managetokens.entity.ClickableFieldUM
|
||||
import com.tangem.features.managetokens.entity.CustomTokenFormUM
|
||||
import com.tangem.features.managetokens.entity.TextInputFieldUM
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
import com.tangem.features.managetokens.ui.customTokenFormContent
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
internal class PreviewCustomTokenFormComponent(
|
||||
networkName: ClickableFieldUM = ClickableFieldUM(
|
||||
label = resourceReference(R.string.custom_token_network_input_title),
|
||||
value = stringReference(value = "Ethereum"),
|
||||
onClick = {},
|
||||
),
|
||||
derivationPath: ClickableFieldUM = ClickableFieldUM(
|
||||
label = resourceReference(R.string.custom_token_derivation_path),
|
||||
value = stringReference(value = "Default"),
|
||||
onClick = {},
|
||||
),
|
||||
canAddToken: Boolean = false,
|
||||
contractAddress: TextInputFieldUM = TextInputFieldUM(
|
||||
label = resourceReference(R.string.custom_token_contract_address_input_title),
|
||||
placeholder = stringReference(value = "0x000000000000000000000000000"),
|
||||
value = "",
|
||||
onValueChange = {},
|
||||
),
|
||||
tokenName: TextInputFieldUM = TextInputFieldUM(
|
||||
label = resourceReference(R.string.custom_token_name_input_title),
|
||||
placeholder = stringReference(value = "E.g. USD Coin"),
|
||||
value = "",
|
||||
onValueChange = {},
|
||||
),
|
||||
tokenSymbol: TextInputFieldUM = TextInputFieldUM(
|
||||
label = resourceReference(R.string.custom_token_token_symbol_input_title),
|
||||
placeholder = stringReference(value = "E.g. USDC"),
|
||||
value = "",
|
||||
onValueChange = {},
|
||||
),
|
||||
tokenDecimals: TextInputFieldUM = TextInputFieldUM(
|
||||
label = resourceReference(R.string.custom_token_decimals_input_title),
|
||||
placeholder = stringReference(value = "8"),
|
||||
value = "",
|
||||
onValueChange = {},
|
||||
),
|
||||
notifications: ImmutableList<CustomTokenFormUM.NotificationUM> = persistentListOf(
|
||||
CustomTokenFormUM.NotificationUM(
|
||||
id = "1",
|
||||
config = NotificationConfig(
|
||||
title = stringReference(value = "Note that tokens can be created by anyone"),
|
||||
subtitle = stringReference(value = "Be aware of adding scam tokens, they can cost nothing"),
|
||||
iconResId = R.drawable.img_attention_20,
|
||||
),
|
||||
),
|
||||
),
|
||||
) : CustomTokenFormComponent {
|
||||
|
||||
private val previewState = CustomTokenFormUM(
|
||||
networkName = networkName,
|
||||
contractAddress = contractAddress,
|
||||
tokenName = tokenName,
|
||||
tokenSymbol = tokenSymbol,
|
||||
tokenDecimals = tokenDecimals,
|
||||
derivationPath = derivationPath,
|
||||
notifications = notifications,
|
||||
canAddToken = canAddToken,
|
||||
onDerivationPathClick = {},
|
||||
onNetworkClick = {},
|
||||
onAddClick = {},
|
||||
)
|
||||
|
||||
override fun content(scope: LazyListScope) {
|
||||
scope.customTokenFormContent(model = previewState)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
package com.tangem.features.managetokens.component.preview
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.features.managetokens.component.CustomTokenNetworkSelectorComponent
|
||||
import com.tangem.features.managetokens.entity.CurrencyNetworkUM
|
||||
import com.tangem.features.managetokens.entity.CustomTokenNetworkSelectorUM
|
||||
import com.tangem.features.managetokens.entity.SelectedNetworkUM
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
import com.tangem.features.managetokens.ui.customTokenNetworkSelectorContent
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
internal class PreviewCustomTokenNetworkSelectorComponent(
|
||||
private val params: CustomTokenNetworkSelectorComponent.Params = CustomTokenNetworkSelectorComponent.Params(
|
||||
userWalletId = UserWalletId(stringValue = "321"),
|
||||
selectedNetwork = null,
|
||||
onNetworkSelected = {},
|
||||
),
|
||||
networksSize: Int = 5,
|
||||
) : CustomTokenNetworkSelectorComponent {
|
||||
|
||||
private val previewNetworks = List(size = networksSize) { networkIndex ->
|
||||
val n = SelectedNetworkUM(
|
||||
id = Network.ID(networkIndex.toString()),
|
||||
name = "Network $networkIndex",
|
||||
)
|
||||
|
||||
CurrencyNetworkUM(
|
||||
id = n.id,
|
||||
name = n.name,
|
||||
type = "N$networkIndex",
|
||||
iconResId = R.drawable.ic_eth_16,
|
||||
isMainNetwork = false,
|
||||
isSelected = n.id == params.selectedNetwork?.id,
|
||||
onSelectedStateChange = { params.onNetworkSelected(n) },
|
||||
)
|
||||
}.toImmutableList()
|
||||
|
||||
private val previewState = CustomTokenNetworkSelectorUM(
|
||||
showTitle = params.selectedNetwork == null,
|
||||
networks = previewNetworks,
|
||||
)
|
||||
|
||||
override fun content(scope: LazyListScope) {
|
||||
scope.customTokenNetworkSelectorContent(
|
||||
model = previewState,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -6,15 +6,14 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.util.fastForEachIndexed
|
||||
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import com.tangem.core.ui.components.rows.model.BlockchainRowUM
|
||||
import com.tangem.core.ui.components.rows.model.ChainRowUM
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.features.managetokens.component.ManageTokensComponent
|
||||
import com.tangem.features.managetokens.entity.CurrencyItemUM
|
||||
import com.tangem.features.managetokens.entity.CurrencyNetworkUM
|
||||
import com.tangem.features.managetokens.entity.ManageTokensUM
|
||||
import com.tangem.features.managetokens.entity.*
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
import com.tangem.features.managetokens.ui.ManageTokensScreen
|
||||
import kotlinx.collections.immutable.mutate
|
||||
|
|
@ -28,11 +27,18 @@ internal class PreviewManageTokensComponent : ManageTokensComponent {
|
|||
private val changedItemsIds: MutableSet<String> = mutableSetOf()
|
||||
|
||||
private var items = initItems()
|
||||
|
||||
private val previewState = MutableStateFlow(
|
||||
value = ManageTokensUM(
|
||||
value = ManageTokensUM.ManageContent(
|
||||
popBack = {},
|
||||
items = items,
|
||||
topBar = ManageTokensTopBarUM.ManageContent(
|
||||
title = resourceReference(id = R.string.main_manage_tokens),
|
||||
onBackButtonClick = {},
|
||||
endButton = TopAppBarButtonUM(
|
||||
iconRes = R.drawable.ic_plus_24,
|
||||
onIconClicked = {},
|
||||
),
|
||||
),
|
||||
search = SearchBarUM(
|
||||
placeholderText = resourceReference(R.string.manage_tokens_search_placeholder),
|
||||
query = "",
|
||||
|
|
@ -41,8 +47,8 @@ internal class PreviewManageTokensComponent : ManageTokensComponent {
|
|||
onActiveChange = ::toggleSearchBar,
|
||||
),
|
||||
hasChanges = false,
|
||||
isLoading = false,
|
||||
onSaveClick = {},
|
||||
onAddCustomToken = {},
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -129,14 +135,11 @@ internal class PreviewManageTokensComponent : ManageTokensComponent {
|
|||
|
||||
private fun getCurrencyNetworks(currencyIndex: Int) = List(size = 3) { networkIndex ->
|
||||
CurrencyNetworkUM(
|
||||
id = networkIndex.toString(),
|
||||
model = BlockchainRowUM(
|
||||
name = "NETWORK$networkIndex",
|
||||
type = "N$networkIndex",
|
||||
iconResId = R.drawable.ic_eth_16,
|
||||
isMainNetwork = networkIndex == 0,
|
||||
isSelected = false,
|
||||
),
|
||||
id = Network.ID(networkIndex.toString()),
|
||||
name = "NETWORK$networkIndex",
|
||||
type = "N$networkIndex",
|
||||
iconResId = R.drawable.ic_eth_16,
|
||||
isMainNetwork = networkIndex == 0,
|
||||
isSelected = false,
|
||||
onSelectedStateChange = { toggleNetwork(currencyIndex, networkIndex, isSelected = it) },
|
||||
)
|
||||
|
|
@ -171,14 +174,11 @@ internal class PreviewManageTokensComponent : ManageTokensComponent {
|
|||
it.fastForEachIndexed { index, network ->
|
||||
if (index == networkIndex) {
|
||||
it[index] = network.copy(
|
||||
model = network.model.copy(
|
||||
iconResId = if (isSelected) {
|
||||
R.drawable.img_eth_22
|
||||
} else {
|
||||
R.drawable.ic_eth_16
|
||||
},
|
||||
isSelected = isSelected,
|
||||
),
|
||||
iconResId = if (isSelected) {
|
||||
R.drawable.img_eth_22
|
||||
} else {
|
||||
R.drawable.ic_eth_16
|
||||
},
|
||||
isSelected = isSelected,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.features.managetokens.di
|
||||
|
||||
import com.tangem.features.managetokens.component.ManageTokensComponent
|
||||
import com.tangem.features.managetokens.component.impl.DefaultManageTokensComponent
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface ComponentModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindManageTokensComponentFactory(factory: DefaultManageTokensComponent.Factory): ManageTokensComponent.Factory
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.features.managetokens.di
|
||||
|
||||
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
|
||||
import com.tangem.features.managetokens.DefaultManageTokensToggles
|
||||
import com.tangem.features.managetokens.ManageTokensToggles
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object FeatureModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideFeatureToggles(featureTogglesManager: FeatureTogglesManager): ManageTokensToggles =
|
||||
DefaultManageTokensToggles(featureTogglesManager = featureTogglesManager)
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.features.managetokens.di
|
||||
|
||||
import com.tangem.core.decompose.di.DecomposeComponent
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.features.managetokens.model.ManageTokensModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.multibindings.ClassKey
|
||||
import dagger.multibindings.IntoMap
|
||||
|
||||
@Module
|
||||
@InstallIn(DecomposeComponent::class)
|
||||
internal interface ModelModule {
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(ManageTokensModel::class)
|
||||
fun provideManageTokensModel(model: ManageTokensModel): Model
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
package com.tangem.features.managetokens.entity
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
|
||||
@Immutable
|
||||
internal sealed class AddCustomTokenUM : TangemBottomSheetConfigContent {
|
||||
|
||||
abstract val selectedNetwork: SelectedNetworkUM?
|
||||
abstract val addTokenButton: AddCustomTokenButtonUM
|
||||
|
||||
abstract val popBack: () -> Unit
|
||||
|
||||
data class NetworkSelector(
|
||||
override val selectedNetwork: SelectedNetworkUM? = null,
|
||||
override val popBack: () -> Unit,
|
||||
) : AddCustomTokenUM() {
|
||||
|
||||
override val addTokenButton: AddCustomTokenButtonUM = AddCustomTokenButtonUM.Hidden
|
||||
}
|
||||
|
||||
data class Form(
|
||||
override val selectedNetwork: SelectedNetworkUM,
|
||||
override val addTokenButton: AddCustomTokenButtonUM.Visible,
|
||||
override val popBack: () -> Unit,
|
||||
) : AddCustomTokenUM()
|
||||
}
|
||||
|
||||
@Immutable
|
||||
internal data class SelectedNetworkUM(
|
||||
val id: Network.ID,
|
||||
val name: String,
|
||||
)
|
||||
|
||||
@Immutable
|
||||
internal sealed class AddCustomTokenButtonUM {
|
||||
|
||||
open val onClick: () -> Unit = {}
|
||||
|
||||
open val isEnabled: Boolean = false
|
||||
|
||||
val isVisible: Boolean
|
||||
get() = this is Visible
|
||||
|
||||
data object Hidden : AddCustomTokenButtonUM() {
|
||||
override val onClick: () -> Unit = {}
|
||||
}
|
||||
|
||||
data class Visible(
|
||||
override val isEnabled: Boolean,
|
||||
override val onClick: () -> Unit,
|
||||
) : AddCustomTokenButtonUM()
|
||||
}
|
||||
|
|
@ -1,12 +1,15 @@
|
|||
package com.tangem.features.managetokens.entity
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.components.rows.model.BlockchainRowUM
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
|
||||
@Immutable
|
||||
internal data class CurrencyNetworkUM(
|
||||
val id: String,
|
||||
val model: BlockchainRowUM,
|
||||
val id: Network.ID,
|
||||
val name: String,
|
||||
val type: String,
|
||||
val iconResId: Int,
|
||||
val isMainNetwork: Boolean,
|
||||
val isSelected: Boolean,
|
||||
val onSelectedStateChange: (Boolean) -> Unit,
|
||||
)
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package com.tangem.features.managetokens.entity
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.components.notifications.NotificationConfig
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Immutable
|
||||
internal data class CustomTokenFormUM(
|
||||
val networkName: ClickableFieldUM,
|
||||
val contractAddress: TextInputFieldUM,
|
||||
val tokenName: TextInputFieldUM,
|
||||
val tokenSymbol: TextInputFieldUM,
|
||||
val tokenDecimals: TextInputFieldUM,
|
||||
val derivationPath: ClickableFieldUM,
|
||||
val notifications: ImmutableList<NotificationUM>,
|
||||
val canAddToken: Boolean,
|
||||
val onNetworkClick: () -> Unit,
|
||||
val onDerivationPathClick: () -> Unit,
|
||||
val onAddClick: () -> Unit,
|
||||
) {
|
||||
|
||||
@Immutable
|
||||
data class NotificationUM(
|
||||
val id: String,
|
||||
val config: NotificationConfig,
|
||||
)
|
||||
}
|
||||
|
||||
@Immutable
|
||||
internal data class TextInputFieldUM(
|
||||
val label: TextReference,
|
||||
val placeholder: TextReference,
|
||||
val value: String,
|
||||
val onValueChange: (String) -> Unit,
|
||||
val error: TextReference? = null,
|
||||
)
|
||||
|
||||
@Immutable
|
||||
internal data class ClickableFieldUM(
|
||||
val label: TextReference,
|
||||
val value: TextReference,
|
||||
val onClick: () -> Unit,
|
||||
)
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.features.managetokens.entity
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Immutable
|
||||
internal data class CustomTokenNetworkSelectorUM(
|
||||
val showTitle: Boolean,
|
||||
val networks: ImmutableList<CurrencyNetworkUM>,
|
||||
)
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package com.tangem.features.managetokens.entity
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
@Immutable
|
||||
internal sealed class ManageTokensTopBarUM {
|
||||
|
||||
abstract val title: TextReference
|
||||
abstract val onBackButtonClick: () -> Unit
|
||||
|
||||
data class ReadContent(
|
||||
override val title: TextReference,
|
||||
override val onBackButtonClick: () -> Unit,
|
||||
) : ManageTokensTopBarUM()
|
||||
|
||||
data class ManageContent(
|
||||
override val title: TextReference,
|
||||
override val onBackButtonClick: () -> Unit,
|
||||
val endButton: TopAppBarButtonUM,
|
||||
) : ManageTokensTopBarUM()
|
||||
}
|
||||
|
|
@ -5,11 +5,40 @@ import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
|||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Immutable
|
||||
internal data class ManageTokensUM(
|
||||
val popBack: () -> Unit,
|
||||
val items: ImmutableList<CurrencyItemUM>,
|
||||
val search: SearchBarUM,
|
||||
val hasChanges: Boolean,
|
||||
val onAddCustomToken: () -> Unit,
|
||||
val onSaveClick: () -> Unit,
|
||||
)
|
||||
internal sealed class ManageTokensUM {
|
||||
|
||||
abstract val popBack: () -> Unit
|
||||
abstract val isLoading: Boolean
|
||||
abstract val items: ImmutableList<CurrencyItemUM>
|
||||
abstract val topBar: ManageTokensTopBarUM
|
||||
abstract val search: SearchBarUM
|
||||
|
||||
data class ReadContent(
|
||||
override val popBack: () -> Unit,
|
||||
override val isLoading: Boolean,
|
||||
override val items: ImmutableList<CurrencyItemUM>,
|
||||
override val topBar: ManageTokensTopBarUM,
|
||||
override val search: SearchBarUM,
|
||||
) : ManageTokensUM()
|
||||
|
||||
data class ManageContent(
|
||||
override val popBack: () -> Unit,
|
||||
override val isLoading: Boolean,
|
||||
override val items: ImmutableList<CurrencyItemUM>,
|
||||
override val topBar: ManageTokensTopBarUM,
|
||||
override val search: SearchBarUM,
|
||||
val onSaveClick: () -> Unit,
|
||||
val hasChanges: Boolean,
|
||||
) : ManageTokensUM()
|
||||
|
||||
fun copySealed(
|
||||
search: SearchBarUM = this.search,
|
||||
items: ImmutableList<CurrencyItemUM> = this.items,
|
||||
hasChanges: Boolean = this is ManageContent && this.hasChanges,
|
||||
): ManageTokensUM {
|
||||
return when (this) {
|
||||
is ManageContent -> copy(search = search, items = items, hasChanges = hasChanges)
|
||||
is ReadContent -> copy(search = search, items = items)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,243 @@
|
|||
package com.tangem.features.managetokens.model
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.util.fastForEachIndexed
|
||||
import com.tangem.core.decompose.di.ComponentScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import com.tangem.core.ui.components.rows.model.ChainRowUM
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.features.managetokens.component.ManageTokensComponent
|
||||
import com.tangem.features.managetokens.entity.*
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.collections.immutable.mutate
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import javax.inject.Inject
|
||||
|
||||
@ComponentScoped
|
||||
internal class ManageTokensModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
private val router: Router,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
) : Model() {
|
||||
|
||||
private val params: ManageTokensComponent.Params = paramsContainer.require()
|
||||
private val changedItemsIds: MutableSet<String> = mutableSetOf()
|
||||
private var items = initItems()
|
||||
|
||||
val state: MutableStateFlow<ManageTokensUM> = MutableStateFlow(value = getInitialState(mode = params.mode))
|
||||
|
||||
private fun getInitialState(mode: ManageTokensComponent.Mode): ManageTokensUM {
|
||||
return when (mode) {
|
||||
ManageTokensComponent.Mode.READ_ONLY -> createReadContentModel()
|
||||
ManageTokensComponent.Mode.MANAGE -> createManageContentModel()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createReadContentModel(): ManageTokensUM.ReadContent {
|
||||
return ManageTokensUM.ReadContent(
|
||||
popBack = router::pop,
|
||||
isLoading = false,
|
||||
items = initItems(),
|
||||
topBar = ManageTokensTopBarUM.ReadContent(
|
||||
title = resourceReference(R.string.common_search_tokens),
|
||||
onBackButtonClick = router::pop,
|
||||
),
|
||||
search = SearchBarUM(
|
||||
placeholderText = resourceReference(R.string.manage_tokens_search_placeholder),
|
||||
query = "",
|
||||
onQueryChange = ::searchCurrencies,
|
||||
isActive = false,
|
||||
onActiveChange = ::toggleSearchBar,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createManageContentModel(): ManageTokensUM.ManageContent {
|
||||
return ManageTokensUM.ManageContent(
|
||||
popBack = router::pop,
|
||||
isLoading = false,
|
||||
items = initItems(),
|
||||
topBar = ManageTokensTopBarUM.ManageContent(
|
||||
title = resourceReference(id = R.string.main_manage_tokens),
|
||||
onBackButtonClick = router::pop,
|
||||
endButton = TopAppBarButtonUM(
|
||||
iconRes = R.drawable.ic_plus_24,
|
||||
onIconClicked = ::onAddCustomToken,
|
||||
),
|
||||
),
|
||||
search = SearchBarUM(
|
||||
placeholderText = resourceReference(R.string.manage_tokens_search_placeholder),
|
||||
query = "",
|
||||
onQueryChange = ::searchCurrencies,
|
||||
isActive = false,
|
||||
onActiveChange = ::toggleSearchBar,
|
||||
),
|
||||
onSaveClick = ::onSaveClick,
|
||||
hasChanges = false,
|
||||
)
|
||||
}
|
||||
|
||||
private fun onAddCustomToken() {
|
||||
// TODO: [REDACTED_JIRA]
|
||||
}
|
||||
|
||||
private fun onSaveClick() {
|
||||
// TODO: [REDACTED_JIRA]
|
||||
}
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
private fun searchCurrencies(query: String) {
|
||||
// TODO: [REDACTED_JIRA]
|
||||
val newItems = if (query.isBlank()) {
|
||||
initItems()
|
||||
} else {
|
||||
state.value.items.filter { currency ->
|
||||
currency.model.name.contains(query, ignoreCase = true)
|
||||
}.toPersistentList()
|
||||
}
|
||||
state.update { state ->
|
||||
state.copySealed(search = state.search.copy(query = query), items = newItems)
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleSearchBar(isActive: Boolean) {
|
||||
state.update { state ->
|
||||
state.copySealed(
|
||||
search = state.search.copy(isActive = isActive),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initItems() = List(size = 30) { index ->
|
||||
if (index < 2) {
|
||||
getCustomItem(index)
|
||||
} else {
|
||||
getBasicItem(index)
|
||||
}
|
||||
}.toPersistentList()
|
||||
|
||||
private fun getCustomItem(index: Int) = CurrencyItemUM.Custom(
|
||||
id = index.toString(),
|
||||
model = ChainRowUM(
|
||||
name = "Custom token $index",
|
||||
type = "CT$index",
|
||||
icon = CurrencyIconState.CustomTokenIcon(
|
||||
tint = Color.White,
|
||||
background = Color.Black,
|
||||
topBadgeIconResId = R.drawable.img_eth_22,
|
||||
isGrayscale = false,
|
||||
showCustomBadge = true,
|
||||
),
|
||||
showCustom = true,
|
||||
),
|
||||
onRemoveClick = {},
|
||||
)
|
||||
|
||||
private fun getBasicItem(index: Int) = CurrencyItemUM.Basic(
|
||||
id = index.toString(),
|
||||
model = ChainRowUM(
|
||||
name = "Currency $index",
|
||||
type = "C$index",
|
||||
icon = CurrencyIconState.CoinIcon(
|
||||
url = null,
|
||||
fallbackResId = R.drawable.img_btc_22,
|
||||
isGrayscale = false,
|
||||
showCustomBadge = false,
|
||||
),
|
||||
showCustom = false,
|
||||
),
|
||||
networks = if (index == 2) {
|
||||
CurrencyItemUM.Basic.NetworksUM.Expanded(getCurrencyNetworks(index))
|
||||
} else {
|
||||
CurrencyItemUM.Basic.NetworksUM.Collapsed
|
||||
},
|
||||
onExpandClick = { toggleCurrency(index) },
|
||||
)
|
||||
|
||||
private fun getCurrencyNetworks(currencyIndex: Int) = List(size = 3) { networkIndex ->
|
||||
CurrencyNetworkUM(
|
||||
id = Network.ID(networkIndex.toString()),
|
||||
name = "NETWORK$networkIndex",
|
||||
type = "N$networkIndex",
|
||||
iconResId = R.drawable.ic_eth_16,
|
||||
isMainNetwork = networkIndex == 0,
|
||||
isSelected = false,
|
||||
onSelectedStateChange = { toggleNetwork(currencyIndex, networkIndex, isSelected = it) },
|
||||
)
|
||||
}.toImmutableList()
|
||||
|
||||
private fun toggleCurrency(index: Int) {
|
||||
val updatedItem = when (val item = items[index]) {
|
||||
is CurrencyItemUM.Basic -> item.copy(
|
||||
networks = if (item.networks is CurrencyItemUM.Basic.NetworksUM.Collapsed) {
|
||||
CurrencyItemUM.Basic.NetworksUM.Expanded(getCurrencyNetworks(index))
|
||||
} else {
|
||||
CurrencyItemUM.Basic.NetworksUM.Collapsed
|
||||
},
|
||||
)
|
||||
is CurrencyItemUM.Custom -> return
|
||||
}
|
||||
|
||||
state.update { state ->
|
||||
items = items.mutate {
|
||||
it[index] = updatedItem
|
||||
}
|
||||
state.copySealed(items = items)
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleNetwork(currencyIndex: Int, networkIndex: Int, isSelected: Boolean) {
|
||||
val updatedItem = when (val item = items[currencyIndex]) {
|
||||
is CurrencyItemUM.Basic -> {
|
||||
val updatedNetworks = (item.networks as? CurrencyItemUM.Basic.NetworksUM.Expanded)
|
||||
?.copy(
|
||||
networks = item.networks.networks.toPersistentList().mutate {
|
||||
it.fastForEachIndexed { index, network ->
|
||||
if (index == networkIndex) {
|
||||
it[index] = network.copy(
|
||||
iconResId = if (isSelected) {
|
||||
R.drawable.img_eth_22
|
||||
} else {
|
||||
R.drawable.ic_eth_16
|
||||
},
|
||||
isSelected = isSelected,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
?: return
|
||||
|
||||
item.copy(networks = updatedNetworks)
|
||||
}
|
||||
is CurrencyItemUM.Custom -> return
|
||||
}
|
||||
|
||||
val id = "${currencyIndex}_$networkIndex"
|
||||
if (changedItemsIds.contains(id)) {
|
||||
changedItemsIds.remove(id)
|
||||
} else {
|
||||
changedItemsIds.add(id)
|
||||
}
|
||||
|
||||
state.update { state ->
|
||||
items = items.mutate {
|
||||
it[currencyIndex] = updatedItem
|
||||
}
|
||||
state.copySealed(
|
||||
items = items,
|
||||
hasChanges = changedItemsIds.isNotEmpty(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,188 @@
|
|||
package com.tangem.features.managetokens.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.material3.FabPosition
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.components.appbar.TangemTopAppBar
|
||||
import com.tangem.core.ui.components.appbar.TangemTopAppBarHeight
|
||||
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetTitle
|
||||
import com.tangem.core.ui.components.isOpened
|
||||
import com.tangem.core.ui.components.keyboardAsState
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.features.managetokens.component.AddCustomTokenComponent
|
||||
import com.tangem.features.managetokens.component.preview.PreviewAddCustomTokenComponent
|
||||
import com.tangem.features.managetokens.entity.AddCustomTokenButtonUM
|
||||
import com.tangem.features.managetokens.entity.AddCustomTokenUM
|
||||
import com.tangem.features.managetokens.entity.SelectedNetworkUM
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
|
||||
@Composable
|
||||
internal fun AddCustomTokenBottomSheet(config: TangemBottomSheetConfig, content: LazyListScope.() -> Unit) {
|
||||
TangemBottomSheet<AddCustomTokenUM>(
|
||||
config = config,
|
||||
title = { model ->
|
||||
Title(model)
|
||||
},
|
||||
containerColor = TangemTheme.colors.background.secondary,
|
||||
content = { model ->
|
||||
Content(
|
||||
model = model,
|
||||
content = content,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Title(model: AddCustomTokenUM, modifier: Modifier = Modifier) {
|
||||
val showTokenNetworkTitle = model is AddCustomTokenUM.NetworkSelector && model.selectedNetwork != null
|
||||
|
||||
if (showTokenNetworkTitle) {
|
||||
TangemTopAppBar(
|
||||
modifier = modifier,
|
||||
title = resourceReference(R.string.custom_token_network_selector_title),
|
||||
titleAlignment = Alignment.CenterHorizontally,
|
||||
startButton = TopAppBarButtonUM.Back(model.popBack),
|
||||
height = TangemTopAppBarHeight.BOTTOM_SHEET,
|
||||
)
|
||||
} else {
|
||||
TangemBottomSheetTitle(
|
||||
modifier = modifier,
|
||||
title = resourceReference(R.string.add_custom_token_title),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Content(model: AddCustomTokenUM, content: LazyListScope.() -> Unit, modifier: Modifier = Modifier) {
|
||||
val density = LocalDensity.current
|
||||
val keyboardState by keyboardAsState()
|
||||
|
||||
var fabHeight by remember { mutableStateOf(0.dp) }
|
||||
|
||||
Scaffold(
|
||||
modifier = modifier.imePadding(),
|
||||
containerColor = TangemTheme.colors.background.secondary,
|
||||
floatingActionButtonPosition = FabPosition.Center,
|
||||
floatingActionButton = {
|
||||
AnimatedVisibility(
|
||||
modifier = Modifier.onSizeChanged {
|
||||
fabHeight = with(density) { it.height.toDp() }
|
||||
},
|
||||
visible = model.addTokenButton.isVisible && !keyboardState.isOpened,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
label = "Add button visibility",
|
||||
) {
|
||||
PrimaryButton(
|
||||
modifier = Modifier
|
||||
.padding(bottom = TangemTheme.dimens.spacing16)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.fillMaxWidth(),
|
||||
text = stringResource(id = R.string.custom_token_add_token),
|
||||
enabled = model.addTokenButton.isEnabled,
|
||||
onClick = model.addTokenButton.onClick,
|
||||
)
|
||||
}
|
||||
},
|
||||
) { paddingValues ->
|
||||
LazyColumn(
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
contentPadding = PaddingValues(
|
||||
start = TangemTheme.dimens.spacing16,
|
||||
end = TangemTheme.dimens.spacing16,
|
||||
bottom = TangemTheme.dimens.spacing32 + fabHeight,
|
||||
),
|
||||
) {
|
||||
item {
|
||||
if (model is AddCustomTokenUM.NetworkSelector && model.selectedNetwork != null) {
|
||||
Spacer(modifier = Modifier.size(TangemTheme.dimens.spacing12))
|
||||
} else {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = TangemTheme.dimens.spacing16),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.fillMaxWidth(fraction = 0.7f),
|
||||
text = stringResource(id = R.string.custom_token_subtitle),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Composable
|
||||
@Preview(showBackground = true)
|
||||
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
private fun Preview_AddCustomTokenBottomSheet(
|
||||
@PreviewParameter(AddCustomTokenComponentPreviewProvider::class) component: AddCustomTokenComponent,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
component.BottomSheet(isVisible = true, onDismiss = {})
|
||||
}
|
||||
}
|
||||
|
||||
private class AddCustomTokenComponentPreviewProvider : PreviewParameterProvider<AddCustomTokenComponent> {
|
||||
override val values: Sequence<AddCustomTokenComponent>
|
||||
get() = sequenceOf(
|
||||
PreviewAddCustomTokenComponent(),
|
||||
PreviewAddCustomTokenComponent(
|
||||
initialState = AddCustomTokenUM.NetworkSelector(
|
||||
popBack = {},
|
||||
selectedNetwork = SelectedNetworkUM(
|
||||
id = Network.ID(value = "0"),
|
||||
name = "Ethereum",
|
||||
),
|
||||
),
|
||||
),
|
||||
PreviewAddCustomTokenComponent(
|
||||
initialState = AddCustomTokenUM.Form(
|
||||
popBack = {},
|
||||
selectedNetwork = SelectedNetworkUM(
|
||||
id = Network.ID(value = "1"),
|
||||
name = "Ethereum",
|
||||
),
|
||||
addTokenButton = AddCustomTokenButtonUM.Visible(
|
||||
isEnabled = false,
|
||||
onClick = {},
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
// endregion Preview
|
||||
|
|
@ -0,0 +1,196 @@
|
|||
package com.tangem.features.managetokens.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import com.tangem.core.ui.components.block.information.InformationBlock
|
||||
import com.tangem.core.ui.components.fields.SimpleTextField
|
||||
import com.tangem.core.ui.components.notifications.Notification
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.managetokens.component.preview.PreviewCustomTokenFormComponent
|
||||
import com.tangem.features.managetokens.entity.ClickableFieldUM
|
||||
import com.tangem.features.managetokens.entity.CustomTokenFormUM
|
||||
import com.tangem.features.managetokens.entity.TextInputFieldUM
|
||||
|
||||
internal fun LazyListScope.customTokenFormContent(model: CustomTokenFormUM) {
|
||||
item {
|
||||
ClickableField(
|
||||
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12),
|
||||
model = model.networkName,
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(bottom = TangemTheme.dimens.spacing12)
|
||||
.background(
|
||||
color = TangemTheme.colors.background.action,
|
||||
shape = TangemTheme.shapes.roundedCornersXMedium,
|
||||
),
|
||||
) {
|
||||
TextField(
|
||||
model = model.contractAddress,
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
imeAction = ImeAction.Next,
|
||||
),
|
||||
)
|
||||
TextField(
|
||||
model = model.tokenName,
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
imeAction = ImeAction.Next,
|
||||
),
|
||||
)
|
||||
TextField(
|
||||
model = model.tokenSymbol,
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
imeAction = ImeAction.Next,
|
||||
),
|
||||
)
|
||||
TextField(
|
||||
model = model.tokenDecimals,
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
keyboardType = KeyboardType.Decimal,
|
||||
imeAction = ImeAction.Next,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
ClickableField(
|
||||
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12),
|
||||
model = model.derivationPath,
|
||||
)
|
||||
}
|
||||
|
||||
items(
|
||||
items = model.notifications,
|
||||
key = { it.id },
|
||||
) { notification ->
|
||||
Notification(
|
||||
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12),
|
||||
config = notification.config,
|
||||
containerColor = TangemTheme.colors.button.disabled,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TextField(
|
||||
model: TextInputFieldUM,
|
||||
modifier: Modifier = Modifier,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
||||
) {
|
||||
InformationBlock(
|
||||
modifier = modifier,
|
||||
title = {
|
||||
val color by animateColorAsState(
|
||||
targetValue = if (model.error != null) {
|
||||
TangemTheme.colors.text.warning
|
||||
} else {
|
||||
TangemTheme.colors.text.tertiary
|
||||
},
|
||||
label = "Field label color",
|
||||
)
|
||||
|
||||
Text(
|
||||
text = (model.error ?: model.label).resolveReference(),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = color,
|
||||
)
|
||||
},
|
||||
content = {
|
||||
SimpleTextField(
|
||||
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12),
|
||||
value = model.value,
|
||||
onValueChange = model.onValueChange,
|
||||
readOnly = false,
|
||||
placeholder = model.placeholder,
|
||||
singleLine = true,
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = keyboardActions,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ClickableField(model: ClickableFieldUM, modifier: Modifier = Modifier) {
|
||||
InformationBlock(
|
||||
modifier = modifier
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.clickable(onClick = model.onClick),
|
||||
title = {
|
||||
Text(
|
||||
text = model.label.resolveReference(),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
},
|
||||
content = {
|
||||
Text(
|
||||
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12),
|
||||
text = model.value.resolveReference(),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Composable
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
private fun Preview_CustomTokenFormContent(
|
||||
@PreviewParameter(PreviewCustomTokenFormComponentProvider::class)
|
||||
component: PreviewCustomTokenFormComponent,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
LazyColumn(
|
||||
modifier = Modifier.background(color = TangemTheme.colors.background.secondary),
|
||||
) { component.content(scope = this) }
|
||||
}
|
||||
}
|
||||
|
||||
private class PreviewCustomTokenFormComponentProvider :
|
||||
PreviewParameterProvider<PreviewCustomTokenFormComponent> {
|
||||
|
||||
override val values: Sequence<PreviewCustomTokenFormComponent>
|
||||
get() = sequenceOf(
|
||||
PreviewCustomTokenFormComponent(),
|
||||
PreviewCustomTokenFormComponent(
|
||||
contractAddress = TextInputFieldUM(
|
||||
label = stringReference("Contract address"),
|
||||
value = "0x1234567890",
|
||||
error = stringReference("Contract address is invalid"),
|
||||
placeholder = stringReference("0x1234567890"),
|
||||
onValueChange = {},
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
// endregion Preview
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
package com.tangem.features.managetokens.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
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.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.components.rows.ChainRow
|
||||
import com.tangem.core.ui.components.rows.model.ChainRowUM
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.features.managetokens.component.CustomTokenNetworkSelectorComponent
|
||||
import com.tangem.features.managetokens.component.preview.PreviewCustomTokenNetworkSelectorComponent
|
||||
import com.tangem.features.managetokens.entity.CurrencyNetworkUM
|
||||
import com.tangem.features.managetokens.entity.CustomTokenNetworkSelectorUM
|
||||
import com.tangem.features.managetokens.entity.SelectedNetworkUM
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
|
||||
internal fun LazyListScope.customTokenNetworkSelectorContent(model: CustomTokenNetworkSelectorUM) {
|
||||
val lastIndex = model.networks.lastIndex
|
||||
|
||||
if (model.showTitle) {
|
||||
item {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = TangemTheme.dimens.size36)
|
||||
.background(
|
||||
color = TangemTheme.colors.background.primary,
|
||||
shape = TangemTheme.shapes.bottomSheet,
|
||||
),
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
top = TangemTheme.dimens.spacing12,
|
||||
bottom = TangemTheme.dimens.spacing6,
|
||||
)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing12),
|
||||
text = stringResource(R.string.add_custom_token_choose_network),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
itemsIndexed(
|
||||
items = model.networks,
|
||||
key = { _, item -> item.id.value },
|
||||
) { index, item ->
|
||||
NetworkItem(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(
|
||||
shape = when {
|
||||
!model.showTitle && index == 0 -> RoundedCornerShape(
|
||||
topStart = TangemTheme.dimens.radius16,
|
||||
topEnd = TangemTheme.dimens.radius16,
|
||||
)
|
||||
index == lastIndex -> RoundedCornerShape(
|
||||
bottomStart = TangemTheme.dimens.radius16,
|
||||
bottomEnd = TangemTheme.dimens.radius16,
|
||||
)
|
||||
else -> RectangleShape
|
||||
},
|
||||
)
|
||||
.background(color = TangemTheme.colors.background.primary)
|
||||
.clickable(onClick = { item.onSelectedStateChange(true) })
|
||||
.padding(horizontal = TangemTheme.dimens.spacing4),
|
||||
model = item,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NetworkItem(model: CurrencyNetworkUM, modifier: Modifier = Modifier) {
|
||||
ChainRow(
|
||||
modifier = modifier,
|
||||
model = with(model) {
|
||||
ChainRowUM(
|
||||
name = name,
|
||||
type = type,
|
||||
icon = CurrencyIconState.CoinIcon(
|
||||
url = null,
|
||||
fallbackResId = model.iconResId,
|
||||
isGrayscale = false,
|
||||
showCustomBadge = false,
|
||||
),
|
||||
showCustom = false,
|
||||
)
|
||||
},
|
||||
action = {
|
||||
AnimatedVisibility(
|
||||
modifier = Modifier.size(TangemTheme.dimens.size24),
|
||||
visible = model.isSelected,
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_check_24),
|
||||
tint = TangemTheme.colors.icon.accent,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Composable
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
private fun Preview_CustomTokenNetworkSelectorContent(
|
||||
@PreviewParameter(CustomTokenNetworkSelectorComponentPreviewProvider::class)
|
||||
component: CustomTokenNetworkSelectorComponent,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
LazyColumn {
|
||||
component.content(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class CustomTokenNetworkSelectorComponentPreviewProvider :
|
||||
PreviewParameterProvider<CustomTokenNetworkSelectorComponent> {
|
||||
override val values: Sequence<CustomTokenNetworkSelectorComponent>
|
||||
get() = sequenceOf(
|
||||
PreviewCustomTokenNetworkSelectorComponent(),
|
||||
PreviewCustomTokenNetworkSelectorComponent(
|
||||
params = CustomTokenNetworkSelectorComponent.Params(
|
||||
userWalletId = UserWalletId(stringValue = "321"),
|
||||
selectedNetwork = SelectedNetworkUM(
|
||||
id = Network.ID(value = "0"),
|
||||
name = "",
|
||||
),
|
||||
onNetworkSelected = {},
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
// endregion Preview
|
||||
|
|
@ -10,6 +10,7 @@ import androidx.compose.foundation.clickable
|
|||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.FabPosition
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Scaffold
|
||||
|
|
@ -35,12 +36,15 @@ import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
|||
import com.tangem.core.ui.components.rows.ArrowRow
|
||||
import com.tangem.core.ui.components.rows.BlockchainRow
|
||||
import com.tangem.core.ui.components.rows.ChainRow
|
||||
import com.tangem.core.ui.components.rows.model.BlockchainRowUM
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.managetokens.component.preview.PreviewManageTokensComponent
|
||||
import com.tangem.features.managetokens.entity.CurrencyItemUM
|
||||
import com.tangem.features.managetokens.entity.CurrencyItemUM.Basic.NetworksUM
|
||||
import com.tangem.features.managetokens.entity.ManageTokensTopBarUM
|
||||
import com.tangem.features.managetokens.entity.ManageTokensUM
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
|
@ -56,14 +60,9 @@ internal fun ManageTokensScreen(state: ManageTokensUM, modifier: Modifier = Modi
|
|||
modifier = modifier,
|
||||
containerColor = TangemTheme.colors.background.primary,
|
||||
topBar = {
|
||||
TangemTopAppBar(
|
||||
ManageTokensTopBar(
|
||||
modifier = Modifier.statusBarsPadding(),
|
||||
title = stringResource(id = R.string.main_manage_tokens),
|
||||
startButton = TopAppBarButtonUM.Back(state.popBack),
|
||||
endButton = TopAppBarButtonUM(
|
||||
iconRes = R.drawable.ic_plus_24,
|
||||
onIconClicked = state.onAddCustomToken,
|
||||
),
|
||||
topBar = state.topBar,
|
||||
)
|
||||
},
|
||||
content = { innerPadding ->
|
||||
|
|
@ -71,18 +70,36 @@ internal fun ManageTokensScreen(state: ManageTokensUM, modifier: Modifier = Modi
|
|||
modifier = Modifier
|
||||
.padding(innerPadding)
|
||||
.fillMaxSize(),
|
||||
state = state,
|
||||
search = state.search,
|
||||
items = state.items,
|
||||
isLoading = state.isLoading,
|
||||
hasChanges = state is ManageTokensUM.ManageContent && state.hasChanges,
|
||||
)
|
||||
},
|
||||
floatingActionButtonPosition = FabPosition.Center,
|
||||
floatingActionButton = {
|
||||
SaveChangesButton(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.fillMaxWidth(),
|
||||
isVisible = state.hasChanges,
|
||||
onClick = state.onSaveClick,
|
||||
)
|
||||
if (state is ManageTokensUM.ManageContent) {
|
||||
SaveChangesButton(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.fillMaxWidth(),
|
||||
isVisible = state.hasChanges,
|
||||
onClick = state.onSaveClick,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ManageTokensTopBar(topBar: ManageTokensTopBarUM, modifier: Modifier = Modifier) {
|
||||
TangemTopAppBar(
|
||||
modifier = modifier,
|
||||
title = topBar.title.resolveReference(),
|
||||
startButton = TopAppBarButtonUM.Back(topBar.onBackButtonClick),
|
||||
endButton = when (topBar) {
|
||||
is ManageTokensTopBarUM.ManageContent -> topBar.endButton
|
||||
is ManageTokensTopBarUM.ReadContent -> null
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -105,24 +122,48 @@ private fun SaveChangesButton(isVisible: Boolean, onClick: () -> Unit, modifier:
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun Content(state: ManageTokensUM, modifier: Modifier = Modifier) {
|
||||
private fun LoadingContent() {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(color = TangemTheme.colors.background.primary),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
CircularProgressIndicator(color = TangemTheme.colors.icon.accent)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Content(
|
||||
search: SearchBarUM,
|
||||
items: ImmutableList<CurrencyItemUM>,
|
||||
isLoading: Boolean,
|
||||
hasChanges: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(modifier = modifier) {
|
||||
Currencies(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
items = state.items,
|
||||
search = state.search,
|
||||
items = items,
|
||||
search = search,
|
||||
)
|
||||
|
||||
AnimatedVisibility(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.fillMaxWidth(),
|
||||
visible = state.hasChanges,
|
||||
visible = hasChanges,
|
||||
label = "bottom_fade_visibility",
|
||||
) {
|
||||
BottomFade()
|
||||
}
|
||||
}
|
||||
|
||||
Crossfade(targetState = isLoading, label = "ManageTokensLoadingContent") {
|
||||
if (it) {
|
||||
LoadingContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
|
|
@ -245,7 +286,15 @@ private fun NetworksList(networks: NetworksUM, currencyId: String, modifier: Mod
|
|||
isLastItem = index == currentItems.lastIndex,
|
||||
content = {
|
||||
BlockchainRow(
|
||||
model = network.model,
|
||||
model = with(network) {
|
||||
BlockchainRowUM(
|
||||
name = name,
|
||||
type = type,
|
||||
iconResId = iconResId,
|
||||
isMainNetwork = isMainNetwork,
|
||||
isSelected = isSelected,
|
||||
)
|
||||
},
|
||||
action = {
|
||||
TangemSwitch(
|
||||
checked = network.isSelected,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import androidx.compose.ui.unit.Dp
|
|||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
|
||||
@Stable
|
||||
interface MarketsListComponent {
|
||||
interface MarketsEntryComponent {
|
||||
|
||||
@Composable
|
||||
fun BottomSheetContent(
|
||||
|
|
@ -18,6 +18,6 @@ interface MarketsListComponent {
|
|||
)
|
||||
|
||||
interface Factory {
|
||||
fun create(context: AppComponentContext): MarketsListComponent
|
||||
fun create(context: AppComponentContext): MarketsEntryComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@ android {
|
|||
dependencies {
|
||||
/* Project - API */
|
||||
api(projects.features.markets.api)
|
||||
implementation(projects.core.navigation)
|
||||
|
||||
/* Domain */
|
||||
implementation(projects.domain.markets)
|
||||
|
|
@ -23,6 +24,7 @@ dependencies {
|
|||
/* Compose */
|
||||
implementation(deps.compose.coil)
|
||||
implementation(deps.compose.foundation)
|
||||
implementation(deps.compose.material)
|
||||
implementation(deps.compose.material3)
|
||||
implementation(deps.compose.ui)
|
||||
implementation(deps.compose.ui.tooling)
|
||||
|
|
@ -37,6 +39,7 @@ dependencies {
|
|||
/* Other */
|
||||
implementation(deps.kotlin.immutable.collections)
|
||||
implementation(deps.timber)
|
||||
implementation(deps.decompose.ext.compose)
|
||||
|
||||
/* Core */
|
||||
implementation(projects.core.decompose)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,162 @@
|
|||
package com.tangem.features.markets
|
||||
|
||||
import androidx.compose.animation.Animatable
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import com.arkivanov.decompose.ExperimentalDecomposeApi
|
||||
import com.arkivanov.decompose.extensions.compose.jetpack.stack.Children
|
||||
import com.arkivanov.decompose.extensions.compose.jetpack.stack.animation.*
|
||||
import com.arkivanov.decompose.extensions.compose.jetpack.subscribeAsState
|
||||
import com.arkivanov.decompose.router.stack.*
|
||||
import com.arkivanov.decompose.value.Value
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.context.childByContext
|
||||
import com.tangem.core.ui.res.LocalMainBottomSheetColor
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.TokenMarket
|
||||
import com.tangem.features.markets.component.BottomSheetState
|
||||
import com.tangem.features.markets.component.MarketsEntryComponent
|
||||
import com.tangem.features.markets.details.api.MarketsTokenDetailsComponent
|
||||
import com.tangem.features.markets.details.api.toSerializable
|
||||
import com.tangem.features.markets.tokenlist.api.MarketsTokenListComponent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
@Stable
|
||||
internal class DefaultMarketsEntryComponent @AssistedInject constructor(
|
||||
@Assisted context: AppComponentContext,
|
||||
private val marketsEntryChildFactory: MarketsEntryChildFactory,
|
||||
) : MarketsEntryComponent, AppComponentContext by context {
|
||||
|
||||
private val stackNavigation = StackNavigation<MarketsEntryChildFactory.Child>()
|
||||
|
||||
val stack: Value<ChildStack<MarketsEntryChildFactory.Child, Any>> = childStack(
|
||||
key = "main",
|
||||
source = stackNavigation,
|
||||
serializer = MarketsEntryChildFactory.Child.serializer(),
|
||||
initialConfiguration = MarketsEntryChildFactory.Child.TokenList,
|
||||
handleBackButton = true,
|
||||
childFactory = { configuration, componentContext ->
|
||||
marketsEntryChildFactory.createChild(
|
||||
child = configuration,
|
||||
appComponentContext = childByContext(componentContext),
|
||||
onTokenSelected = ::marketsListTokenSelected,
|
||||
onDetailsBack = ::onDetailsBack,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
override fun BottomSheetContent(
|
||||
bottomSheetState: State<BottomSheetState>,
|
||||
onHeaderSizeChange: (Dp) -> Unit,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
val primary = TangemTheme.colors.background.primary
|
||||
val secondary = TangemTheme.colors.background.secondary
|
||||
val backgroundColor = remember { Animatable(primary) }
|
||||
val stackState = stack.subscribeAsState()
|
||||
|
||||
LocalMainBottomSheetColor.current.value = backgroundColor.value
|
||||
|
||||
Children(
|
||||
stack = stackState.value,
|
||||
animation = stackAnimation(slide()),
|
||||
) {
|
||||
when (it.configuration) {
|
||||
is MarketsEntryChildFactory.Child.TokenDetails -> {
|
||||
(it.instance as MarketsTokenDetailsComponent).BottomSheetContent(
|
||||
bottomSheetState = bottomSheetState,
|
||||
onHeaderSizeChange = onHeaderSizeChange,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
MarketsEntryChildFactory.Child.TokenList -> {
|
||||
(it.instance as MarketsTokenListComponent).BottomSheetContent(
|
||||
bottomSheetState = bottomSheetState,
|
||||
onHeaderSizeChange = onHeaderSizeChange,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val activeChild = stackState.value.active.configuration
|
||||
|
||||
LaunchedEffect(bottomSheetState.value) {
|
||||
if (activeChild is MarketsEntryChildFactory.Child.TokenDetails) {
|
||||
when (bottomSheetState.value) {
|
||||
BottomSheetState.EXPANDED -> {
|
||||
backgroundColor.animateTo(
|
||||
secondary,
|
||||
animationSpec = tween(durationMillis = 100),
|
||||
)
|
||||
}
|
||||
BottomSheetState.COLLAPSED -> {
|
||||
backgroundColor.animateTo(
|
||||
primary,
|
||||
animationSpec = tween(durationMillis = 100),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(activeChild) {
|
||||
when (activeChild) {
|
||||
is MarketsEntryChildFactory.Child.TokenDetails -> {
|
||||
backgroundColor.animateTo(
|
||||
secondary,
|
||||
animationSpec = tween(durationMillis = 500),
|
||||
)
|
||||
}
|
||||
MarketsEntryChildFactory.Child.TokenList -> {
|
||||
backgroundColor.animateTo(
|
||||
primary,
|
||||
animationSpec = tween(durationMillis = 500),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(primary, secondary) {
|
||||
if (backgroundColor.isRunning) return@LaunchedEffect
|
||||
|
||||
when (activeChild) {
|
||||
is MarketsEntryChildFactory.Child.TokenDetails -> {
|
||||
backgroundColor.snapTo(secondary)
|
||||
}
|
||||
MarketsEntryChildFactory.Child.TokenList -> {
|
||||
backgroundColor.snapTo(primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalDecomposeApi::class)
|
||||
private fun marketsListTokenSelected(token: TokenMarket, appCurrency: AppCurrency) {
|
||||
stackNavigation.pushNew(
|
||||
configuration = MarketsEntryChildFactory.Child.TokenDetails(
|
||||
params = MarketsTokenDetailsComponent.Params(
|
||||
token = token.toSerializable(),
|
||||
appCurrency = appCurrency,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun onDetailsBack() {
|
||||
stackNavigation.popWhile { it != MarketsEntryChildFactory.Child.TokenList }
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : MarketsEntryComponent.Factory {
|
||||
override fun create(context: AppComponentContext): DefaultMarketsEntryComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
package com.tangem.features.markets
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.TokenMarket
|
||||
import com.tangem.features.markets.details.api.MarketsTokenDetailsComponent
|
||||
import com.tangem.features.markets.tokenlist.api.MarketsTokenListComponent
|
||||
import kotlinx.serialization.Serializable
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class MarketsEntryChildFactory @Inject constructor(
|
||||
private val tokenListComponentFactory: MarketsTokenListComponent.Factory,
|
||||
private val tokenDetailsComponentFactory: MarketsTokenDetailsComponent.Factory,
|
||||
) {
|
||||
|
||||
@Serializable
|
||||
@Immutable
|
||||
sealed interface Child {
|
||||
|
||||
@Serializable
|
||||
@Immutable
|
||||
data object TokenList : Child
|
||||
|
||||
@Serializable
|
||||
@Immutable
|
||||
data class TokenDetails(val params: MarketsTokenDetailsComponent.Params) : Child
|
||||
}
|
||||
|
||||
fun createChild(
|
||||
child: Child,
|
||||
appComponentContext: AppComponentContext,
|
||||
onTokenSelected: (TokenMarket, AppCurrency) -> Unit,
|
||||
onDetailsBack: () -> Unit,
|
||||
): Any {
|
||||
return when (child) {
|
||||
is Child.TokenDetails -> {
|
||||
tokenDetailsComponentFactory.create(
|
||||
context = appComponentContext,
|
||||
params = child.params,
|
||||
onBack = onDetailsBack,
|
||||
)
|
||||
}
|
||||
is Child.TokenList -> {
|
||||
tokenListComponentFactory.create(
|
||||
context = appComponentContext,
|
||||
onTokenSelected = onTokenSelected,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
package com.tangem.features.markets.component.impl
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.features.markets.component.BottomSheetState
|
||||
import com.tangem.features.markets.component.MarketsListComponent
|
||||
import com.tangem.features.markets.model.MarketsListModel
|
||||
import com.tangem.features.markets.ui.MarketsList
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
internal class DefaultMarketsListComponent @AssistedInject constructor(
|
||||
@Assisted context: AppComponentContext,
|
||||
) : MarketsListComponent, AppComponentContext by context {
|
||||
|
||||
private val model: MarketsListModel = getOrCreateModel()
|
||||
|
||||
@Composable
|
||||
override fun BottomSheetContent(
|
||||
bottomSheetState: State<BottomSheetState>,
|
||||
onHeaderSizeChange: (Dp) -> Unit,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
val state by model.state.collectAsStateWithLifecycle()
|
||||
val bsState by bottomSheetState
|
||||
|
||||
LaunchedEffect(bsState) {
|
||||
model.containerBottomSheetState.value = bsState
|
||||
}
|
||||
|
||||
MarketsList(
|
||||
modifier = modifier,
|
||||
state = state,
|
||||
onHeaderSizeChange = onHeaderSizeChange,
|
||||
bottomSheetState = bsState,
|
||||
)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : MarketsListComponent.Factory {
|
||||
override fun create(context: AppComponentContext): DefaultMarketsListComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package com.tangem.features.markets.details.api
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.features.markets.component.BottomSheetState
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Stable
|
||||
interface MarketsTokenDetailsComponent {
|
||||
|
||||
@Serializable
|
||||
data class Params(
|
||||
val token: TokenMarketSerializable,
|
||||
val appCurrency: AppCurrency,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun BottomSheetContent(
|
||||
bottomSheetState: State<BottomSheetState>,
|
||||
onHeaderSizeChange: (Dp) -> Unit,
|
||||
modifier: Modifier,
|
||||
)
|
||||
|
||||
interface Factory {
|
||||
fun create(context: AppComponentContext, params: Params, onBack: () -> Unit): MarketsTokenDetailsComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package com.tangem.features.markets.details.api
|
||||
|
||||
import com.tangem.domain.core.serialization.SerializedBigDecimal
|
||||
import com.tangem.domain.markets.TokenMarket
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class TokenMarketSerializable(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val symbol: String,
|
||||
val marketCap: SerializedBigDecimal?,
|
||||
val tokenQuotes: Quotes,
|
||||
val imageUrl: String,
|
||||
) {
|
||||
|
||||
@Serializable
|
||||
data class Quotes(
|
||||
val currentPrice: SerializedBigDecimal,
|
||||
val h24Percent: SerializedBigDecimal,
|
||||
val weekPercent: SerializedBigDecimal,
|
||||
val monthPercent: SerializedBigDecimal,
|
||||
)
|
||||
}
|
||||
|
||||
fun TokenMarket.toSerializable(): TokenMarketSerializable {
|
||||
return TokenMarketSerializable(
|
||||
id = id,
|
||||
name = name,
|
||||
symbol = symbol,
|
||||
marketCap = marketCap,
|
||||
tokenQuotes = TokenMarketSerializable.Quotes(
|
||||
currentPrice = tokenQuotes.currentPrice,
|
||||
h24Percent = tokenQuotes.h24Percent(),
|
||||
weekPercent = tokenQuotes.weekPercent(),
|
||||
monthPercent = tokenQuotes.monthPercent(),
|
||||
),
|
||||
imageUrl = imageUrlLarge,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
package com.tangem.features.markets.details.impl
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.features.markets.component.BottomSheetState
|
||||
import com.tangem.features.markets.details.api.MarketsTokenDetailsComponent
|
||||
import com.tangem.features.markets.details.impl.model.MarketsTokenDetailsModel
|
||||
import com.tangem.features.markets.details.impl.ui.MarketsTokenDetailsContent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
@Stable
|
||||
internal class DefaultMarketsTokenDetailsComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted params: MarketsTokenDetailsComponent.Params,
|
||||
@Assisted private val onBack: () -> Unit,
|
||||
) : AppComponentContext by appComponentContext, MarketsTokenDetailsComponent {
|
||||
|
||||
private val model: MarketsTokenDetailsModel = getOrCreateModel(params)
|
||||
|
||||
@Composable
|
||||
override fun BottomSheetContent(
|
||||
bottomSheetState: State<BottomSheetState>,
|
||||
onHeaderSizeChange: (Dp) -> Unit,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
val state by model.state.collectAsStateWithLifecycle()
|
||||
|
||||
MarketsTokenDetailsContent(
|
||||
state = state,
|
||||
onBackClick = { onBack() },
|
||||
onHeaderSizeChange = onHeaderSizeChange,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : MarketsTokenDetailsComponent.Factory {
|
||||
override fun create(
|
||||
context: AppComponentContext,
|
||||
params: MarketsTokenDetailsComponent.Params,
|
||||
onBack: () -> Unit,
|
||||
): DefaultMarketsTokenDetailsComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.features.markets.details.impl.di
|
||||
|
||||
import com.tangem.features.markets.details.api.MarketsTokenDetailsComponent
|
||||
import com.tangem.features.markets.details.impl.DefaultMarketsTokenDetailsComponent
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface ComponentModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindMarketsTokenDetailsComponent(
|
||||
factory: DefaultMarketsTokenDetailsComponent.Factory,
|
||||
): MarketsTokenDetailsComponent.Factory
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.features.markets.details.impl.di
|
||||
|
||||
import com.tangem.core.decompose.di.DecomposeComponent
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.features.markets.details.impl.model.MarketsTokenDetailsModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.multibindings.ClassKey
|
||||
import dagger.multibindings.IntoMap
|
||||
|
||||
@Module
|
||||
@InstallIn(DecomposeComponent::class)
|
||||
internal interface ModelModule {
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(MarketsTokenDetailsModel::class)
|
||||
fun provideMarketsTokenDetailsModel(model: MarketsTokenDetailsModel): Model
|
||||
}
|
||||
|
|
@ -0,0 +1,384 @@
|
|||
package com.tangem.features.markets.details.impl.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.common.ui.charts.state.*
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.core.ui.utils.DateTimeFormatters
|
||||
import com.tangem.core.ui.utils.toTimeFormat
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.GetTokenMarketInfoUseCase
|
||||
import com.tangem.domain.markets.GetTokenPriceChartUseCase
|
||||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import com.tangem.features.markets.details.api.MarketsTokenDetailsComponent
|
||||
import com.tangem.features.markets.details.impl.model.converters.DescriptionConverter
|
||||
import com.tangem.features.markets.details.impl.model.converters.TokenMarketInfoConverter
|
||||
import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent
|
||||
import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM
|
||||
import com.tangem.features.markets.impl.R
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
import com.tangem.utils.coroutines.saveIn
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LargeClass")
|
||||
@Stable
|
||||
internal class MarketsTokenDetailsModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
private val getTokenPriceChartUseCase: GetTokenPriceChartUseCase,
|
||||
private val getTokenMarketInfoUseCase: GetTokenMarketInfoUseCase,
|
||||
private val urlOpener: UrlOpener,
|
||||
) : Model() {
|
||||
|
||||
val params = paramsContainer.require<MarketsTokenDetailsComponent.Params>()
|
||||
|
||||
private val currentAppCurrency = getSelectedAppCurrencyUseCase()
|
||||
.map { maybeAppCurrency ->
|
||||
maybeAppCurrency.getOrElse { AppCurrency.Default }
|
||||
}.stateIn(
|
||||
scope = modelScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = params.appCurrency,
|
||||
)
|
||||
|
||||
private val infoConverter = TokenMarketInfoConverter(
|
||||
appCurrency = Provider { currentAppCurrency.value },
|
||||
onInfoClick = {
|
||||
showInfoBottomSheet(it)
|
||||
},
|
||||
onLinkClick = {
|
||||
urlOpener.openUrl(it.url)
|
||||
},
|
||||
)
|
||||
private val descriptionConverter = DescriptionConverter(
|
||||
onReadModeClicked = {
|
||||
showInfoBottomSheet(it)
|
||||
},
|
||||
)
|
||||
|
||||
private val chartDataProducer = MarketChartDataProducer.build(dispatcher = dispatchers.default) {
|
||||
chartData = MarketChartData.NoData.Loading
|
||||
|
||||
updateLook {
|
||||
it.copy(
|
||||
type = getChartTypeByPercent(params.token.tokenQuotes.h24Percent),
|
||||
xAxisFormatter = { value ->
|
||||
value.toLong().toTimeFormat(DateTimeFormatters.timeFormatter)
|
||||
},
|
||||
yAxisFormatter = { value ->
|
||||
BigDecimalFormatter.formatFiatAmountUncapped(
|
||||
fiatAmount = value,
|
||||
fiatCurrencyCode = currentAppCurrency.value.code,
|
||||
fiatCurrencySymbol = "",
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val state = MutableStateFlow(
|
||||
MarketsTokenDetailsUM(
|
||||
tokenName = params.token.name,
|
||||
priceText = BigDecimalFormatter.formatFiatAmountUncapped(
|
||||
fiatAmount = params.token.tokenQuotes.currentPrice,
|
||||
fiatCurrencyCode = currentAppCurrency.value.code,
|
||||
fiatCurrencySymbol = currentAppCurrency.value.symbol,
|
||||
),
|
||||
dateTimeText = resourceReference(R.string.common_today),
|
||||
priceChangePercentText = BigDecimalFormatter.formatPercent(
|
||||
percent = params.token.tokenQuotes.h24Percent,
|
||||
useAbsoluteValue = true,
|
||||
),
|
||||
priceChangeType = if (params.token.tokenQuotes.h24Percent < BigDecimal.ZERO) {
|
||||
PriceChangeType.DOWN
|
||||
} else {
|
||||
PriceChangeType.UP
|
||||
},
|
||||
iconUrl = params.token.imageUrl,
|
||||
chartState = MarketsTokenDetailsUM.ChartState(
|
||||
dataProducer = chartDataProducer,
|
||||
chartLook = MarketChartLook(),
|
||||
onLoadRetryClick = ::onLoadRetryClicked,
|
||||
status = MarketsTokenDetailsUM.ChartState.Status.LOADING,
|
||||
onMarkerPointSelected = ::onMarkerPointSelected,
|
||||
),
|
||||
selectedInterval = PriceChangeInterval.H24,
|
||||
onSelectedIntervalChange = ::onSelectedIntervalChange,
|
||||
body = MarketsTokenDetailsUM.Body.Loading,
|
||||
infoBottomSheet = TangemBottomSheetConfig(
|
||||
isShow = false,
|
||||
onDismissRequest = {},
|
||||
content = TangemBottomSheetConfigContent.Empty,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
private val loadChartJobHolder = JobHolder()
|
||||
|
||||
init {
|
||||
// reload screen if currency changed
|
||||
modelScope.launch {
|
||||
currentAppCurrency
|
||||
.filter { it != params.appCurrency }
|
||||
.collectLatest {
|
||||
initialLoad()
|
||||
}
|
||||
}
|
||||
|
||||
initialLoad()
|
||||
}
|
||||
|
||||
private fun initialLoad() {
|
||||
loadChart(state.value.selectedInterval)
|
||||
loadInfo()
|
||||
}
|
||||
|
||||
private fun onSelectedIntervalChange(interval: PriceChangeInterval) {
|
||||
if (state.value.selectedInterval == interval) return
|
||||
|
||||
state.update {
|
||||
it.copy(
|
||||
selectedInterval = interval,
|
||||
priceChangeType = PriceChangeType.UP,
|
||||
)
|
||||
}
|
||||
|
||||
loadChart(interval)
|
||||
}
|
||||
|
||||
private fun loadChart(interval: PriceChangeInterval) {
|
||||
modelScope.launch {
|
||||
state.update {
|
||||
it.copy(
|
||||
chartState = it.chartState.copy(
|
||||
status = MarketsTokenDetailsUM.ChartState.Status.LOADING,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
chartDataProducer.runTransactionSuspend {
|
||||
chartData = MarketChartData.NoData.Loading
|
||||
}
|
||||
|
||||
val chart = getTokenPriceChartUseCase.invoke(
|
||||
appCurrency = currentAppCurrency.value,
|
||||
interval = interval,
|
||||
tokenId = params.token.id,
|
||||
)
|
||||
|
||||
state.update {
|
||||
it.copy(
|
||||
selectedInterval = interval,
|
||||
chartState = it.chartState.copy(
|
||||
status = MarketsTokenDetailsUM.ChartState.Status.LOADING,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val xAxisFormatter = getFormatterByInterval(state.value.selectedInterval)
|
||||
|
||||
chart.onRight {
|
||||
chartDataProducer.runTransactionSuspend {
|
||||
chartData = MarketChartData.Data(
|
||||
x = it.timeStamps.map { it.toBigDecimal() }.toImmutableList(),
|
||||
y = it.priceY.toImmutableList(),
|
||||
)
|
||||
|
||||
updateLook {
|
||||
it.copy(
|
||||
xAxisFormatter = xAxisFormatter,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
state.update {
|
||||
it.copy(
|
||||
chartState = it.chartState.copy(
|
||||
status = MarketsTokenDetailsUM.ChartState.Status.DATA,
|
||||
),
|
||||
)
|
||||
}
|
||||
}.onLeft {
|
||||
state.update {
|
||||
it.copy(
|
||||
chartState = it.chartState.copy(
|
||||
status = MarketsTokenDetailsUM.ChartState.Status.ERROR,
|
||||
),
|
||||
body = if (it.body is MarketsTokenDetailsUM.Body.Error) {
|
||||
MarketsTokenDetailsUM.Body.Nothing
|
||||
} else {
|
||||
it.body
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}.saveIn(loadChartJobHolder)
|
||||
}
|
||||
|
||||
private fun loadInfo() {
|
||||
state.update {
|
||||
it.copy(
|
||||
body = MarketsTokenDetailsUM.Body.Loading,
|
||||
)
|
||||
}
|
||||
|
||||
modelScope.launch {
|
||||
val tokenMarketInfo = getTokenMarketInfoUseCase(
|
||||
appCurrency = currentAppCurrency.value,
|
||||
tokenId = params.token.id,
|
||||
)
|
||||
|
||||
tokenMarketInfo.fold(
|
||||
ifRight = { result ->
|
||||
state.update {
|
||||
it.copy(
|
||||
body = MarketsTokenDetailsUM.Body.Content(
|
||||
description = descriptionConverter.convert(result),
|
||||
infoBlocks = infoConverter.convert(result),
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
ifLeft = {
|
||||
state.update {
|
||||
if (it.chartState.status == MarketsTokenDetailsUM.ChartState.Status.DATA) {
|
||||
it.copy(
|
||||
body = MarketsTokenDetailsUM.Body.Error(
|
||||
onLoadRetryClick = ::onLoadRetryClicked,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
it.copy(
|
||||
body = MarketsTokenDetailsUM.Body.Nothing,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getFormatterByInterval(interval: PriceChangeInterval): (BigDecimal) -> String {
|
||||
return when (interval) {
|
||||
PriceChangeInterval.H24 -> { value: BigDecimal ->
|
||||
value.toLong().toTimeFormat(DateTimeFormatters.timeFormatter)
|
||||
}
|
||||
PriceChangeInterval.WEEK,
|
||||
PriceChangeInterval.MONTH,
|
||||
PriceChangeInterval.MONTH3,
|
||||
PriceChangeInterval.MONTH6,
|
||||
-> { value ->
|
||||
value.toLong().toTimeFormat(DateTimeFormatters.dateMMMMd)
|
||||
}
|
||||
PriceChangeInterval.YEAR -> { value ->
|
||||
value.toLong().toTimeFormat(DateTimeFormatters.dateMMMMd)
|
||||
}
|
||||
PriceChangeInterval.ALL_TIME -> { value ->
|
||||
value.toLong().toTimeFormat(DateTimeFormatters.dateYYYY)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private fun onMarkerPointSelected(time: BigDecimal?, price: BigDecimal?) {
|
||||
val timeText = time?.toLong()?.toTimeFormat(DateTimeFormatters.dateTimeFormatter)?.let {
|
||||
resourceReference(R.string.common_range, wrappedList(it, resourceReference(R.string.common_now)))
|
||||
} ?: resourceReference(R.string.common_today)
|
||||
|
||||
val percent = price?.subtract(params.token.tokenQuotes.currentPrice)
|
||||
?.divide(params.token.tokenQuotes.currentPrice, 4, RoundingMode.HALF_UP)
|
||||
?.multiply(BigDecimal(-100))
|
||||
?: params.token.tokenQuotes.h24Percent
|
||||
|
||||
val percentText = BigDecimalFormatter.formatPercent(
|
||||
percent = percent,
|
||||
useAbsoluteValue = true,
|
||||
)
|
||||
|
||||
state.update {
|
||||
it.copy(
|
||||
dateTimeText = timeText,
|
||||
priceText = BigDecimalFormatter.formatFiatAmountUncapped(
|
||||
fiatAmount = price ?: params.token.tokenQuotes.currentPrice,
|
||||
fiatCurrencyCode = currentAppCurrency.value.code,
|
||||
fiatCurrencySymbol = currentAppCurrency.value.symbol,
|
||||
),
|
||||
priceChangePercentText = percentText,
|
||||
priceChangeType = when {
|
||||
percent < BigDecimal.ZERO -> PriceChangeType.DOWN
|
||||
percent > BigDecimal.ZERO -> PriceChangeType.UP
|
||||
else -> PriceChangeType.NEUTRAL
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
chartDataProducer.runTransaction {
|
||||
updateLook {
|
||||
it.copy(
|
||||
type = getChartTypeByPercent(percent),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getChartTypeByPercent(percent: BigDecimal): MarketChartLook.Type {
|
||||
return if (percent >= BigDecimal.ZERO) {
|
||||
MarketChartLook.Type.Growing
|
||||
} else {
|
||||
MarketChartLook.Type.Falling
|
||||
}
|
||||
}
|
||||
|
||||
private fun showInfoBottomSheet(content: InfoBottomSheetContent) {
|
||||
state.update { stateToUpdate ->
|
||||
stateToUpdate.copy(
|
||||
infoBottomSheet = stateToUpdate.infoBottomSheet.copy(
|
||||
isShow = true,
|
||||
onDismissRequest = ::hideInfoBottomSheet,
|
||||
content = content,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun hideInfoBottomSheet() {
|
||||
state.update { stateToUpdate ->
|
||||
stateToUpdate.copy(
|
||||
infoBottomSheet = stateToUpdate.infoBottomSheet.copy(
|
||||
isShow = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onLoadRetryClicked() {
|
||||
val currentState = state.value
|
||||
|
||||
if (currentState.chartState.status == MarketsTokenDetailsUM.ChartState.Status.ERROR) {
|
||||
loadChart(currentState.selectedInterval)
|
||||
}
|
||||
|
||||
if (currentState.body is MarketsTokenDetailsUM.Body.Error ||
|
||||
currentState.body is MarketsTokenDetailsUM.Body.Nothing
|
||||
) {
|
||||
loadInfo()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package com.tangem.features.markets.details.impl.model.converters
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent
|
||||
import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM
|
||||
import com.tangem.features.markets.impl.R
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
@Stable
|
||||
internal class DescriptionConverter(
|
||||
val onReadModeClicked: (InfoBottomSheetContent) -> Unit,
|
||||
) : Converter<TokenMarketInfo, MarketsTokenDetailsUM.Description?> {
|
||||
|
||||
override fun convert(value: TokenMarketInfo): MarketsTokenDetailsUM.Description? {
|
||||
return value.shortDescription?.let { desc ->
|
||||
MarketsTokenDetailsUM.Description(
|
||||
shortDescription = stringReference(desc),
|
||||
fullDescription = value.fullDescription?.let { fullDescription ->
|
||||
stringReference(fullDescription)
|
||||
},
|
||||
onReadMoreClick = {
|
||||
onReadModeClicked(
|
||||
InfoBottomSheetContent(
|
||||
title = resourceReference(
|
||||
R.string.markets_token_details_about_token_title,
|
||||
wrappedList(
|
||||
value.name,
|
||||
),
|
||||
),
|
||||
body = stringReference(value.fullDescription ?: ""),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
package com.tangem.features.markets.details.impl.model.converters
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent
|
||||
import com.tangem.features.markets.details.impl.ui.state.InfoPointUM
|
||||
import com.tangem.features.markets.details.impl.ui.state.InsightsUM
|
||||
import com.tangem.features.markets.impl.R
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.StringsSigns
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Stable
|
||||
internal class InsightsConverter(
|
||||
private val appCurrency: Provider<AppCurrency>,
|
||||
private val onInfoClick: (InfoBottomSheetContent) -> Unit,
|
||||
) : Converter<TokenMarketInfo.Insights, InsightsUM> {
|
||||
|
||||
override fun convert(value: TokenMarketInfo.Insights): InsightsUM {
|
||||
return with(value) {
|
||||
InsightsUM(
|
||||
h24Info = createInfoPointList(
|
||||
experiencedBuyerChange = experiencedBuyerChange?.day,
|
||||
holdersChange = holdersChange?.day,
|
||||
liquidityChange = liquidityChange?.day,
|
||||
buyPressureChange = buyPressureChange?.day,
|
||||
),
|
||||
weekInfo = createInfoPointList(
|
||||
experiencedBuyerChange = experiencedBuyerChange?.week,
|
||||
holdersChange = holdersChange?.week,
|
||||
liquidityChange = liquidityChange?.week,
|
||||
buyPressureChange = buyPressureChange?.week,
|
||||
),
|
||||
monthInfo = createInfoPointList(
|
||||
experiencedBuyerChange = experiencedBuyerChange?.month,
|
||||
holdersChange = holdersChange?.month,
|
||||
liquidityChange = liquidityChange?.month,
|
||||
buyPressureChange = buyPressureChange?.month,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createInfoPointList(
|
||||
experiencedBuyerChange: BigDecimal?,
|
||||
holdersChange: BigDecimal?,
|
||||
liquidityChange: BigDecimal?,
|
||||
buyPressureChange: BigDecimal?,
|
||||
): ImmutableList<InfoPointUM> {
|
||||
return persistentListOf(
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_experienced_buyers),
|
||||
value = experiencedBuyerChange.convertChange(),
|
||||
onInfoClick = {
|
||||
onInfoClick(
|
||||
InfoBottomSheetContent(
|
||||
title = resourceReference(R.string.markets_token_details_experienced_buyers),
|
||||
body = resourceReference(R.string.markets_token_details_experienced_buyers_description),
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_buy_pressure),
|
||||
value = buyPressureChange.convertChange(isFiatValue = true),
|
||||
onInfoClick = {
|
||||
onInfoClick(
|
||||
InfoBottomSheetContent(
|
||||
title = resourceReference(R.string.markets_token_details_buy_pressure),
|
||||
body = resourceReference(R.string.markets_token_details_buy_pressure_description),
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_holders),
|
||||
value = holdersChange.convertChange(),
|
||||
onInfoClick = {
|
||||
onInfoClick(
|
||||
InfoBottomSheetContent(
|
||||
title = resourceReference(R.string.markets_token_details_holders),
|
||||
body = resourceReference(R.string.markets_token_details_holders_description),
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_liquidity),
|
||||
value = liquidityChange.convertChange(),
|
||||
onInfoClick = {
|
||||
onInfoClick(
|
||||
InfoBottomSheetContent(
|
||||
title = resourceReference(R.string.markets_token_details_liquidity),
|
||||
body = resourceReference(R.string.markets_token_details_liquidity_description),
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun BigDecimal?.convertChange(isFiatValue: Boolean = false): String {
|
||||
if (this == null) return StringsSigns.DASH_SIGN
|
||||
|
||||
val value = if (isFiatValue) {
|
||||
val currency = appCurrency()
|
||||
BigDecimalFormatter.formatCompactFiatAmount(
|
||||
amount = this.abs(),
|
||||
fiatCurrencyCode = currency.code,
|
||||
fiatCurrencySymbol = currency.symbol,
|
||||
)
|
||||
} else {
|
||||
BigDecimalFormatter.formatCompactAmount(amount = this.abs())
|
||||
}
|
||||
|
||||
val spacing = if (isFiatValue) " " else ""
|
||||
|
||||
return when {
|
||||
this > BigDecimal.ZERO -> StringsSigns.PLUS + spacing + value
|
||||
this < BigDecimal.ZERO -> StringsSigns.MINUS + spacing + value
|
||||
this == BigDecimal.ZERO -> value
|
||||
else -> StringsSigns.DASH_SIGN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
package com.tangem.features.markets.details.impl.model.converters
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.features.markets.details.impl.ui.state.LinksUM
|
||||
import com.tangem.features.markets.impl.R
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@Stable
|
||||
internal class LinksConverter(
|
||||
private val onLinkClick: (LinksUM.Link) -> Unit,
|
||||
) : Converter<TokenMarketInfo.Links, LinksUM> {
|
||||
|
||||
override fun convert(value: TokenMarketInfo.Links): LinksUM {
|
||||
return LinksUM(
|
||||
officialLinks = value.officialLinks?.map { it.convert() }.orEmpty().toImmutableList(),
|
||||
social = value.social?.map { it.convert() }.orEmpty().toImmutableList(),
|
||||
repository = value.repository?.map { it.convert() }.orEmpty().toImmutableList(),
|
||||
blockchainSite = value.blockchainSite?.map { it.convert() }.orEmpty().toImmutableList(),
|
||||
onLinkClick = onLinkClick,
|
||||
)
|
||||
}
|
||||
|
||||
private fun TokenMarketInfo.Link.convert(): LinksUM.Link {
|
||||
return LinksUM.Link(
|
||||
title = stringReference(title),
|
||||
iconRes = getIconById(id),
|
||||
url = link,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getIconById(id: String?): Int {
|
||||
return when (id) {
|
||||
"linkedin" -> R.drawable.ic_linkedin_24
|
||||
"discord" -> R.drawable.ic_discord_24
|
||||
"youtube" -> R.drawable.ic_youtube_24
|
||||
"telegram" -> R.drawable.ic_telegram_24
|
||||
"github" -> R.drawable.ic_github_24
|
||||
"twitter" -> R.drawable.ic_twitter_24
|
||||
"facebook" -> R.drawable.ic_facebook_24
|
||||
"reddit" -> R.drawable.ic_reddit_24
|
||||
"instagram" -> R.drawable.ic_instagram_24
|
||||
"whitepaper" -> R.drawable.ic_doc_24
|
||||
else -> R.drawable.ic_arrow_top_right_24
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
package com.tangem.features.markets.details.impl.model.converters
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent
|
||||
import com.tangem.features.markets.details.impl.ui.state.InfoPointUM
|
||||
import com.tangem.features.markets.details.impl.ui.state.MetricsUM
|
||||
import com.tangem.features.markets.impl.R
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.StringsSigns
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
import java.text.NumberFormat
|
||||
import java.util.Locale
|
||||
|
||||
@Stable
|
||||
internal class MetricsConverter(
|
||||
private val appCurrency: Provider<AppCurrency>,
|
||||
private val onInfoClick: (InfoBottomSheetContent) -> Unit,
|
||||
) : Converter<TokenMarketInfo.Metrics, MetricsUM> {
|
||||
|
||||
@Suppress("LongMethod")
|
||||
override fun convert(value: TokenMarketInfo.Metrics): MetricsUM {
|
||||
return with(value) {
|
||||
MetricsUM(
|
||||
metrics = persistentListOf(
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_market_capitalization),
|
||||
value = marketCap.formatAmount(),
|
||||
onInfoClick = {
|
||||
onInfoClick(
|
||||
InfoBottomSheetContent(
|
||||
title = resourceReference(R.string.markets_token_details_market_capitalization),
|
||||
body = resourceReference(
|
||||
R.string.markets_token_details_market_capitalization_description,
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_market_rating),
|
||||
value = marketRating?.toString() ?: StringsSigns.DASH_SIGN,
|
||||
onInfoClick = {
|
||||
onInfoClick(
|
||||
InfoBottomSheetContent(
|
||||
title = resourceReference(R.string.markets_token_details_market_rating),
|
||||
body = resourceReference(R.string.markets_token_details_market_rating_description),
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_trading_volume),
|
||||
value = volume24h.formatAmount(),
|
||||
onInfoClick = {
|
||||
onInfoClick(
|
||||
InfoBottomSheetContent(
|
||||
title = resourceReference(R.string.markets_token_details_trading_volume),
|
||||
body = resourceReference(
|
||||
R.string.markets_token_details_trading_volume_24h_description,
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_fully_diluted_valuation),
|
||||
value = fullyDilutedValuation.formatAmount(),
|
||||
onInfoClick = {
|
||||
onInfoClick(
|
||||
InfoBottomSheetContent(
|
||||
title = resourceReference(R.string.markets_token_details_fully_diluted_valuation),
|
||||
body = resourceReference(
|
||||
R.string.markets_token_details_fully_diluted_valuation_description,
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_circulating_supply),
|
||||
value = circulatingSupply.formatAmount(crypto = true),
|
||||
onInfoClick = {
|
||||
onInfoClick(
|
||||
InfoBottomSheetContent(
|
||||
title = resourceReference(R.string.markets_token_details_circulating_supply),
|
||||
body = resourceReference(
|
||||
R.string.markets_token_details_circulating_supply_description,
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_total_supply),
|
||||
value = totalSupply.formatAmount(crypto = true),
|
||||
onInfoClick = {
|
||||
onInfoClick(
|
||||
InfoBottomSheetContent(
|
||||
title = resourceReference(R.string.markets_token_details_total_supply),
|
||||
body = resourceReference(R.string.markets_token_details_total_supply_description),
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun BigDecimal?.formatAmount(crypto: Boolean = false): String {
|
||||
return if (crypto) {
|
||||
val formatter = NumberFormat.getNumberInstance(Locale.getDefault()).apply {
|
||||
maximumFractionDigits = 0
|
||||
isGroupingUsed = true
|
||||
roundingMode = RoundingMode.HALF_UP
|
||||
}
|
||||
formatter.format(this)
|
||||
} else {
|
||||
val currency = appCurrency()
|
||||
BigDecimalFormatter.formatFiatAmount(
|
||||
fiatAmount = this,
|
||||
fiatCurrencyCode = currency.code,
|
||||
fiatCurrencySymbol = currency.symbol,
|
||||
decimals = 0,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
package com.tangem.features.markets.details.impl.model.converters
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.features.markets.details.impl.ui.state.PricePerformanceUM
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.StringsSigns
|
||||
import com.tangem.utils.converter.Converter
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
|
||||
@Stable
|
||||
internal class PricePerformanceConverter(
|
||||
private val appCurrency: Provider<AppCurrency>,
|
||||
) : Converter<TokenMarketInfo.PricePerformance, PricePerformanceUM> {
|
||||
|
||||
override fun convert(value: TokenMarketInfo.PricePerformance): PricePerformanceUM {
|
||||
return PricePerformanceUM(
|
||||
h24 = value.day.convert(),
|
||||
month = value.month.convert(),
|
||||
all = value.allTime.convert(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun TokenMarketInfo.Range?.convert(): PricePerformanceUM.Value {
|
||||
if (this == null) {
|
||||
return PricePerformanceUM.Value(
|
||||
low = StringsSigns.DASH_SIGN,
|
||||
high = StringsSigns.DASH_SIGN,
|
||||
indicatorFraction = 0f,
|
||||
)
|
||||
}
|
||||
|
||||
return PricePerformanceUM.Value(
|
||||
low = low.convert(),
|
||||
high = high.convert(),
|
||||
indicatorFraction = calculateFraction(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun BigDecimal?.convert(): String {
|
||||
val currency = appCurrency()
|
||||
return BigDecimalFormatter.formatCompactFiatAmount(
|
||||
amount = this,
|
||||
fiatCurrencyCode = currency.code,
|
||||
fiatCurrencySymbol = currency.symbol,
|
||||
)
|
||||
}
|
||||
|
||||
private fun TokenMarketInfo.Range.calculateFraction(): Float {
|
||||
if (low == null || high == null || low == BigDecimal.ZERO) return 0f
|
||||
return (high!! - low!!).divide(low!!, RoundingMode.HALF_UP)
|
||||
.setScale(2, RoundingMode.HALF_UP)
|
||||
.toFloat().coerceAtMost(1f)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package com.tangem.features.markets.details.impl.model.converters
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent
|
||||
import com.tangem.features.markets.details.impl.ui.state.SecurityScoreUM
|
||||
import com.tangem.features.markets.impl.R
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
// TODO implement when backend is ready
|
||||
@Stable
|
||||
internal class SecurityScoreConverter(
|
||||
private val onInfoClick: (InfoBottomSheetContent) -> Unit,
|
||||
) : Converter<Unit, SecurityScoreUM> {
|
||||
|
||||
override fun convert(value: Unit): SecurityScoreUM {
|
||||
return with(value) {
|
||||
SecurityScoreUM(
|
||||
score = 4.7f,
|
||||
description = "Based on 3 ratings",
|
||||
onInfoClick = {
|
||||
onInfoClick(
|
||||
InfoBottomSheetContent(
|
||||
title = resourceReference(R.string.markets_token_details_security_score),
|
||||
body = stringReference("markets_token_details_security_score_description"),
|
||||
// FIXME
|
||||
// resourceReference(R.string.markets_token_details_security_score_description)
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package com.tangem.features.markets.details.impl.model.converters
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent
|
||||
import com.tangem.features.markets.details.impl.ui.state.LinksUM
|
||||
import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
@Stable
|
||||
internal class TokenMarketInfoConverter(
|
||||
appCurrency: Provider<AppCurrency>,
|
||||
onInfoClick: (InfoBottomSheetContent) -> Unit,
|
||||
onLinkClick: (LinksUM.Link) -> Unit,
|
||||
) : Converter<TokenMarketInfo, MarketsTokenDetailsUM.InformationBlocks> {
|
||||
|
||||
private val insightsConverter = InsightsConverter(appCurrency = appCurrency, onInfoClick = onInfoClick)
|
||||
private val securityScoreConverter = SecurityScoreConverter(onInfoClick = onInfoClick)
|
||||
private val metricsConverter = MetricsConverter(appCurrency = appCurrency, onInfoClick = onInfoClick)
|
||||
private val pricePerformanceConverter = PricePerformanceConverter(appCurrency = appCurrency)
|
||||
private val linksConverter = LinksConverter(onLinkClick = onLinkClick)
|
||||
|
||||
override fun convert(value: TokenMarketInfo): MarketsTokenDetailsUM.InformationBlocks {
|
||||
return MarketsTokenDetailsUM.InformationBlocks(
|
||||
insights = value.insights?.let { insightsConverter.convert(it) },
|
||||
securityScore = securityScoreConverter.convert(Unit),
|
||||
metrics = value.metrics?.let { metricsConverter.convert(it) },
|
||||
pricePerformance = value.pricePerformance?.let { pricePerformanceConverter.convert(it) },
|
||||
links = value.links?.let { linksConverter.convert(it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,252 @@
|
|||
package com.tangem.features.markets.details.impl.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import com.tangem.common.ui.charts.state.MarketChartDataProducer
|
||||
import com.tangem.common.ui.charts.state.MarketChartLook
|
||||
import com.tangem.core.ui.components.*
|
||||
import com.tangem.core.ui.components.appbar.TangemTopAppBar
|
||||
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons
|
||||
import com.tangem.core.ui.components.currency.icon.CoinIcon
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeInPercent
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.res.LocalMainBottomSheetColor
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.utils.disableNestedScroll
|
||||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import com.tangem.features.markets.details.impl.ui.components.InfoBottomSheet
|
||||
import com.tangem.features.markets.details.impl.ui.components.MarketTokenDetailsChart
|
||||
import com.tangem.features.markets.details.impl.ui.components.tokenMarketDetailsBody
|
||||
import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM
|
||||
import com.tangem.features.markets.impl.R
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
@Composable
|
||||
internal fun MarketsTokenDetailsContent(
|
||||
state: MarketsTokenDetailsUM,
|
||||
onBackClick: () -> Unit,
|
||||
onHeaderSizeChange: (Dp) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Content(
|
||||
modifier = modifier,
|
||||
state = state,
|
||||
onBackClick = onBackClick,
|
||||
onHeaderSizeChange = onHeaderSizeChange,
|
||||
)
|
||||
|
||||
InfoBottomSheet(config = state.infoBottomSheet)
|
||||
}
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
@Composable
|
||||
private fun Content(
|
||||
state: MarketsTokenDetailsUM,
|
||||
onBackClick: () -> Unit,
|
||||
onHeaderSizeChange: (Dp) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val backgroundColor = LocalMainBottomSheetColor.current.value
|
||||
val density = LocalDensity.current
|
||||
val bottomBarHeight = with(density) { WindowInsets.systemBars.getBottom(this).toDp() }
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.drawBehind { drawRect(backgroundColor) }
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
TangemTopAppBar(
|
||||
modifier = Modifier.onGloballyPositioned {
|
||||
if (it.size.height > 0) {
|
||||
with(density) {
|
||||
onHeaderSizeChange(it.size.height.toDp())
|
||||
}
|
||||
}
|
||||
},
|
||||
title = state.tokenName,
|
||||
startButton = TopAppBarButtonUM.Back(onBackClick),
|
||||
)
|
||||
|
||||
SpacerH4()
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.disableNestedScroll(),
|
||||
contentPadding = PaddingValues(bottom = bottomBarHeight),
|
||||
) {
|
||||
item("header") {
|
||||
Header(
|
||||
state = state,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
item { SpacerH16() }
|
||||
item("intervalSelector") {
|
||||
IntervalSelector(
|
||||
trendInterval = state.selectedInterval,
|
||||
onIntervalClick = state.onSelectedIntervalChange,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
item { SpacerH32() }
|
||||
item(
|
||||
contentType = "chart",
|
||||
) {
|
||||
MarketTokenDetailsChart(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
state = state.chartState,
|
||||
)
|
||||
}
|
||||
item { SpacerH16() }
|
||||
|
||||
tokenMarketDetailsBody(
|
||||
state = state.body,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Header(state: MarketsTokenDetailsUM, modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Column {
|
||||
Text(
|
||||
text = state.priceText,
|
||||
style = TangemTheme.typography.head,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4)) {
|
||||
Text(
|
||||
text = state.dateTimeText.resolveReference(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
PriceChangeInPercent(
|
||||
valueInPercent = state.priceChangePercentText,
|
||||
type = state.priceChangeType,
|
||||
textStyle = TangemTheme.typography.caption2,
|
||||
)
|
||||
}
|
||||
}
|
||||
SpacerW4()
|
||||
CoinIcon(
|
||||
modifier = Modifier.size(TangemTheme.dimens.size48),
|
||||
url = state.iconUrl,
|
||||
alpha = 1f,
|
||||
colorFilter = null,
|
||||
fallbackResId = R.drawable.ic_custom_token_44,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun IntervalSelector(
|
||||
trendInterval: PriceChangeInterval,
|
||||
onIntervalClick: (PriceChangeInterval) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
SegmentedButtons(
|
||||
config = persistentListOf(
|
||||
PriceChangeInterval.H24,
|
||||
PriceChangeInterval.WEEK,
|
||||
PriceChangeInterval.MONTH,
|
||||
PriceChangeInterval.MONTH3,
|
||||
PriceChangeInterval.MONTH6,
|
||||
PriceChangeInterval.YEAR,
|
||||
PriceChangeInterval.ALL_TIME,
|
||||
),
|
||||
color = TangemTheme.colors.button.secondary,
|
||||
initialSelectedItem = trendInterval,
|
||||
onClick = onIntervalClick,
|
||||
modifier = modifier,
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.align(Alignment.Center)
|
||||
.padding(
|
||||
vertical = TangemTheme.dimens.spacing4,
|
||||
),
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
text = it.getText().resolveReference(),
|
||||
style = TangemTheme.typography.caption1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PriceChangeInterval.getText(): TextReference {
|
||||
return when (this) {
|
||||
PriceChangeInterval.H24 -> resourceReference(R.string.markets_selector_interval_24h_title)
|
||||
PriceChangeInterval.WEEK -> resourceReference(R.string.markets_selector_interval_7d_title)
|
||||
PriceChangeInterval.MONTH -> resourceReference(R.string.markets_selector_interval_1m_title)
|
||||
PriceChangeInterval.MONTH3 -> resourceReference(R.string.markets_selector_interval_3m_title)
|
||||
PriceChangeInterval.MONTH6 -> resourceReference(R.string.markets_selector_interval_6m_title)
|
||||
PriceChangeInterval.YEAR -> resourceReference(R.string.markets_selector_interval_1y_title)
|
||||
PriceChangeInterval.ALL_TIME -> resourceReference(R.string.markets_selector_interval_all_title)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview() {
|
||||
TangemThemePreview {
|
||||
Content(
|
||||
modifier = Modifier.background(TangemTheme.colors.background.tertiary),
|
||||
state = MarketsTokenDetailsUM(
|
||||
tokenName = "Token Name",
|
||||
priceText = "Price",
|
||||
dateTimeText = stringReference("Date Time"),
|
||||
priceChangePercentText = "Price Change",
|
||||
iconUrl = "",
|
||||
priceChangeType = PriceChangeType.UP,
|
||||
chartState = MarketsTokenDetailsUM.ChartState(
|
||||
dataProducer = MarketChartDataProducer.build { },
|
||||
chartLook = MarketChartLook(),
|
||||
onLoadRetryClick = {},
|
||||
status = MarketsTokenDetailsUM.ChartState.Status.LOADING,
|
||||
onMarkerPointSelected = { _, _ -> },
|
||||
),
|
||||
selectedInterval = PriceChangeInterval.H24,
|
||||
onSelectedIntervalChange = { },
|
||||
body = MarketsTokenDetailsUM.Body.Loading,
|
||||
infoBottomSheet = TangemBottomSheetConfig(
|
||||
isShow = false,
|
||||
onDismissRequest = {},
|
||||
content = TangemBottomSheetConfigContent.Empty,
|
||||
),
|
||||
),
|
||||
onHeaderSizeChange = {},
|
||||
onBackClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
package com.tangem.features.markets.details.impl.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.components.TextShimmer
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.utils.PreviewShimmerContainer
|
||||
import com.tangem.features.markets.impl.R
|
||||
|
||||
@Composable
|
||||
internal fun Description(
|
||||
description: TextReference,
|
||||
hasFullDescription: Boolean,
|
||||
onReadMoreClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (hasFullDescription) {
|
||||
val text = buildAnnotatedString {
|
||||
withStyle(SpanStyle(color = TangemTheme.colors.text.secondary)) {
|
||||
append(description.resolveReference())
|
||||
}
|
||||
withStyle(SpanStyle(color = TangemTheme.colors.text.accent)) {
|
||||
append(" " + stringResource(R.string.common_read_more))
|
||||
}
|
||||
}
|
||||
|
||||
ClickableText(
|
||||
modifier = modifier,
|
||||
text = text,
|
||||
style = TangemTheme.typography.body2,
|
||||
) {
|
||||
text.spanStyles.getOrNull(1)?.let { spanStyle ->
|
||||
if (it in spanStyle.start..spanStyle.end) {
|
||||
onReadMoreClick()
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Text(
|
||||
modifier = modifier,
|
||||
text = description.resolveReference(),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun DescriptionPlaceholder(modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
) {
|
||||
TextShimmer(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
style = TangemTheme.typography.body2,
|
||||
textSizeHeight = true,
|
||||
)
|
||||
TextShimmer(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
style = TangemTheme.typography.body2,
|
||||
textSizeHeight = true,
|
||||
)
|
||||
TextShimmer(
|
||||
modifier = Modifier.fillMaxWidth(fraction = 0.8f),
|
||||
style = TangemTheme.typography.body2,
|
||||
textSizeHeight = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun ContentPreview() {
|
||||
TangemThemePreview {
|
||||
Description(
|
||||
description = stringReference(
|
||||
"XRP (XRP) is a cryptocurrency launched in January 2009, where the first " +
|
||||
"genesis block was mined on 9th January 2009",
|
||||
),
|
||||
hasFullDescription = true,
|
||||
onReadMoreClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun PreviewPlaceholder() {
|
||||
TangemThemePreview {
|
||||
PreviewShimmerContainer(
|
||||
actualContent = {
|
||||
ContentPreview()
|
||||
},
|
||||
shimmerContent = {
|
||||
DescriptionPlaceholder()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
package com.tangem.features.markets.details.impl.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.systemBars
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetTitle
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent
|
||||
|
||||
@Composable
|
||||
internal fun InfoBottomSheet(config: TangemBottomSheetConfig) {
|
||||
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
|
||||
|
||||
TangemBottomSheet<InfoBottomSheetContent>(
|
||||
config = config,
|
||||
skipPartiallyExpanded = false,
|
||||
addBottomInsets = false,
|
||||
title = {
|
||||
TangemBottomSheetTitle(title = it.title)
|
||||
},
|
||||
content = {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = TangemTheme.dimens.spacing28),
|
||||
) {
|
||||
Text(
|
||||
text = it.body.resolveReference(),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
)
|
||||
SpacerH(bottomBarHeight)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
package com.tangem.features.markets.details.impl.ui.components
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.TextShimmer
|
||||
import com.tangem.core.ui.components.text.TooltipText
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.utils.PreviewShimmerContainer
|
||||
import com.tangem.features.markets.details.impl.ui.state.InfoPointUM
|
||||
|
||||
@Composable
|
||||
internal fun InfoPoint(infoPointUM: InfoPointUM, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier.padding(vertical = TangemTheme.dimens.spacing8),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
) {
|
||||
if (infoPointUM.onInfoClick != null) {
|
||||
TooltipText(
|
||||
text = infoPointUM.title,
|
||||
onInfoClick = infoPointUM.onInfoClick,
|
||||
textStyle = TangemTheme.typography.caption2,
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = infoPointUM.title.resolveReference(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = infoPointUM.value,
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun InfoPointShimmer(modifier: Modifier = Modifier, withTooltip: Boolean = false) {
|
||||
Column(
|
||||
modifier = modifier.padding(vertical = TangemTheme.dimens.spacing8),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
) {
|
||||
if (withTooltip) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.requiredHeight(TangemTheme.dimens.size16)
|
||||
.fillMaxWidth(),
|
||||
contentAlignment = Alignment.CenterStart,
|
||||
) {
|
||||
TextShimmer(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
textSizeHeight = false,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
TextShimmer(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
textSizeHeight = true,
|
||||
)
|
||||
}
|
||||
TextShimmer(
|
||||
modifier = Modifier.fillMaxWidth(fraction = 0.5f),
|
||||
style = TangemTheme.typography.body1,
|
||||
textSizeHeight = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview("Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun ContentPreview() {
|
||||
TangemThemePreview {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.width(150.dp)
|
||||
.background(TangemTheme.colors.background.tertiary),
|
||||
) {
|
||||
InfoPoint(
|
||||
infoPointUM = InfoPointUM(
|
||||
title = stringReference("Market Cap"),
|
||||
value = "$1,000,000,000",
|
||||
),
|
||||
)
|
||||
InfoPoint(
|
||||
infoPointUM = InfoPointUM(
|
||||
title = stringReference("Market Cap"),
|
||||
value = "$1,000,000,000",
|
||||
onInfoClick = { },
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview("Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun PreviewShimmer() {
|
||||
TangemThemePreview {
|
||||
PreviewShimmerContainer(
|
||||
shimmerContent = {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.width(150.dp)
|
||||
.background(TangemTheme.colors.background.tertiary),
|
||||
) {
|
||||
InfoPointShimmer(modifier = Modifier.fillMaxWidth())
|
||||
InfoPointShimmer(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
withTooltip = true,
|
||||
)
|
||||
}
|
||||
},
|
||||
actualContent = {
|
||||
ContentPreview()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,198 @@
|
|||
package com.tangem.features.markets.details.impl.ui.components
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.components.block.information.GridItems
|
||||
import com.tangem.core.ui.components.block.information.InformationBlock
|
||||
import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.utils.PreviewShimmerContainer
|
||||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import com.tangem.features.markets.details.impl.ui.state.InfoPointUM
|
||||
import com.tangem.features.markets.details.impl.ui.state.InsightsUM
|
||||
import com.tangem.features.markets.details.impl.ui.getText
|
||||
import com.tangem.features.markets.impl.R
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@Composable
|
||||
internal fun InsightsBlock(state: InsightsUM, modifier: Modifier = Modifier) {
|
||||
var currentInterval by remember { mutableStateOf(PriceChangeInterval.H24) }
|
||||
|
||||
InformationBlock(
|
||||
modifier = modifier,
|
||||
title = {
|
||||
Text(
|
||||
modifier = Modifier,
|
||||
text = stringResource(R.string.markets_token_details_insights),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
},
|
||||
action = {
|
||||
SegmentedButtons(
|
||||
config = persistentListOf(
|
||||
PriceChangeInterval.H24,
|
||||
PriceChangeInterval.WEEK,
|
||||
PriceChangeInterval.MONTH,
|
||||
),
|
||||
initialSelectedItem = PriceChangeInterval.H24,
|
||||
onClick = { currentInterval = it },
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.align(Alignment.Center)
|
||||
.padding(vertical = TangemTheme.dimens.spacing4),
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
text = it.getText().resolveReference(),
|
||||
style = TangemTheme.typography.caption1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
content = {
|
||||
val infoPoints = when (currentInterval) {
|
||||
PriceChangeInterval.H24 -> state.h24Info
|
||||
PriceChangeInterval.WEEK -> state.weekInfo
|
||||
PriceChangeInterval.MONTH -> state.monthInfo
|
||||
else -> state.h24Info
|
||||
}
|
||||
|
||||
GridItems(
|
||||
items = infoPoints,
|
||||
itemContent = {
|
||||
InfoPoint(
|
||||
modifier = Modifier.align(Alignment.CenterStart),
|
||||
infoPointUM = it,
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun InsightsBlockPlaceholder(modifier: Modifier = Modifier) {
|
||||
val subtitle2dp = with(LocalDensity.current) { TangemTheme.typography.subtitle2.lineHeight.toDp() }
|
||||
val caption1dp = with(LocalDensity.current) { TangemTheme.typography.caption1.lineHeight.toDp() }
|
||||
val headerHeight = maxOf(subtitle2dp, caption1dp) + TangemTheme.dimens.spacing4
|
||||
|
||||
InformationBlock(
|
||||
modifier = modifier,
|
||||
title = {
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.height(headerHeight)
|
||||
.fillMaxWidth(),
|
||||
radius = TangemTheme.dimens.radius3,
|
||||
)
|
||||
},
|
||||
content = {
|
||||
GridItems(
|
||||
items = List(size = 4) { it }.toImmutableList(),
|
||||
horizontalArragement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
itemContent = {
|
||||
InfoPointShimmer(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun ContentPreview() {
|
||||
TangemThemePreview {
|
||||
InsightsBlock(
|
||||
state = InsightsUM(
|
||||
h24Info = persistentListOf(
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_experienced_buyers),
|
||||
value = "1 000 000 000",
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_buy_pressure),
|
||||
value = "1 000 000 000",
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_holders),
|
||||
value = "1 000 000 000",
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_liquidity),
|
||||
value = "1 000 000 000",
|
||||
),
|
||||
),
|
||||
weekInfo = persistentListOf(
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_experienced_buyers),
|
||||
value = "1 000 000",
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_buy_pressure),
|
||||
value = "1 000 000",
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_holders),
|
||||
value = "1 000 000",
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_liquidity),
|
||||
value = "1 000 000",
|
||||
),
|
||||
),
|
||||
monthInfo = persistentListOf(
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_experienced_buyers),
|
||||
value = "1 000",
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_buy_pressure),
|
||||
value = "1 000",
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_holders),
|
||||
value = "1 000",
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_liquidity),
|
||||
value = "1 000",
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun PreviewPlaceholder() {
|
||||
TangemThemePreview {
|
||||
PreviewShimmerContainer(
|
||||
actualContent = { ContentPreview() },
|
||||
shimmerContent = { InsightsBlockPlaceholder() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,221 @@
|
|||
package com.tangem.features.markets.details.impl.ui.components
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import com.tangem.core.ui.components.SmallButtonShimmer
|
||||
import com.tangem.core.ui.components.TextShimmer
|
||||
import com.tangem.core.ui.components.block.information.InformationBlock
|
||||
import com.tangem.core.ui.components.buttons.SecondarySmallButton
|
||||
import com.tangem.core.ui.components.buttons.SmallButtonConfig
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
|
||||
import com.tangem.core.ui.components.inputrow.inner.DividerContainer
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.utils.PreviewShimmerContainer
|
||||
import com.tangem.features.markets.details.impl.ui.state.LinksUM
|
||||
import com.tangem.features.markets.impl.R
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@Composable
|
||||
internal fun LinksBlock(state: LinksUM, modifier: Modifier = Modifier) {
|
||||
InformationBlock(
|
||||
modifier = modifier,
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(id = R.string.markets_token_details_links),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
},
|
||||
content = {
|
||||
Column {
|
||||
SubBlock(
|
||||
title = stringResource(id = R.string.markets_token_details_official_links),
|
||||
links = state.officialLinks,
|
||||
onLinkClick = state.onLinkClick,
|
||||
)
|
||||
SubBlock(
|
||||
title = stringResource(id = R.string.markets_token_details_social),
|
||||
links = state.social,
|
||||
onLinkClick = state.onLinkClick,
|
||||
)
|
||||
SubBlock(
|
||||
title = stringResource(id = R.string.markets_token_details_repository),
|
||||
links = state.repository,
|
||||
onLinkClick = state.onLinkClick,
|
||||
)
|
||||
SubBlock(
|
||||
title = stringResource(id = R.string.markets_token_details_blockchain_site),
|
||||
links = state.blockchainSite,
|
||||
onLinkClick = state.onLinkClick,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
private fun SubBlock(
|
||||
links: ImmutableList<LinksUM.Link>,
|
||||
onLinkClick: (LinksUM.Link) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
lastBlock: Boolean = false,
|
||||
title: String = "Official links",
|
||||
) {
|
||||
if (links.isEmpty()) return
|
||||
|
||||
DividerContainer(
|
||||
modifier = modifier,
|
||||
showDivider = !lastBlock,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing12),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
links.fastForEach {
|
||||
SecondarySmallButton(
|
||||
config = SmallButtonConfig(
|
||||
text = it.title,
|
||||
onClick = { onLinkClick(it) },
|
||||
icon = TangemButtonIconPosition.Start(iconResId = it.iconRes),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LinksBlockPlaceholder(modifier: Modifier = Modifier) {
|
||||
InformationBlock(
|
||||
modifier = modifier,
|
||||
title = {
|
||||
TextShimmer(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
)
|
||||
},
|
||||
content = {
|
||||
Column {
|
||||
SubBlockPlaceholder()
|
||||
SubBlockPlaceholder()
|
||||
SubBlockPlaceholder(lastBlock = true)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SubBlockPlaceholder(modifier: Modifier = Modifier, lastBlock: Boolean = false) {
|
||||
DividerContainer(
|
||||
modifier = modifier,
|
||||
showDivider = !lastBlock,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing12),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
TextShimmer(
|
||||
modifier = Modifier.width(78.dp),
|
||||
style = TangemTheme.typography.caption2,
|
||||
)
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
repeat(times = 3) {
|
||||
SmallButtonShimmer(
|
||||
modifier = Modifier.weight(1f),
|
||||
withIcon = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun ContentPreview() {
|
||||
TangemThemePreview {
|
||||
LinksBlock(
|
||||
state = LinksUM(
|
||||
officialLinks = persistentListOf(
|
||||
LinksUM.Link(
|
||||
title = stringReference("Website"),
|
||||
iconRes = R.drawable.ic_plus_24,
|
||||
url = "https://tangem.com",
|
||||
),
|
||||
LinksUM.Link(
|
||||
title = stringReference("Website"),
|
||||
iconRes = R.drawable.ic_plus_24,
|
||||
url = "https://tangem.com",
|
||||
),
|
||||
LinksUM.Link(
|
||||
title = stringReference("Website"),
|
||||
iconRes = R.drawable.ic_plus_24,
|
||||
url = "https://tangem.com",
|
||||
),
|
||||
),
|
||||
social = persistentListOf(
|
||||
LinksUM.Link(
|
||||
title = stringReference("Twitter"),
|
||||
iconRes = R.drawable.ic_plus_24,
|
||||
url = "https://tangem.com",
|
||||
),
|
||||
LinksUM.Link(
|
||||
title = stringReference("Facebook"),
|
||||
iconRes = R.drawable.ic_plus_24,
|
||||
url = "https://tangem.com",
|
||||
),
|
||||
),
|
||||
repository = persistentListOf(
|
||||
LinksUM.Link(
|
||||
title = stringReference("Github"),
|
||||
iconRes = R.drawable.ic_plus_24,
|
||||
url = "https://tangem.com",
|
||||
),
|
||||
),
|
||||
blockchainSite = persistentListOf(),
|
||||
onLinkClick = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun PlaceholderPreview() {
|
||||
TangemThemePreview {
|
||||
PreviewShimmerContainer(
|
||||
shimmerContent = { LinksBlockPlaceholder() },
|
||||
actualContent = { ContentPreview() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
package com.tangem.features.markets.details.impl.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import com.tangem.common.ui.charts.MarketChart
|
||||
import com.tangem.common.ui.charts.getMarketChartBottomAxisHeight
|
||||
import com.tangem.common.ui.charts.state.MarketChartLook
|
||||
import com.tangem.common.ui.charts.state.rememberMarketChartState
|
||||
import com.tangem.core.ui.res.LocalMainBottomSheetColor
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.components.UnableToLoadData
|
||||
|
||||
@Composable
|
||||
internal fun MarketTokenDetailsChart(state: MarketsTokenDetailsUM.ChartState, modifier: Modifier = Modifier) {
|
||||
val growingColor = TangemTheme.colors.icon.accent
|
||||
val fallingColor = TangemTheme.colors.icon.warning
|
||||
|
||||
val chartState = rememberMarketChartState(
|
||||
dataProducer = state.dataProducer,
|
||||
colorMapper = {
|
||||
when (it) {
|
||||
MarketChartLook.Type.Growing -> growingColor
|
||||
MarketChartLook.Type.Falling -> fallingColor
|
||||
}
|
||||
},
|
||||
onMarkerShown = state.onMarkerPointSelected,
|
||||
)
|
||||
|
||||
val backgroundColor = LocalMainBottomSheetColor.current.value
|
||||
val bottomChartAxisHeight = getMarketChartBottomAxisHeight()
|
||||
|
||||
Box(modifier) {
|
||||
MarketChart(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
state = chartState,
|
||||
)
|
||||
|
||||
if (state.status != MarketsTokenDetailsUM.ChartState.Status.DATA) {
|
||||
Box(
|
||||
Modifier
|
||||
.drawBehind { drawRect(backgroundColor) }
|
||||
.matchParentSize()
|
||||
.padding(bottom = bottomChartAxisHeight),
|
||||
) {
|
||||
when (state.status) {
|
||||
MarketsTokenDetailsUM.ChartState.Status.LOADING -> {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size16)
|
||||
.align(Alignment.Center),
|
||||
color = TangemTheme.colors.text.accent,
|
||||
strokeWidth = TangemTheme.dimens.size2,
|
||||
)
|
||||
}
|
||||
MarketsTokenDetailsUM.ChartState.Status.ERROR -> {
|
||||
UnableToLoadData(
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
horizontal = TangemTheme.dimens.spacing16,
|
||||
vertical = TangemTheme.dimens.spacing12,
|
||||
)
|
||||
.align(Alignment.Center),
|
||||
onRetryClick = state.onLoadRetryClick,
|
||||
)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,166 @@
|
|||
package com.tangem.features.markets.details.impl.ui.components
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.components.TextButton
|
||||
import com.tangem.core.ui.components.TextShimmer
|
||||
import com.tangem.core.ui.components.block.information.GridItems
|
||||
import com.tangem.core.ui.components.block.information.InformationBlock
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.utils.PreviewShimmerContainer
|
||||
import com.tangem.features.markets.details.impl.ui.state.InfoPointUM
|
||||
import com.tangem.features.markets.details.impl.ui.state.MetricsUM
|
||||
import com.tangem.features.markets.impl.R
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
const val MAX_METRICS_COUNT = 6
|
||||
|
||||
@Composable
|
||||
internal fun MetricsBlock(state: MetricsUM, modifier: Modifier = Modifier) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
||||
InformationBlock(
|
||||
modifier = modifier,
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(id = R.string.markets_token_details_metrics),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
},
|
||||
action = {
|
||||
if (state.metrics.size > MAX_METRICS_COUNT) {
|
||||
ShowLessMoreButton(expanded = expanded, onClick = { expanded = !expanded })
|
||||
}
|
||||
},
|
||||
content = {
|
||||
val metrics = if (expanded) {
|
||||
state.metrics
|
||||
} else {
|
||||
state.metrics.take(MAX_METRICS_COUNT).toImmutableList()
|
||||
}
|
||||
|
||||
GridItems(
|
||||
items = metrics,
|
||||
itemContent = {
|
||||
InfoPoint(infoPointUM = it)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// TODO make TextButton clickable area smaller and remove paddings for an action in InformationBlock
|
||||
@Composable
|
||||
private fun ShowLessMoreButton(expanded: Boolean, onClick: () -> Unit) {
|
||||
// FIXME add string resources
|
||||
val text = if (expanded) {
|
||||
"See less"
|
||||
} else {
|
||||
"See more"
|
||||
}
|
||||
|
||||
TextButton(
|
||||
text = text,
|
||||
onClick = onClick,
|
||||
colors = TangemButtonsDefaults.positiveButtonColors,
|
||||
textStyle = TangemTheme.typography.body2,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun MetricsBlockPlaceholder(modifier: Modifier = Modifier) {
|
||||
InformationBlock(
|
||||
modifier = modifier,
|
||||
title = {
|
||||
TextShimmer(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
radius = TangemTheme.dimens.radius3,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
)
|
||||
},
|
||||
action = {
|
||||
Box(Modifier)
|
||||
},
|
||||
content = {
|
||||
GridItems(
|
||||
items = List(size = 6) { it }.toImmutableList(),
|
||||
horizontalArragement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
itemContent = {
|
||||
InfoPointShimmer(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
withTooltip = true,
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview("Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun BlockPreview() {
|
||||
TangemThemePreview {
|
||||
MetricsBlock(
|
||||
state = MetricsUM(
|
||||
metrics = persistentListOf(
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_market_capitalization),
|
||||
value = "1.2T",
|
||||
onInfoClick = {},
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_market_rating),
|
||||
value = "A",
|
||||
onInfoClick = {},
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_trading_volume),
|
||||
value = "1.2T",
|
||||
onInfoClick = {},
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_fully_diluted_valuation),
|
||||
value = "1.2T",
|
||||
onInfoClick = {},
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_circulating_supply),
|
||||
value = "1.2T",
|
||||
onInfoClick = {},
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_total_supply),
|
||||
value = "1.2T",
|
||||
onInfoClick = {},
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview("Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun PreviewPlaceholder() {
|
||||
TangemThemePreview {
|
||||
PreviewShimmerContainer(
|
||||
actualContent = { BlockPreview() },
|
||||
shimmerContent = { MetricsBlockPlaceholder() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,248 @@
|
|||
package com.tangem.features.markets.details.impl.ui.components
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.components.SpacerW8
|
||||
import com.tangem.core.ui.components.TextShimmer
|
||||
import com.tangem.core.ui.components.block.information.InformationBlock
|
||||
import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemAnimations
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.utils.PreviewShimmerContainer
|
||||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import com.tangem.features.markets.details.impl.ui.state.PricePerformanceUM
|
||||
import com.tangem.features.markets.details.impl.ui.getText
|
||||
import com.tangem.features.markets.impl.R
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@Composable
|
||||
internal fun PricePerformanceBlock(state: PricePerformanceUM, modifier: Modifier = Modifier) {
|
||||
var currentInterval by remember { mutableStateOf(PriceChangeInterval.H24) }
|
||||
|
||||
InformationBlock(
|
||||
modifier = modifier,
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(id = R.string.markets_token_details_price_performance),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
},
|
||||
action = {
|
||||
SegmentedButtons(
|
||||
config = persistentListOf(
|
||||
PriceChangeInterval.H24,
|
||||
PriceChangeInterval.MONTH,
|
||||
PriceChangeInterval.ALL_TIME,
|
||||
),
|
||||
initialSelectedItem = PriceChangeInterval.H24,
|
||||
onClick = { currentInterval = it },
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.align(Alignment.Center)
|
||||
.padding(vertical = TangemTheme.dimens.spacing4),
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
text = it.getText().resolveReference(),
|
||||
style = TangemTheme.typography.caption1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
content = {
|
||||
val value = when (currentInterval) {
|
||||
PriceChangeInterval.H24 -> state.h24
|
||||
PriceChangeInterval.MONTH -> state.month
|
||||
PriceChangeInterval.ALL_TIME -> state.all
|
||||
else -> error("")
|
||||
}
|
||||
|
||||
Content(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
state = value,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Content(state: PricePerformanceUM.Value, modifier: Modifier = Modifier) {
|
||||
val animatedIndicatorFraction by TangemAnimations.horizontalIndicatorAsState(
|
||||
targetFraction = state.indicatorFraction,
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.padding(vertical = TangemTheme.dimens.spacing8),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.markets_token_details_low),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
SpacerW8()
|
||||
Text(
|
||||
text = stringResource(R.string.markets_token_details_high),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
LinearProgressIndicator(
|
||||
modifier = Modifier
|
||||
.height(TangemTheme.dimens.size6)
|
||||
.fillMaxWidth(),
|
||||
progress = { animatedIndicatorFraction },
|
||||
color = TangemTheme.colors.text.accent,
|
||||
trackColor = TangemTheme.colors.background.tertiary,
|
||||
strokeCap = StrokeCap.Round,
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Text(
|
||||
text = state.low,
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
SpacerW8()
|
||||
Text(
|
||||
text = state.high,
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.End,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun PricePerformanceBlockPlaceholder(modifier: Modifier = Modifier) {
|
||||
val subtitle2dp = with(LocalDensity.current) { TangemTheme.typography.subtitle2.lineHeight.toDp() }
|
||||
val caption1dp = with(LocalDensity.current) { TangemTheme.typography.caption1.lineHeight.toDp() }
|
||||
val headerHeight = maxOf(subtitle2dp, caption1dp) + TangemTheme.dimens.spacing4
|
||||
|
||||
InformationBlock(
|
||||
modifier = modifier,
|
||||
title = {
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.height(headerHeight)
|
||||
.fillMaxWidth(),
|
||||
radius = TangemTheme.dimens.radius3,
|
||||
)
|
||||
},
|
||||
content = {
|
||||
Column(
|
||||
modifier = modifier.padding(vertical = TangemTheme.dimens.spacing8),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
TextShimmer(
|
||||
modifier = Modifier.width(35.dp),
|
||||
style = TangemTheme.typography.caption2,
|
||||
)
|
||||
SpacerW8()
|
||||
TextShimmer(
|
||||
modifier = Modifier.width(35.dp),
|
||||
style = TangemTheme.typography.caption2,
|
||||
)
|
||||
}
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.height(TangemTheme.dimens.size6)
|
||||
.fillMaxWidth(),
|
||||
radius = 27.dp,
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
TextShimmer(
|
||||
modifier = Modifier.width(TangemTheme.dimens.size56),
|
||||
style = TangemTheme.typography.body1,
|
||||
)
|
||||
SpacerW8()
|
||||
TextShimmer(
|
||||
modifier = Modifier.width(TangemTheme.dimens.size56),
|
||||
style = TangemTheme.typography.body1,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview("Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun ContentPreview() {
|
||||
TangemThemePreview {
|
||||
PricePerformanceBlock(
|
||||
modifier = Modifier,
|
||||
state = PricePerformanceUM(
|
||||
h24 = PricePerformanceUM.Value(
|
||||
low = "\$38,5K",
|
||||
high = "\$58,5K",
|
||||
indicatorFraction = 0.5f,
|
||||
),
|
||||
month = PricePerformanceUM.Value(
|
||||
low = "\$500,5K",
|
||||
high = "\$5800,5K",
|
||||
indicatorFraction = 0.8f,
|
||||
),
|
||||
all = PricePerformanceUM.Value(
|
||||
low = "\$58,52",
|
||||
high = "\$580,5M",
|
||||
indicatorFraction = 0.2f,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun PlaceholderPreview() {
|
||||
TangemThemePreview {
|
||||
PreviewShimmerContainer(
|
||||
shimmerContent = {
|
||||
PricePerformanceBlockPlaceholder()
|
||||
},
|
||||
actualContent = {
|
||||
ContentPreview()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,203 @@
|
|||
package com.tangem.features.markets.details.impl.ui.components
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.annotation.FloatRange
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawWithCache
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.BlendMode
|
||||
import androidx.compose.ui.graphics.CompositingStrategy
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.components.TextShimmer
|
||||
import com.tangem.core.ui.components.block.information.InformationBlock
|
||||
import com.tangem.core.ui.components.text.TooltipText
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.utils.PreviewShimmerContainer
|
||||
import com.tangem.features.markets.details.impl.ui.state.SecurityScoreUM
|
||||
import com.tangem.features.markets.impl.R
|
||||
import kotlin.math.round
|
||||
|
||||
private const val STARS_COUNT = 5
|
||||
|
||||
@Composable
|
||||
internal fun SecurityScoreBlock(state: SecurityScoreUM, modifier: Modifier = Modifier) {
|
||||
val rounded = state.score.roundTo1decimal()
|
||||
val percentage = rounded / STARS_COUNT
|
||||
InformationBlock(
|
||||
modifier = modifier,
|
||||
title = {
|
||||
Column(
|
||||
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing6),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4),
|
||||
) {
|
||||
TooltipText(
|
||||
text = resourceReference(R.string.markets_token_details_security_score),
|
||||
onInfoClick = state.onInfoClick,
|
||||
textStyle = TangemTheme.typography.subtitle2,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = state.description,
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
},
|
||||
action = {
|
||||
Row(
|
||||
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing6),
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = rounded.toString(),
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
Stars(fraction = percentage)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
@Composable
|
||||
private fun Stars(@FloatRange(0.0, 1.0) fraction: Float = 0f) {
|
||||
val grayColor = TangemTheme.colors.icon.inactive
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
repeat(times = 5) { i ->
|
||||
Box(
|
||||
modifier = Modifier.size(TangemTheme.dimens.size16),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.requiredSize(13.dp)
|
||||
.graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen)
|
||||
.drawWithCache {
|
||||
onDrawWithContent {
|
||||
val starFraction = ((fraction - i * 0.2) / 0.2).coerceIn(0.0, 1.0)
|
||||
val starFractionFloat = starFraction
|
||||
.toFloat()
|
||||
.roundTo1decimal()
|
||||
|
||||
drawContent()
|
||||
drawRect(
|
||||
color = grayColor,
|
||||
topLeft = Offset(x = size.width * starFractionFloat, y = 0f),
|
||||
size = Size(size.width * (1 - starFractionFloat), size.height),
|
||||
blendMode = BlendMode.SrcIn,
|
||||
)
|
||||
}
|
||||
},
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_star_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.accent,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private fun Float.roundTo1decimal(): Float {
|
||||
return round(this * 10) / 10
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun SecurityScorePlaceHolder(modifier: Modifier = Modifier) {
|
||||
InformationBlock(
|
||||
modifier = modifier,
|
||||
title = {
|
||||
Column(
|
||||
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing6),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4),
|
||||
) {
|
||||
TextShimmer(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
textSizeHeight = true,
|
||||
)
|
||||
TextShimmer(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
style = TangemTheme.typography.body2,
|
||||
textSizeHeight = true,
|
||||
)
|
||||
}
|
||||
},
|
||||
action = {
|
||||
Box(
|
||||
modifier = Modifier.padding(
|
||||
start = TangemTheme.dimens.spacing24,
|
||||
bottom = TangemTheme.dimens.spacing6,
|
||||
),
|
||||
contentAlignment = Alignment.CenterEnd,
|
||||
) {
|
||||
TextShimmer(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
style = TangemTheme.typography.body2,
|
||||
textSizeHeight = true,
|
||||
)
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.height(TangemTheme.dimens.size16)
|
||||
.fillMaxWidth(),
|
||||
radius = TangemTheme.dimens.radius3,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun ContentPreview() {
|
||||
TangemThemePreview {
|
||||
SecurityScoreBlock(
|
||||
state = SecurityScoreUM(
|
||||
score = 3.5f,
|
||||
description = "Based on 3 ratings",
|
||||
onInfoClick = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun PreviewPlaceholder() {
|
||||
TangemThemePreview {
|
||||
PreviewShimmerContainer(
|
||||
shimmerContent = {
|
||||
SecurityScorePlaceHolder(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
},
|
||||
actualContent = {
|
||||
ContentPreview()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
package com.tangem.features.markets.details.impl.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.components.UnableToLoadData
|
||||
|
||||
internal fun LazyListScope.tokenMarketDetailsBody(state: MarketsTokenDetailsUM.Body) {
|
||||
when (state) {
|
||||
MarketsTokenDetailsUM.Body.Loading -> {
|
||||
loading()
|
||||
}
|
||||
is MarketsTokenDetailsUM.Body.Content -> {
|
||||
if (state.description != null) {
|
||||
description(state.description)
|
||||
}
|
||||
|
||||
infoBlocksList(state.infoBlocks)
|
||||
}
|
||||
is MarketsTokenDetailsUM.Body.Error -> {
|
||||
error(state)
|
||||
}
|
||||
MarketsTokenDetailsUM.Body.Nothing -> {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.error(state: MarketsTokenDetailsUM.Body.Error) {
|
||||
item("body-error") {
|
||||
Box(Modifier.fillMaxWidth()) {
|
||||
UnableToLoadData(
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.padding(
|
||||
horizontal = TangemTheme.dimens.spacing16,
|
||||
vertical = TangemTheme.dimens.spacing40,
|
||||
),
|
||||
onRetryClick = state.onLoadRetryClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.description(description: MarketsTokenDetailsUM.Description) {
|
||||
item("description") {
|
||||
Description(
|
||||
modifier = Modifier.blockPaddings(),
|
||||
description = description.shortDescription,
|
||||
hasFullDescription = description.fullDescription != null,
|
||||
onReadMoreClick = description.onReadMoreClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun LazyListScope.infoBlocksList(state: MarketsTokenDetailsUM.InformationBlocks) {
|
||||
if (state.insights != null) {
|
||||
item("insights") {
|
||||
InsightsBlock(
|
||||
modifier = Modifier.blockPaddings(),
|
||||
state = state.insights,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (state.securityScore != null) {
|
||||
item("securityScore") {
|
||||
SecurityScoreBlock(
|
||||
modifier = Modifier.blockPaddings(),
|
||||
state = state.securityScore,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (state.metrics != null) {
|
||||
item("metrics") {
|
||||
MetricsBlock(
|
||||
modifier = Modifier.blockPaddings(),
|
||||
state = state.metrics,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (state.pricePerformance != null) {
|
||||
item("pricePerformance") {
|
||||
PricePerformanceBlock(
|
||||
modifier = Modifier.blockPaddings(),
|
||||
state = state.pricePerformance,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (state.links != null) {
|
||||
item("links") {
|
||||
LinksBlock(
|
||||
modifier = Modifier.blockPaddings(),
|
||||
state = state.links,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.loading() {
|
||||
item("description-loading") {
|
||||
DescriptionPlaceholder(modifier = Modifier.blockPaddings())
|
||||
}
|
||||
|
||||
item("insights-loading") {
|
||||
InsightsBlockPlaceholder(modifier = Modifier.blockPaddings())
|
||||
}
|
||||
|
||||
item("securityScore-loading") {
|
||||
SecurityScorePlaceHolder(modifier = Modifier.blockPaddings())
|
||||
}
|
||||
|
||||
item("metrics-loading") {
|
||||
MetricsBlockPlaceholder(modifier = Modifier.blockPaddings())
|
||||
}
|
||||
|
||||
item("pricePerformance-loading") {
|
||||
PricePerformanceBlockPlaceholder(modifier = Modifier.blockPaddings())
|
||||
}
|
||||
|
||||
item("links-loading") {
|
||||
LinksBlockPlaceholder(modifier = Modifier.blockPaddings())
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Modifier.blockPaddings(): Modifier {
|
||||
return this.padding(
|
||||
start = TangemTheme.dimens.spacing16,
|
||||
end = TangemTheme.dimens.spacing16,
|
||||
bottom = TangemTheme.dimens.spacing12,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.tangem.features.markets.details.impl.ui.state
|
||||
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
internal data class InfoBottomSheetContent(
|
||||
val title: TextReference,
|
||||
val body: TextReference,
|
||||
) : TangemBottomSheetConfigContent
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.tangem.features.markets.details.impl.ui.state
|
||||
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
internal data class InfoPointUM(
|
||||
val title: TextReference,
|
||||
val value: String,
|
||||
val onInfoClick: (() -> Unit)? = null,
|
||||
)
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.tangem.features.markets.details.impl.ui.state
|
||||
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
internal data class InsightsUM(
|
||||
val h24Info: ImmutableList<InfoPointUM>,
|
||||
val weekInfo: ImmutableList<InfoPointUM>,
|
||||
val monthInfo: ImmutableList<InfoPointUM>,
|
||||
)
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.tangem.features.markets.details.impl.ui.state
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
internal data class LinksUM(
|
||||
val officialLinks: ImmutableList<Link>,
|
||||
val social: ImmutableList<Link>,
|
||||
val repository: ImmutableList<Link>,
|
||||
val blockchainSite: ImmutableList<Link>,
|
||||
val onLinkClick: (Link) -> Unit,
|
||||
) {
|
||||
data class Link(
|
||||
@DrawableRes val iconRes: Int,
|
||||
val title: TextReference,
|
||||
val url: String,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
package com.tangem.features.markets.details.impl.ui.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.common.ui.charts.state.MarketChartDataProducer
|
||||
import com.tangem.common.ui.charts.state.MarketChartLook
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal data class MarketsTokenDetailsUM(
|
||||
val tokenName: String,
|
||||
val priceText: String,
|
||||
val iconUrl: String,
|
||||
val dateTimeText: TextReference,
|
||||
val priceChangePercentText: String,
|
||||
val priceChangeType: PriceChangeType,
|
||||
val selectedInterval: PriceChangeInterval,
|
||||
val chartState: ChartState,
|
||||
val onSelectedIntervalChange: (PriceChangeInterval) -> Unit,
|
||||
val infoBottomSheet: TangemBottomSheetConfig,
|
||||
val body: Body,
|
||||
) {
|
||||
|
||||
data class ChartState(
|
||||
val status: Status,
|
||||
val dataProducer: MarketChartDataProducer,
|
||||
val chartLook: MarketChartLook,
|
||||
val onLoadRetryClick: () -> Unit,
|
||||
val onMarkerPointSelected: (time: BigDecimal?, price: BigDecimal?) -> Unit,
|
||||
) {
|
||||
enum class Status {
|
||||
LOADING, ERROR, DATA
|
||||
}
|
||||
}
|
||||
|
||||
data class InformationBlocks(
|
||||
val insights: InsightsUM?,
|
||||
val securityScore: SecurityScoreUM?,
|
||||
val metrics: MetricsUM?,
|
||||
val pricePerformance: PricePerformanceUM?,
|
||||
val links: LinksUM?,
|
||||
)
|
||||
|
||||
@Immutable
|
||||
sealed interface Body {
|
||||
|
||||
data class Error(
|
||||
val onLoadRetryClick: () -> Unit,
|
||||
) : Body
|
||||
|
||||
data object Loading : Body
|
||||
|
||||
data class Content(
|
||||
val description: Description?,
|
||||
val infoBlocks: InformationBlocks,
|
||||
) : Body
|
||||
|
||||
data object Nothing : Body
|
||||
}
|
||||
|
||||
data class Description(
|
||||
val shortDescription: TextReference,
|
||||
val fullDescription: TextReference?,
|
||||
val onReadMoreClick: () -> Unit,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.tangem.features.markets.details.impl.ui.state
|
||||
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
|
||||
internal data class MetricsUM(
|
||||
val metrics: PersistentList<InfoPointUM>,
|
||||
)
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.features.markets.details.impl.ui.state
|
||||
|
||||
import androidx.annotation.FloatRange
|
||||
|
||||
internal data class PricePerformanceUM(
|
||||
val h24: Value,
|
||||
val month: Value,
|
||||
val all: Value,
|
||||
) {
|
||||
data class Value(
|
||||
val low: String,
|
||||
val high: String,
|
||||
@FloatRange(from = 0.0, to = 1.0) val indicatorFraction: Float,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.tangem.features.markets.details.impl.ui.state
|
||||
|
||||
import androidx.annotation.FloatRange
|
||||
|
||||
internal data class SecurityScoreUM(
|
||||
@FloatRange(from = 0.0, to = 5.0) val score: Float,
|
||||
val description: String,
|
||||
val onInfoClick: () -> Unit,
|
||||
)
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.features.markets.di
|
||||
|
||||
import com.tangem.features.markets.component.MarketsListComponent
|
||||
import com.tangem.features.markets.component.impl.DefaultMarketsListComponent
|
||||
import com.tangem.features.markets.component.MarketsEntryComponent
|
||||
import com.tangem.features.markets.DefaultMarketsEntryComponent
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -14,5 +14,5 @@ internal interface ComponentModule {
|
|||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindMarketsListComponent(factory: DefaultMarketsListComponent.Factory): MarketsListComponent.Factory
|
||||
fun bindMarketsListComponent(factory: DefaultMarketsEntryComponent.Factory): MarketsEntryComponent.Factory
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package com.tangem.features.markets.tokenlist.api
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.TokenMarket
|
||||
import com.tangem.features.markets.component.BottomSheetState
|
||||
|
||||
@Stable
|
||||
interface MarketsTokenListComponent {
|
||||
|
||||
@Composable
|
||||
fun BottomSheetContent(
|
||||
bottomSheetState: State<BottomSheetState>,
|
||||
onHeaderSizeChange: (Dp) -> Unit,
|
||||
modifier: Modifier,
|
||||
)
|
||||
|
||||
interface Factory {
|
||||
fun create(
|
||||
context: AppComponentContext,
|
||||
onTokenSelected: (TokenMarket, AppCurrency) -> Unit,
|
||||
): MarketsTokenListComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
package com.tangem.features.markets.tokenlist.impl
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.lifecycle.compose.LifecycleStartEffect
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.TokenMarket
|
||||
import com.tangem.features.markets.component.BottomSheetState
|
||||
import com.tangem.features.markets.tokenlist.api.MarketsTokenListComponent
|
||||
import com.tangem.features.markets.tokenlist.impl.model.MarketsListModel
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.MarketsList
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
||||
class DefaultMarketsTokenListComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted private val onTokenSelected: (TokenMarket, AppCurrency) -> Unit,
|
||||
) : AppComponentContext by appComponentContext, MarketsTokenListComponent {
|
||||
|
||||
private val model: MarketsListModel = getOrCreateModel()
|
||||
|
||||
init {
|
||||
model.tokenSelected
|
||||
.onEach { onTokenSelected(it.first, it.second) }
|
||||
.launchIn(componentScope)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun BottomSheetContent(
|
||||
bottomSheetState: State<BottomSheetState>,
|
||||
onHeaderSizeChange: (Dp) -> Unit,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
LifecycleStartEffect(Unit) {
|
||||
model.isVisibleOnScreen.value = true
|
||||
onStopOrDispose {
|
||||
model.isVisibleOnScreen.value = false
|
||||
}
|
||||
}
|
||||
|
||||
val state by model.state.collectAsStateWithLifecycle()
|
||||
val bsState by bottomSheetState
|
||||
|
||||
LaunchedEffect(bsState) {
|
||||
model.containerBottomSheetState.value = bsState
|
||||
}
|
||||
|
||||
MarketsList(
|
||||
modifier = modifier,
|
||||
state = state,
|
||||
onHeaderSizeChange = onHeaderSizeChange,
|
||||
bottomSheetState = bsState,
|
||||
)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : MarketsTokenListComponent.Factory {
|
||||
override fun create(
|
||||
context: AppComponentContext,
|
||||
onTokenSelected: (TokenMarket, AppCurrency) -> Unit,
|
||||
): DefaultMarketsTokenListComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.features.markets.tokenlist.impl.di
|
||||
|
||||
import com.tangem.features.markets.tokenlist.api.MarketsTokenListComponent
|
||||
import com.tangem.features.markets.tokenlist.impl.DefaultMarketsTokenListComponent
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface ComponentModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindMarketsTokenListComponent(
|
||||
factory: DefaultMarketsTokenListComponent.Factory,
|
||||
): MarketsTokenListComponent.Factory
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
package com.tangem.features.markets.di
|
||||
package com.tangem.features.markets.tokenlist.impl.di
|
||||
|
||||
import com.tangem.core.decompose.di.DecomposeComponent
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.features.markets.model.MarketsListModel
|
||||
import com.tangem.features.markets.tokenlist.impl.model.MarketsListModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.features.markets.model
|
||||
package com.tangem.features.markets.tokenlist.impl.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import arrow.core.getOrElse
|
||||
|
|
@ -7,11 +7,13 @@ import com.tangem.core.decompose.model.Model
|
|||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase
|
||||
import com.tangem.domain.markets.TokenMarket
|
||||
import com.tangem.features.markets.component.BottomSheetState
|
||||
import com.tangem.features.markets.model.statemanager.MarketsListUMStateManager
|
||||
import com.tangem.features.markets.model.statemanager.MarketsListBatchFlowManager
|
||||
import com.tangem.features.markets.ui.entity.ListUM
|
||||
import com.tangem.features.markets.ui.entity.SortByTypeUM
|
||||
import com.tangem.features.markets.tokenlist.impl.model.statemanager.MarketsListUMStateManager
|
||||
import com.tangem.features.markets.tokenlist.impl.model.statemanager.MarketsListBatchFlowManager
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.state.ListUM
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListItemUM
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.state.SortByTypeUM
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
|
|
@ -32,6 +34,8 @@ internal class MarketsListModel @Inject constructor(
|
|||
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
) : Model() {
|
||||
|
||||
private var updateQuotesJob = JobHolder()
|
||||
|
||||
private val currentAppCurrency = getSelectedAppCurrencyUseCase()
|
||||
.map { maybeAppCurrency ->
|
||||
maybeAppCurrency.getOrElse { AppCurrency.Default }
|
||||
|
|
@ -47,8 +51,8 @@ internal class MarketsListModel @Inject constructor(
|
|||
onLoadMoreUiItems = { activeListManager.loadMore() },
|
||||
visibleItemsChanged = { visibleItemIds.value = it },
|
||||
onRetryButtonClicked = { activeListManager.reload() },
|
||||
onTokenClick = { onTokenUIClicked(it) },
|
||||
)
|
||||
|
||||
private val mainMarketsListManager = MarketsListBatchFlowManager(
|
||||
getMarketsTokenListFlowUseCase = getMarketsTokenListFlowUseCase,
|
||||
batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Main,
|
||||
|
|
@ -59,6 +63,7 @@ internal class MarketsListModel @Inject constructor(
|
|||
modelScope = modelScope,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
|
||||
private val searchMarketsListManager = MarketsListBatchFlowManager(
|
||||
getMarketsTokenListFlowUseCase = getMarketsTokenListFlowUseCase,
|
||||
batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Search,
|
||||
|
|
@ -72,7 +77,12 @@ internal class MarketsListModel @Inject constructor(
|
|||
|
||||
private var activeListManager: MarketsListBatchFlowManager = mainMarketsListManager
|
||||
|
||||
private val _tokenSelected = MutableSharedFlow<Pair<TokenMarket, AppCurrency>>()
|
||||
|
||||
val tokenSelected = _tokenSelected.asSharedFlow()
|
||||
|
||||
val containerBottomSheetState = MutableStateFlow(BottomSheetState.COLLAPSED)
|
||||
val isVisibleOnScreen = MutableStateFlow(false)
|
||||
|
||||
val state = marketsListUMStateManager.state.asStateFlow()
|
||||
|
||||
|
|
@ -169,6 +179,7 @@ internal class MarketsListModel @Inject constructor(
|
|||
}
|
||||
.distinctUntilChanged()
|
||||
.collectLatest { visibleBatchKeys ->
|
||||
// TODO load batch on scroll heat area
|
||||
activeListManager.loadCharts(visibleBatchKeys, marketsListUMStateManager.selectedInterval)
|
||||
}
|
||||
}
|
||||
|
|
@ -189,9 +200,12 @@ internal class MarketsListModel @Inject constructor(
|
|||
|
||||
modelScope.launch {
|
||||
marketsListUMStateManager.searchQueryFlow
|
||||
.filter { it.isNotEmpty() }
|
||||
.debounce(timeoutMillis = SEARCH_QUERY_DEBOUNCE_MILLIS)
|
||||
.filter { activeListManager == searchMarketsListManager }
|
||||
.distinctUntilChanged()
|
||||
.onEach {
|
||||
if (it.isEmpty()) searchMarketsListManager.clearStateAndStopAllActions()
|
||||
}
|
||||
.filter { it.isNotEmpty() && activeListManager == searchMarketsListManager }
|
||||
.collectLatest {
|
||||
searchMarketsListManager.reload(searchText = it)
|
||||
}
|
||||
|
|
@ -210,14 +224,24 @@ internal class MarketsListModel @Inject constructor(
|
|||
mainMarketsListManager.reload()
|
||||
}
|
||||
|
||||
private var updateQuotesJob = JobHolder()
|
||||
private fun onTokenUIClicked(token: MarketsListItemUM) {
|
||||
modelScope.launch {
|
||||
activeListManager.getTokenById(token.id)?.let { found ->
|
||||
_tokenSelected.emit(found to currentAppCurrency.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun CoroutineScope.loadQuotesWithTimer(timeMillis: Long) {
|
||||
launch {
|
||||
while (true) {
|
||||
delay(timeMillis)
|
||||
// Update quotes only when the container bottom sheet is in the expanded state
|
||||
containerBottomSheetState.first { it == BottomSheetState.EXPANDED }
|
||||
activeListManager.updateQuotes() // TODO update a batch that is currently on screen
|
||||
// and is visible on the screen
|
||||
isVisibleOnScreen.first { it }
|
||||
|
||||
activeListManager.updateQuotes()
|
||||
}
|
||||
}.saveIn(updateQuotesJob)
|
||||
}
|
||||
|
|
@ -1,15 +1,16 @@
|
|||
package com.tangem.features.markets.model.converters
|
||||
package com.tangem.features.markets.tokenlist.impl.model.converters
|
||||
|
||||
import com.tangem.common.ui.charts.state.DefaultPointValuesConverter
|
||||
import com.tangem.common.ui.charts.state.converter.PriceAndTimePointValuesConverter
|
||||
import com.tangem.common.ui.charts.state.MarketChartData
|
||||
import com.tangem.common.ui.charts.state.MarketChartRawData
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.TokenMarket
|
||||
import com.tangem.features.markets.ui.entity.MarketsListItemUM
|
||||
import com.tangem.features.markets.ui.entity.MarketsListUM.TrendInterval
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListItemUM
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListUM.TrendInterval
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
|
||||
|
|
@ -18,6 +19,8 @@ internal class MarketsTokenItemConverter(
|
|||
private val appCurrency: AppCurrency,
|
||||
) : Converter<TokenMarket, MarketsListItemUM> {
|
||||
|
||||
private val priceAndTimePointValuesConverter = PriceAndTimePointValuesConverter(needToFormatAxis = false)
|
||||
|
||||
override fun convert(value: TokenMarket): MarketsListItemUM {
|
||||
return MarketsListItemUM(
|
||||
id = value.id,
|
||||
|
|
@ -30,7 +33,7 @@ internal class MarketsTokenItemConverter(
|
|||
trendPercentText = value.getTrendPercent(),
|
||||
trendType = value.getTrendType(),
|
||||
chardData = value.getChartData(),
|
||||
showUnder100kMarketCap = value.isUnder100kMarketCap(),
|
||||
isUnder100kMarketCap = value.isUnder100kMarketCap(),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -67,10 +70,11 @@ internal class MarketsTokenItemConverter(
|
|||
private fun TokenMarket.getMarketCap(): String? {
|
||||
val value = marketCap?.takeIf { marketCap != BigDecimal.ZERO } ?: return null
|
||||
|
||||
return BigDecimalFormatter.formatCompactAmount(
|
||||
value,
|
||||
return BigDecimalFormatter.formatCompactFiatAmount(
|
||||
amount = value,
|
||||
fiatCurrencyCode = appCurrency.code,
|
||||
fiatCurrencySymbol = appCurrency.symbol,
|
||||
threeDigitsMethod = true,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -107,10 +111,10 @@ internal class MarketsTokenItemConverter(
|
|||
}
|
||||
|
||||
return chart?.let { ct ->
|
||||
DefaultPointValuesConverter.convert(
|
||||
priceAndTimePointValuesConverter.convert(
|
||||
MarketChartData.Data(
|
||||
y = ct.priceY,
|
||||
x = ct.timeStamp.map { it.toBigDecimal() },
|
||||
y = ct.priceY.toImmutableList(),
|
||||
x = ct.timeStamps.map { it.toBigDecimal() }.toImmutableList(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -144,7 +148,7 @@ internal class MarketsTokenItemConverter(
|
|||
}
|
||||
|
||||
private fun TokenMarket.isUnder100kMarketCap(): Boolean {
|
||||
return tokenQuotes.currentPrice.compareTo(decimal100k) == -1
|
||||
return marketCap?.let { it < decimal100k } ?: true
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
|
@ -1,23 +1,27 @@
|
|||
package com.tangem.features.markets.model.statemanager
|
||||
package com.tangem.features.markets.tokenlist.impl.model.statemanager
|
||||
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.*
|
||||
import com.tangem.features.markets.model.converters.MarketsTokenItemConverter
|
||||
import com.tangem.features.markets.model.utils.logAction
|
||||
import com.tangem.features.markets.model.utils.logStatus
|
||||
import com.tangem.features.markets.model.utils.logUpdateResults
|
||||
import com.tangem.features.markets.ui.entity.MarketsListItemUM
|
||||
import com.tangem.features.markets.ui.entity.MarketsListUM.TrendInterval
|
||||
import com.tangem.features.markets.ui.entity.SortByTypeUM
|
||||
import com.tangem.pagination.*
|
||||
import com.tangem.features.markets.tokenlist.impl.model.converters.MarketsTokenItemConverter
|
||||
import com.tangem.features.markets.tokenlist.impl.model.utils.logAction
|
||||
import com.tangem.features.markets.tokenlist.impl.model.utils.logStatus
|
||||
import com.tangem.features.markets.tokenlist.impl.model.utils.logUpdateResults
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListItemUM
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListUM.TrendInterval
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.state.SortByTypeUM
|
||||
import com.tangem.pagination.Batch
|
||||
import com.tangem.pagination.BatchAction
|
||||
import com.tangem.pagination.BatchFetchResult
|
||||
import com.tangem.pagination.PaginationStatus
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
import com.tangem.utils.coroutines.saveIn
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
private const val LOG_EVENTS = true
|
||||
|
||||
|
|
@ -33,6 +37,7 @@ internal class MarketsListBatchFlowManager(
|
|||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) {
|
||||
private val actionsFlow = MutableSharedFlow<BatchAction<Int, TokenMarketListConfig, TokenMarketUpdateRequest>>()
|
||||
private val updateStateJob = JobHolder()
|
||||
|
||||
private val batchFlow = getMarketsTokenListFlowUseCase(
|
||||
batchingContext = TokenListBatchingContext(
|
||||
|
|
@ -42,6 +47,9 @@ internal class MarketsListBatchFlowManager(
|
|||
batchFlowType = batchFlowType,
|
||||
)
|
||||
|
||||
private val resultBatches = MutableStateFlow(ResultBatches())
|
||||
private val uiBatches = resultBatches.map { it.uiBatches }
|
||||
|
||||
val uiItems: StateFlow<ImmutableList<MarketsListItemUM>>
|
||||
get() = uiBatches
|
||||
.map { batches ->
|
||||
|
|
@ -97,16 +105,20 @@ internal class MarketsListBatchFlowManager(
|
|||
initialValue = false,
|
||||
)
|
||||
|
||||
private val uiBatches = MutableStateFlow<List<Batch<Int, List<MarketsListItemUM>>>>(emptyList())
|
||||
|
||||
init {
|
||||
batchFlow.state
|
||||
.map { it.data }
|
||||
.distinctUntilChanged { a, b ->
|
||||
a.size == b.size && a.map { it.data }.flatten() == b.map { it.data }.flatten()
|
||||
a.size == b.size &&
|
||||
a.map { it.key } == b.map { it.key } &&
|
||||
a.map { it.data }.flatten() == b.map { it.data }.flatten()
|
||||
}
|
||||
.onEachWithPrevious { prev, list ->
|
||||
updateState(prev, list)
|
||||
.onEach {
|
||||
coroutineScope {
|
||||
launch {
|
||||
updateState(it)
|
||||
}.saveIn(updateStateJob)
|
||||
}
|
||||
}
|
||||
.flowOn(dispatchers.default)
|
||||
.launchIn(modelScope)
|
||||
|
|
@ -127,58 +139,75 @@ internal class MarketsListBatchFlowManager(
|
|||
}
|
||||
}
|
||||
|
||||
private fun updateState(
|
||||
previousList: List<Batch<Int, List<TokenMarket>>>?,
|
||||
list: List<Batch<Int, List<TokenMarket>>>,
|
||||
forceUpdate: Boolean = false,
|
||||
) = uiBatches.update { items ->
|
||||
val converter = MarketsTokenItemConverter(currentTrendInterval(), appCurrency = currentAppCurrency())
|
||||
private suspend fun updateState(newList: List<Batch<Int, List<TokenMarket>>>, forceUpdate: Boolean = false) =
|
||||
withContext(dispatchers.default) {
|
||||
resultBatches.update { resultBatches ->
|
||||
val items = resultBatches.uiBatches
|
||||
val previousList = resultBatches.processedItems
|
||||
|
||||
if (previousList == null || list.size < previousList.size || forceUpdate) {
|
||||
list.map {
|
||||
Batch(
|
||||
key = it.key,
|
||||
data = converter.convertList(it.data),
|
||||
val converter = MarketsTokenItemConverter(currentTrendInterval(), appCurrency = currentAppCurrency())
|
||||
|
||||
if (newList.isEmpty()) {
|
||||
return@update ResultBatches(processedItems = emptyList())
|
||||
}
|
||||
|
||||
val isInitialLoading =
|
||||
forceUpdate || previousList.isNullOrEmpty() || newList.first().key != previousList.first().key
|
||||
|
||||
val outItems = if (isInitialLoading) {
|
||||
newList.map {
|
||||
Batch(
|
||||
key = it.key,
|
||||
data = converter.convertList(it.data),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
previousList!!
|
||||
if (previousList.size != newList.size) {
|
||||
val keysToAdd = newList.map { it.key }.subtract(previousList.map { it.key }.toSet())
|
||||
val newBatches = newList.filter { keysToAdd.contains(it.key) }
|
||||
|
||||
items + newBatches.map {
|
||||
Batch(
|
||||
key = it.key,
|
||||
data = converter.convertList(it.data),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
items.mapIndexed { batchIndex, batch ->
|
||||
val prevBatch = previousList[batchIndex]
|
||||
val newBatch = newList[batchIndex]
|
||||
if (previousList == newBatch) return@mapIndexed batch
|
||||
|
||||
Batch(
|
||||
key = batch.key,
|
||||
data = batch.data.mapIndexed { index, marketsListItemUM ->
|
||||
val prevItem = prevBatch.data[index]
|
||||
val newItem = newBatch.data[index]
|
||||
|
||||
converter.update(
|
||||
prevItem,
|
||||
marketsListItemUM,
|
||||
newItem,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
currentCoroutineContext().ensureActive()
|
||||
|
||||
ResultBatches(
|
||||
uiBatches = outItems,
|
||||
processedItems = newList,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (previousList.size != list.size) {
|
||||
val keysToAdd = list.map { it.key }.subtract(previousList.map { it.key }.toSet())
|
||||
val newBatches = list.filter { keysToAdd.contains(it.key) }
|
||||
|
||||
items + newBatches.map {
|
||||
Batch(
|
||||
key = it.key,
|
||||
data = converter.convertList(it.data),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
items.mapIndexed { batchIndex, batch ->
|
||||
val prevBatch = previousList[batchIndex]
|
||||
val newBatch = list[batchIndex]
|
||||
if (previousList == newBatch) return@mapIndexed batch
|
||||
|
||||
Batch(
|
||||
key = batch.key,
|
||||
data = batch.data.mapIndexed { index, marketsListItemUM ->
|
||||
val prevItem = prevBatch.data[index]
|
||||
val newItem = newBatch.data[index]
|
||||
|
||||
converter.update(
|
||||
prevItem,
|
||||
marketsListItemUM,
|
||||
newItem,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun reload(searchText: String? = null) {
|
||||
modelScope.launch {
|
||||
uiBatches.value = emptyList()
|
||||
resultBatches.value = ResultBatches()
|
||||
actionsFlow.emit(
|
||||
BatchAction.Reload(
|
||||
requestParams = TokenMarketListConfig(
|
||||
|
|
@ -188,7 +217,6 @@ internal class MarketsListBatchFlowManager(
|
|||
} else {
|
||||
searchText ?: currentSearchText()
|
||||
},
|
||||
showUnder100kMarketCapTokens = false, // TODO
|
||||
priceChangeInterval = currentTrendInterval().toBatchRequestInterval(),
|
||||
order = currentSortByType().toRequestOrder(),
|
||||
),
|
||||
|
|
@ -206,12 +234,14 @@ internal class MarketsListBatchFlowManager(
|
|||
fun updateUIWithSameState() {
|
||||
modelScope.launch(dispatchers.default) {
|
||||
val current = batchFlow.state.value.data
|
||||
updateState(current, current, forceUpdate = true)
|
||||
}
|
||||
updateState(current, forceUpdate = true)
|
||||
}.saveIn(updateStateJob)
|
||||
}
|
||||
|
||||
fun loadCharts(batchKeys: Set<Int>, interval: TrendInterval) {
|
||||
modelScope.launch(dispatchers.default) {
|
||||
if (batchKeys.isEmpty()) return
|
||||
|
||||
modelScope.launch {
|
||||
val currentData = batchFlow.state.value.data
|
||||
val alreadyLoadedChartsBatchKeys = currentData
|
||||
.filter {
|
||||
|
|
@ -233,7 +263,7 @@ internal class MarketsListBatchFlowManager(
|
|||
BatchAction.UpdateBatches(
|
||||
keys = batchesKeysToLoad,
|
||||
updateRequest = TokenMarketUpdateRequest.UpdateChart(
|
||||
interval = interval.toRequestInterval(),
|
||||
interval = interval.toBatchRequestInterval(),
|
||||
currency = currentAppCurrency().code,
|
||||
),
|
||||
async = true,
|
||||
|
|
@ -266,7 +296,7 @@ internal class MarketsListBatchFlowManager(
|
|||
}
|
||||
|
||||
fun clearStateAndStopAllActions() {
|
||||
uiBatches.value = emptyList()
|
||||
resultBatches.value = ResultBatches()
|
||||
modelScope.launch {
|
||||
actionsFlow.emit(BatchAction.Reset)
|
||||
}
|
||||
|
|
@ -281,6 +311,10 @@ internal class MarketsListBatchFlowManager(
|
|||
.toSet()
|
||||
}
|
||||
|
||||
fun getTokenById(id: String): TokenMarket? {
|
||||
return batchFlow.state.value.data.map { it.data }.flatten().find { it.id == id }
|
||||
}
|
||||
|
||||
private fun SortByTypeUM.toRequestOrder(): TokenMarketListConfig.Order {
|
||||
return when (this) {
|
||||
SortByTypeUM.Rating -> TokenMarketListConfig.Order.ByRating
|
||||
|
|
@ -299,20 +333,8 @@ internal class MarketsListBatchFlowManager(
|
|||
}
|
||||
}
|
||||
|
||||
private fun TrendInterval.toRequestInterval(): PriceChangeInterval {
|
||||
return when (this) {
|
||||
TrendInterval.H24 -> PriceChangeInterval.H24
|
||||
TrendInterval.D7 -> PriceChangeInterval.WEEK
|
||||
TrendInterval.M1 -> PriceChangeInterval.MONTH
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T> Flow<T>.onEachWithPrevious(operation: suspend (prev: T?, value: T) -> Unit): Flow<T> = flow {
|
||||
var prev: T? = null
|
||||
collect { value ->
|
||||
operation(prev, value)
|
||||
prev = value
|
||||
emit(value)
|
||||
}
|
||||
}
|
||||
private data class ResultBatches(
|
||||
val uiBatches: List<Batch<Int, List<MarketsListItemUM>>> = emptyList(),
|
||||
val processedItems: List<Batch<Int, List<TokenMarket>>>? = null,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.features.markets.model.statemanager
|
||||
package com.tangem.features.markets.tokenlist.impl.model.statemanager
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
|
|
@ -7,12 +7,14 @@ import com.tangem.core.ui.event.consumedEvent
|
|||
import com.tangem.core.ui.event.triggeredEvent
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.features.markets.impl.R
|
||||
import com.tangem.features.markets.ui.entity.SortByBottomSheetContentUM
|
||||
import com.tangem.features.markets.ui.entity.ListUM
|
||||
import com.tangem.features.markets.ui.entity.MarketsListItemUM
|
||||
import com.tangem.features.markets.ui.entity.MarketsListUM
|
||||
import com.tangem.features.markets.ui.entity.SortByTypeUM
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.state.SortByBottomSheetContentUM
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.state.ListUM
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListItemUM
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListUM
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.state.SortByTypeUM
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
@Stable
|
||||
|
|
@ -20,6 +22,7 @@ internal class MarketsListUMStateManager(
|
|||
private val onLoadMoreUiItems: () -> Unit,
|
||||
private val visibleItemsChanged: (itemsKeys: List<String>) -> Unit,
|
||||
private val onRetryButtonClicked: () -> Unit,
|
||||
private val onTokenClick: (MarketsListItemUM) -> Unit,
|
||||
) {
|
||||
|
||||
private var sortByBottomSheetIsShown
|
||||
|
|
@ -98,21 +101,74 @@ internal class MarketsListUMStateManager(
|
|||
it.copy(list = ListUM.Loading)
|
||||
}
|
||||
else -> {
|
||||
it.copy(
|
||||
list = ListUM.Content(
|
||||
items = uiItems,
|
||||
loadMore = onLoadMoreUiItems,
|
||||
visibleIdsChanged = visibleItemsChanged,
|
||||
showUnder100kTokens = true,
|
||||
onShowTokensUnder100kClicked = { },
|
||||
triggerScrollReset = consumedEvent(),
|
||||
),
|
||||
)
|
||||
it.updateItems(newItems = uiItems)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun MarketsListUM.updateItems(newItems: ImmutableList<MarketsListItemUM>): MarketsListUM {
|
||||
val currentState = this
|
||||
val isNextPageInSearch = isInSearchState && (this.list as? ListUM.Content)?.showUnder100kTokens == true
|
||||
var searchUiItemsCached: ImmutableList<MarketsListItemUM> = persistentListOf()
|
||||
|
||||
val items = when {
|
||||
isInSearchState && isNextPageInSearch.not() -> {
|
||||
searchUiItemsCached = newItems
|
||||
val filtered = newItems.filter { item -> item.isUnder100kMarketCap.not() }.toImmutableList()
|
||||
|
||||
if (filtered.size == newItems.size) {
|
||||
return currentState.copy(list = generalContentState(newItems))
|
||||
} else {
|
||||
filtered
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
searchUiItemsCached = persistentListOf()
|
||||
newItems
|
||||
}
|
||||
}
|
||||
|
||||
return currentState.copy(
|
||||
list = ListUM.Content(
|
||||
items = items,
|
||||
loadMore = onLoadMoreUiItems,
|
||||
visibleIdsChanged = visibleItemsChanged,
|
||||
showUnder100kTokens = isInSearchState.not() || isNextPageInSearch,
|
||||
onShowTokensUnder100kClicked = {
|
||||
if (searchUiItemsCached.isNotEmpty()) {
|
||||
state.update { s ->
|
||||
if (s.list is ListUM.Content) {
|
||||
s.copy(
|
||||
list = s.list.copy(
|
||||
items = searchUiItemsCached,
|
||||
showUnder100kTokens = true,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
s
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
triggerScrollReset = consumedEvent(),
|
||||
onItemClick = onTokenClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun generalContentState(newItems: ImmutableList<MarketsListItemUM>): ListUM.Content {
|
||||
return ListUM.Content(
|
||||
items = newItems,
|
||||
loadMore = onLoadMoreUiItems,
|
||||
visibleIdsChanged = visibleItemsChanged,
|
||||
showUnder100kTokens = true,
|
||||
onShowTokensUnder100kClicked = {},
|
||||
triggerScrollReset = consumedEvent(),
|
||||
onItemClick = onTokenClick,
|
||||
)
|
||||
}
|
||||
|
||||
private fun state(): MarketsListUM = MarketsListUM(
|
||||
list = ListUM.Loading,
|
||||
searchBar = SearchBarUM(
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.features.markets.model.utils
|
||||
package com.tangem.features.markets.tokenlist.impl.model.utils
|
||||
|
||||
import com.tangem.domain.markets.TokenMarket
|
||||
import com.tangem.domain.markets.TokenMarketListConfig
|
||||
|
|
@ -1,16 +1,15 @@
|
|||
package com.tangem.features.markets.ui
|
||||
package com.tangem.features.markets.tokenlist.impl.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
|
|
@ -32,17 +31,18 @@ import com.tangem.core.ui.components.keyboardAsState
|
|||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.res.LocalMainBottomSheetColor
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.markets.component.BottomSheetState
|
||||
import com.tangem.features.markets.impl.R
|
||||
import com.tangem.features.markets.ui.components.MarketsListLazyColumn
|
||||
import com.tangem.features.markets.ui.components.MarketsListSortByBottomSheet
|
||||
import com.tangem.features.markets.ui.entity.ListUM
|
||||
import com.tangem.features.markets.ui.entity.MarketsListUM
|
||||
import com.tangem.features.markets.ui.entity.SortByBottomSheetContentUM
|
||||
import com.tangem.features.markets.ui.entity.SortByTypeUM
|
||||
import com.tangem.features.markets.ui.preview.MarketChartListItemPreviewDataProvider
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.components.MarketsListLazyColumn
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.components.MarketsListSortByBottomSheet
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.state.ListUM
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListUM
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.state.SortByBottomSheetContentUM
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.state.SortByTypeUM
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.preview.MarketChartListItemPreviewDataProvider
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
|
|
@ -68,23 +68,28 @@ internal fun MarketsList(
|
|||
@Composable
|
||||
private fun Content(state: MarketsListUM, onHeaderSizeChange: (Dp) -> Unit, modifier: Modifier = Modifier) {
|
||||
val density = LocalDensity.current
|
||||
val background = LocalMainBottomSheetColor.current.value
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.imePadding()
|
||||
.background(color = TangemTheme.colors.background.primary),
|
||||
.drawBehind { drawRect(background) },
|
||||
) {
|
||||
SearchBar(
|
||||
modifier = Modifier
|
||||
.background(color = TangemTheme.colors.background.primary)
|
||||
.drawBehind { drawRect(background) }
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing16,
|
||||
end = TangemTheme.dimens.spacing16,
|
||||
bottom = TangemTheme.dimens.spacing4,
|
||||
)
|
||||
.onGloballyPositioned {
|
||||
with(density) { onHeaderSizeChange(it.size.height.toDp()) }
|
||||
if (it.size.height > 0) {
|
||||
with(density) {
|
||||
onHeaderSizeChange(it.size.height.toDp())
|
||||
}
|
||||
}
|
||||
},
|
||||
state = state.searchBar,
|
||||
)
|
||||
|
|
@ -225,44 +230,52 @@ private fun KeyboardEvents(isSortByBottomSheetShown: Boolean, bottomSheetState:
|
|||
//region: Preview
|
||||
|
||||
@Preview
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview() {
|
||||
TangemThemePreview {
|
||||
MarketsList(
|
||||
state = MarketsListUM(
|
||||
list = ListUM.Content(
|
||||
items = MarketChartListItemPreviewDataProvider().values
|
||||
.flatMap { item -> List(size = 10) { item } }
|
||||
.mapIndexed { index, item ->
|
||||
item.copy(id = index.toString())
|
||||
}
|
||||
.toImmutableList(),
|
||||
showUnder100kTokens = false,
|
||||
loadMore = {},
|
||||
visibleIdsChanged = {},
|
||||
onShowTokensUnder100kClicked = {},
|
||||
triggerScrollReset = consumedEvent(),
|
||||
TangemThemePreview(alwaysShowBottomSheets = false) {
|
||||
val primaryBackground = TangemTheme.colors.background.primary
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalMainBottomSheetColor provides remember { mutableStateOf(primaryBackground) },
|
||||
) {
|
||||
MarketsList(
|
||||
state = MarketsListUM(
|
||||
list = ListUM.Content(
|
||||
items = MarketChartListItemPreviewDataProvider().values
|
||||
.flatMap { item -> List(size = 10) { item } }
|
||||
.mapIndexed { index, item ->
|
||||
item.copy(id = index.toString())
|
||||
}
|
||||
.toImmutableList(),
|
||||
showUnder100kTokens = false,
|
||||
loadMore = {},
|
||||
visibleIdsChanged = {},
|
||||
onShowTokensUnder100kClicked = {},
|
||||
triggerScrollReset = consumedEvent(),
|
||||
onItemClick = {},
|
||||
),
|
||||
searchBar = SearchBarUM(
|
||||
placeholderText = resourceReference(R.string.manage_tokens_search_placeholder),
|
||||
query = "",
|
||||
onQueryChange = {},
|
||||
isActive = false,
|
||||
onActiveChange = { },
|
||||
),
|
||||
selectedSortBy = SortByTypeUM.Rating,
|
||||
selectedInterval = MarketsListUM.TrendInterval.H24,
|
||||
onIntervalClick = {},
|
||||
onSortByButtonClick = {},
|
||||
sortByBottomSheet = TangemBottomSheetConfig(
|
||||
isShow = false,
|
||||
onDismissRequest = {},
|
||||
content = SortByBottomSheetContentUM(selectedOption = SortByTypeUM.Rating) {},
|
||||
),
|
||||
),
|
||||
searchBar = SearchBarUM(
|
||||
placeholderText = resourceReference(R.string.manage_tokens_search_placeholder),
|
||||
query = "",
|
||||
onQueryChange = {},
|
||||
isActive = false,
|
||||
onActiveChange = { },
|
||||
),
|
||||
selectedSortBy = SortByTypeUM.Rating,
|
||||
selectedInterval = MarketsListUM.TrendInterval.H24,
|
||||
onIntervalClick = {},
|
||||
onSortByButtonClick = {},
|
||||
sortByBottomSheet = TangemBottomSheetConfig(
|
||||
isShow = false,
|
||||
onDismissRequest = {},
|
||||
content = SortByBottomSheetContentUM(selectedOption = SortByTypeUM.Rating) {},
|
||||
),
|
||||
),
|
||||
onHeaderSizeChange = {},
|
||||
bottomSheetState = BottomSheetState.EXPANDED,
|
||||
)
|
||||
onHeaderSizeChange = {},
|
||||
bottomSheetState = BottomSheetState.EXPANDED,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.features.markets.ui.components
|
||||
package com.tangem.features.markets.tokenlist.impl.ui.components
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.Animatable
|
||||
|
|
@ -39,14 +39,15 @@ import com.tangem.core.ui.components.*
|
|||
import com.tangem.core.ui.components.currency.icon.CoinIcon
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeInPercent
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.haptic.TangemHapticEffect
|
||||
import com.tangem.core.ui.res.LocalHapticManager
|
||||
import com.tangem.core.ui.res.LocalWindowSize
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.windowsize.WindowSizeType
|
||||
import com.tangem.features.markets.impl.R
|
||||
import com.tangem.features.markets.ui.entity.MarketsListItemUM
|
||||
import com.tangem.features.markets.ui.preview.MarketChartListItemPreviewDataProvider
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListItemUM
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.preview.MarketChartListItemPreviewDataProvider
|
||||
import com.tangem.utils.StringsSigns.MINUS
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.roundToInt
|
||||
|
|
@ -97,14 +98,14 @@ fun MarketsListItem(
|
|||
.collect {
|
||||
val border = -actionWidthPx * SWIPE_THRESHOLD_PERCENT
|
||||
if (it < border && actionPerformed.not()) {
|
||||
hapticManager.vibrateLong()
|
||||
hapticManager.perform(TangemHapticEffect.View.GestureThresholdActivate)
|
||||
actionPerformed = true
|
||||
releasePerformed = false
|
||||
}
|
||||
|
||||
if (it > border) {
|
||||
if (releasePerformed.not()) {
|
||||
hapticManager.vibrateShort()
|
||||
hapticManager.perform(TangemHapticEffect.View.GestureThresholdDeactivate)
|
||||
releasePerformed = true
|
||||
}
|
||||
actionPerformed = false
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.features.markets.ui.components
|
||||
package com.tangem.features.markets.tokenlist.impl.ui.components
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.features.markets.ui.components
|
||||
package com.tangem.features.markets.tokenlist.impl.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
|
|
@ -9,10 +9,6 @@ import androidx.compose.material3.Text
|
|||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.tangem.core.ui.components.buttons.SecondarySmallButton
|
||||
|
|
@ -20,11 +16,13 @@ import com.tangem.core.ui.components.buttons.SmallButtonConfig
|
|||
import com.tangem.core.ui.event.EventEffect
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.utils.disableNestedScroll
|
||||
import com.tangem.features.markets.impl.R
|
||||
import com.tangem.features.markets.ui.entity.ListUM
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.state.ListUM
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
private const val LOAD_NEXT_PAGE_ON_END_INDEX = 50
|
||||
private const val TOKEN_LAZY_LIST_ID_SEPARATOR = "***"
|
||||
|
||||
@Composable
|
||||
@Suppress("LongMethod")
|
||||
|
|
@ -53,7 +51,7 @@ internal fun MarketsListLazyColumn(
|
|||
|
||||
if (state is ListUM.Loading) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.nestedScroll(DisableParentConnection),
|
||||
modifier = Modifier.disableNestedScroll(),
|
||||
state = rememberLazyListState(),
|
||||
contentPadding = PaddingValues(bottom = bottomBarHeight),
|
||||
userScrollEnabled = false,
|
||||
|
|
@ -64,7 +62,7 @@ internal fun MarketsListLazyColumn(
|
|||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = modifier.nestedScroll(DisableParentConnection),
|
||||
modifier = modifier.disableNestedScroll(),
|
||||
state = lazyListState,
|
||||
contentPadding = PaddingValues(bottom = bottomBarHeight),
|
||||
userScrollEnabled = true,
|
||||
|
|
@ -89,9 +87,12 @@ internal fun MarketsListLazyColumn(
|
|||
is ListUM.Content -> {
|
||||
items(
|
||||
items = state.items,
|
||||
key = { it.id },
|
||||
key = { it.id + TOKEN_LAZY_LIST_ID_SEPARATOR + it.marketCap.toString() },
|
||||
) { item ->
|
||||
MarketsListItem(model = item)
|
||||
MarketsListItem(
|
||||
model = item,
|
||||
onClick = { state.onItemClick(item) },
|
||||
)
|
||||
}
|
||||
|
||||
if (isInSearchMode && state.showUnder100kTokens.not()) {
|
||||
|
|
@ -114,8 +115,11 @@ internal fun MarketsListLazyColumn(
|
|||
buffer = LOAD_NEXT_PAGE_ON_END_INDEX,
|
||||
onLoadMore = remember(state) {
|
||||
{
|
||||
if (state is ListUM.Content) {
|
||||
if (state is ListUM.Content && state.showUnder100kTokens) {
|
||||
state.loadMore()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -181,7 +185,9 @@ private fun SearchNothingFoundText(modifier: Modifier = Modifier) {
|
|||
private fun VisibleItemsTracker(listState: LazyListState, state: ListUM) {
|
||||
val visibleItems by remember {
|
||||
derivedStateOf {
|
||||
listState.layoutInfo.visibleItemsInfo.mapNotNull { it.key as? String }
|
||||
listState.layoutInfo.visibleItemsInfo.mapNotNull {
|
||||
(it.key as? String)?.split(TOKEN_LAZY_LIST_ID_SEPARATOR)?.first()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -193,7 +199,7 @@ private fun VisibleItemsTracker(listState: LazyListState, state: ListUM) {
|
|||
}
|
||||
|
||||
@Composable
|
||||
fun InfiniteListHandler(listState: LazyListState, onLoadMore: () -> Unit, buffer: Int = 2) {
|
||||
fun InfiniteListHandler(listState: LazyListState, onLoadMore: () -> Boolean, buffer: Int = 2) {
|
||||
val loadMore by remember {
|
||||
derivedStateOf {
|
||||
val layoutInfo = listState.layoutInfo
|
||||
|
|
@ -209,14 +215,7 @@ fun InfiniteListHandler(listState: LazyListState, onLoadMore: () -> Unit, buffer
|
|||
|
||||
LaunchedEffect(loadMore) {
|
||||
if (loadMore && !emitted) {
|
||||
emitted = true
|
||||
onLoadMore()
|
||||
emitted = onLoadMore()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private object DisableParentConnection : NestedScrollConnection {
|
||||
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
|
||||
return available.copy(x = 0f)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.features.markets.ui.components
|
||||
package com.tangem.features.markets.tokenlist.impl.ui.components
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
|
|
@ -19,8 +19,8 @@ import com.tangem.core.ui.extensions.resourceReference
|
|||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.markets.impl.R
|
||||
import com.tangem.features.markets.ui.entity.SortByBottomSheetContentUM
|
||||
import com.tangem.features.markets.ui.entity.SortByTypeUM
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.state.SortByBottomSheetContentUM
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.state.SortByTypeUM
|
||||
|
||||
@Composable
|
||||
fun MarketsListSortByBottomSheet(config: TangemBottomSheetConfig) {
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.features.markets.ui.components
|
||||
package com.tangem.features.markets.tokenlist.impl.ui.components
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
|
|
@ -1,10 +1,11 @@
|
|||
@file:Suppress("MagicNumber")
|
||||
package com.tangem.features.markets.ui.preview
|
||||
package com.tangem.features.markets.tokenlist.impl.ui.preview
|
||||
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import com.tangem.common.ui.charts.state.MarketChartRawData
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.features.markets.ui.entity.MarketsListItemUM
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListItemUM
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParameterProvider<MarketsListItemUM>(
|
||||
collection = listOf(
|
||||
|
|
@ -19,7 +20,7 @@ internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParamet
|
|||
trendPercentText = "12.43%",
|
||||
trendType = PriceChangeType.UP,
|
||||
chardData = MarketChartRawData(
|
||||
y = listOf(0.4f, 0.2f, 0.4f, 0.1f, 0.4f, 2f, 5f, 0.1f, 2f, 2f, 3f),
|
||||
y = persistentListOf(0.4, 0.2, 0.4, 0.1, 0.4, 2.0, 5.0, 0.1, 2.0, 2.0, 3.0),
|
||||
),
|
||||
),
|
||||
MarketsListItemUM(
|
||||
|
|
@ -45,7 +46,7 @@ internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParamet
|
|||
trendPercentText = "12.43%",
|
||||
trendType = PriceChangeType.DOWN,
|
||||
chardData = MarketChartRawData(
|
||||
y = listOf(0.4f, 0.2f, 0.4f, 0.1f, 0.4f, 2f, 5f, 0.1f, 2f, 2f, 3f),
|
||||
y = persistentListOf(0.4, 0.2, 0.4, 0.1, 0.4, 2.0, 5.0, 0.1, 2.0, 2.0, 3.0),
|
||||
),
|
||||
),
|
||||
MarketsListItemUM(
|
||||
|
|
@ -59,7 +60,7 @@ internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParamet
|
|||
trendPercentText = "12.43%",
|
||||
trendType = PriceChangeType.UP,
|
||||
chardData = MarketChartRawData(
|
||||
y = listOf(0.4f, 0.2f, 0.4f, 0.1f, 0.4f, 2f, 5f, 0.1f, 2f, 2f, 3f),
|
||||
y = persistentListOf(0.4, 0.2, 0.4, 0.1, 0.4, 2.0, 5.0, 0.1, 2.0, 2.0, 3.0),
|
||||
),
|
||||
),
|
||||
MarketsListItemUM(
|
||||
|
|
@ -73,7 +74,7 @@ internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParamet
|
|||
trendPercentText = "12.43%",
|
||||
trendType = PriceChangeType.UP,
|
||||
chardData = MarketChartRawData(
|
||||
y = listOf(0.4f, 0.2f, 0.4f, 0.1f, 0.4f, 2f, 5f, 0.1f, 2f, 2f, 3f),
|
||||
y = persistentListOf(0.4, 0.2, 0.4, 0.1, 0.4, 2.0, 5.0, 0.1, 2.0, 2.0, 3.0),
|
||||
),
|
||||
),
|
||||
MarketsListItemUM(
|
||||
|
|
@ -87,7 +88,7 @@ internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParamet
|
|||
trendPercentText = "12.43%",
|
||||
trendType = PriceChangeType.UP,
|
||||
chardData = MarketChartRawData(
|
||||
y = listOf(0.4f, 0.2f, 0.4f, 0.1f, 0.4f, 2f, 5f, 0.1f, 2f, 2f, 3f),
|
||||
y = persistentListOf(0.4, 0.2, 0.4, 0.1, 0.4, 2.0, 5.0, 0.1, 2.0, 2.0, 3.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.features.markets.ui.entity
|
||||
package com.tangem.features.markets.tokenlist.impl.ui.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.common.ui.charts.state.MarketChartLook
|
||||
|
|
@ -17,7 +17,7 @@ data class MarketsListItemUM(
|
|||
val trendPercentText: String,
|
||||
val trendType: PriceChangeType,
|
||||
val chardData: MarketChartRawData?,
|
||||
val showUnder100kMarketCap: Boolean = false,
|
||||
val isUnder100kMarketCap: Boolean = false,
|
||||
) {
|
||||
val chartType: MarketChartLook.Type = when (trendType) {
|
||||
PriceChangeType.UP,
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.features.markets.ui.entity
|
||||
package com.tangem.features.markets.tokenlist.impl.ui.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
|
|
@ -46,6 +46,7 @@ sealed class ListUM {
|
|||
val visibleIdsChanged: (List<String>) -> Unit,
|
||||
val onShowTokensUnder100kClicked: () -> Unit,
|
||||
val triggerScrollReset: StateEvent<Unit>,
|
||||
val onItemClick: (MarketsListItemUM) -> Unit,
|
||||
) : ListUM()
|
||||
|
||||
data object Loading : ListUM()
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.features.markets.ui.entity
|
||||
package com.tangem.features.markets.tokenlist.impl.ui.state
|
||||
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
package com.tangem.features.send.api.featuretoggles
|
||||
|
||||
/**
|
||||
* Send feature toggles
|
||||
*/
|
||||
interface SendFeatureToggles {
|
||||
|
||||
/** Availability of redesigned send screen */
|
||||
val isRedesignedSendEnabled: Boolean
|
||||
|
||||
/** Updates remote toggle */
|
||||
suspend fun fetchNewSendEnabled()
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
package com.tangem.features.send.impl.di
|
||||
|
||||
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.features.send.api.featuretoggles.SendFeatureToggles
|
||||
import com.tangem.features.send.impl.featuretoggles.DefaultSendFeatureToggles
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* DI module provides implementation of [SendFeatureToggles]
|
||||
*/
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object SendFeatureTogglesModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSendFeatureToggles(
|
||||
featureTogglesManager: FeatureTogglesManager,
|
||||
tangemTechApi: TangemTechApi,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): SendFeatureToggles {
|
||||
return DefaultSendFeatureToggles(
|
||||
featureTogglesManager = featureTogglesManager,
|
||||
tangemTechApi = tangemTechApi,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
package com.tangem.features.send.impl.featuretoggles
|
||||
|
||||
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.features.send.api.featuretoggles.SendFeatureToggles
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.runCatching
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Default implementation of Send feature toggles
|
||||
*
|
||||
* @property featureTogglesManager manager for getting information about the availability of feature toggles
|
||||
* @property tangemTechApi api to get remote feature toggle for send
|
||||
* @property dispatchers coroutine dispatchers
|
||||
*/
|
||||
internal class DefaultSendFeatureToggles(
|
||||
private val featureTogglesManager: FeatureTogglesManager,
|
||||
private val tangemTechApi: TangemTechApi,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : SendFeatureToggles {
|
||||
|
||||
private val remoteSendEnabled: MutableStateFlow<Boolean> = MutableStateFlow(true)
|
||||
|
||||
override val isRedesignedSendEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled(name = "REDESIGNED_SEND_SCREEN_ENABLED") &&
|
||||
remoteSendEnabled.value
|
||||
|
||||
override suspend fun fetchNewSendEnabled() {
|
||||
runCatching(dispatchers.io) {
|
||||
tangemTechApi.getFeatures().getOrThrow()
|
||||
}.onSuccess { response ->
|
||||
remoteSendEnabled.update { response.isNewSendEnabled }
|
||||
}.onFailure {
|
||||
Timber.e(it.localizedMessage, "Unable to fetch new send toggle")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -141,7 +141,7 @@ private fun LazyListScope.memoField(memoField: SendTextField.RecipientMemo?, onM
|
|||
onValueChange = memoField.onValueChange,
|
||||
onPasteClick = onMemoChange,
|
||||
modifier = Modifier.padding(top = TangemTheme.dimens.spacing20),
|
||||
labelStyle = TangemTheme.typography.caption2,
|
||||
labelStyle = TangemTheme.typography.subtitle2,
|
||||
isError = memoField.isError,
|
||||
error = memoField.error,
|
||||
isReadOnly = !memoField.isEnabled,
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ internal fun RecipientBlock(
|
|||
private fun AddressBlock(address: SendTextField.RecipientAddress) {
|
||||
Text(
|
||||
text = address.label.resolveReference(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
)
|
||||
Row(
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
package com.tangem.features.staking.impl.presentation.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Immutable
|
||||
sealed class FeeState {
|
||||
|
||||
data class Content(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package com.tangem.features.staking.impl.presentation.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.staking.model.stakekit.BalanceType
|
||||
import com.tangem.domain.staking.model.stakekit.PendingAction
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
|
@ -11,19 +13,23 @@ sealed class InnerYieldBalanceState {
|
|||
val rewardsCrypto: String,
|
||||
val rewardsFiat: String,
|
||||
val isRewardsToClaim: Boolean,
|
||||
val balance: List<BalanceGroupedState>,
|
||||
val balance: ImmutableList<BalanceGroupedState>,
|
||||
) : InnerYieldBalanceState()
|
||||
|
||||
data object Empty : InnerYieldBalanceState()
|
||||
}
|
||||
|
||||
// TODO staking get rid of unstable types
|
||||
@Immutable
|
||||
data class BalanceGroupedState(
|
||||
val items: ImmutableList<BalanceState>,
|
||||
val footer: TextReference?,
|
||||
val title: TextReference,
|
||||
val type: BalanceGroupType,
|
||||
val type: BalanceType,
|
||||
val isClickable: Boolean,
|
||||
)
|
||||
|
||||
@Immutable
|
||||
data class BalanceState(
|
||||
val validator: Yield.Validator,
|
||||
val cryptoValue: String,
|
||||
|
|
@ -33,10 +39,4 @@ data class BalanceState(
|
|||
val rawCurrencyId: String?,
|
||||
val unbondingPeriod: TextReference,
|
||||
val pendingActions: ImmutableList<PendingAction>,
|
||||
)
|
||||
|
||||
enum class BalanceGroupType {
|
||||
ACTIVE,
|
||||
UNSTAKED,
|
||||
UNKNOWN,
|
||||
}
|
||||
)
|
||||
|
|
@ -3,6 +3,7 @@ package com.tangem.features.staking.impl.presentation.state
|
|||
import com.tangem.core.ui.components.notifications.NotificationConfig
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.features.staking.impl.R
|
||||
|
||||
|
|
@ -23,7 +24,15 @@ internal sealed class StakingNotification(val config: NotificationConfig) {
|
|||
onCloseClick = onCloseClick,
|
||||
),
|
||||
) {
|
||||
// TODO staking
|
||||
data class StakedPositionNotFoundError(val message: String) : Error(
|
||||
title = stringReference(message),
|
||||
subtitle = stringReference(message),
|
||||
)
|
||||
|
||||
data class Common(val subtitle: TextReference) : Error(
|
||||
title = resourceReference(R.string.common_error),
|
||||
subtitle = subtitle,
|
||||
)
|
||||
}
|
||||
|
||||
sealed class Warning(
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
package com.tangem.features.staking.impl.presentation.state
|
||||
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.common.ui.navigationButtons.NavigationButtonsState
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
|
||||
import com.tangem.features.staking.impl.presentation.state.stub.StakingClickIntentsStub
|
||||
import com.tangem.features.staking.impl.presentation.state.transformers.SetButtonsStateTransformer
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
|
@ -20,20 +24,26 @@ internal class StakingStateController @Inject constructor() {
|
|||
|
||||
val uiState: StateFlow<StakingUiState> get() = mutableUiState.asStateFlow()
|
||||
|
||||
private val buttonsTransformer = SetButtonsStateTransformer()
|
||||
|
||||
fun update(function: (StakingUiState) -> StakingUiState) {
|
||||
mutableUiState.update(function = function)
|
||||
mutableUiState.update(function = buttonsTransformer::transform)
|
||||
}
|
||||
|
||||
fun update(transformer: Transformer<StakingUiState>) {
|
||||
mutableUiState.update(function = transformer::transform)
|
||||
mutableUiState.update(function = buttonsTransformer::transform)
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
mutableUiState.update { getInitialState() }
|
||||
mutableUiState.update(function = buttonsTransformer::transform)
|
||||
}
|
||||
|
||||
private fun getInitialState(): StakingUiState {
|
||||
return StakingUiState(
|
||||
title = TextReference.EMPTY,
|
||||
clickIntents = StakingClickIntentsStub,
|
||||
cryptoCurrencyName = "",
|
||||
currentStep = StakingStep.InitialInfo,
|
||||
|
|
@ -44,7 +54,8 @@ internal class StakingStateController @Inject constructor() {
|
|||
isBalanceHidden = false,
|
||||
event = consumedEvent(),
|
||||
bottomSheetConfig = null,
|
||||
routeType = RouteType.STAKE,
|
||||
actionType = StakingActionCommonType.ENTER,
|
||||
buttonsState = NavigationButtonsState.Empty,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.features.staking.impl.presentation.state
|
||||
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
|
||||
|
||||
internal class StakingStateRouter(
|
||||
private val appRouter: AppRouter,
|
||||
|
|
@ -14,12 +15,12 @@ internal class StakingStateRouter(
|
|||
|
||||
fun onNextClick() {
|
||||
when (stateController.value.currentStep) {
|
||||
StakingStep.InitialInfo -> when (stateController.value.routeType) {
|
||||
RouteType.STAKE -> showAmount()
|
||||
RouteType.OTHER,
|
||||
RouteType.UNSTAKE,
|
||||
StakingStep.InitialInfo -> when (stateController.value.actionType) {
|
||||
StakingActionCommonType.ENTER -> showAmount()
|
||||
StakingActionCommonType.PENDING_OTHER,
|
||||
StakingActionCommonType.EXIT,
|
||||
-> showConfirmation()
|
||||
RouteType.CLAIM -> showRewardsValidators()
|
||||
StakingActionCommonType.PENDING_REWARDS -> showRewardsValidators()
|
||||
}
|
||||
StakingStep.RewardsValidators,
|
||||
StakingStep.Validators,
|
||||
|
|
@ -32,10 +33,17 @@ internal class StakingStateRouter(
|
|||
}
|
||||
|
||||
fun onPrevClick() {
|
||||
when (stateController.uiState.value.currentStep) {
|
||||
val uiState = stateController.uiState.value
|
||||
when (uiState.currentStep) {
|
||||
StakingStep.InitialInfo -> onBackClick()
|
||||
StakingStep.Amount -> showInitial()
|
||||
StakingStep.Confirmation -> showAmount()
|
||||
StakingStep.Confirmation -> {
|
||||
if (uiState.actionType != StakingActionCommonType.ENTER) {
|
||||
showInitial()
|
||||
} else {
|
||||
showAmount()
|
||||
}
|
||||
}
|
||||
StakingStep.Validators -> showConfirmation()
|
||||
StakingStep.RewardsValidators -> showInitial()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,11 +2,13 @@ package com.tangem.features.staking.impl.presentation.state
|
|||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.common.ui.navigationButtons.NavigationButtonsState
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.list.RoundedListWithDividersItemData
|
||||
import com.tangem.core.ui.event.StateEvent
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.staking.model.stakekit.PendingAction
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
|
||||
import com.tangem.features.staking.impl.presentation.state.transformers.InfoType
|
||||
import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
|
@ -16,6 +18,7 @@ import kotlinx.collections.immutable.ImmutableList
|
|||
*/
|
||||
@Immutable
|
||||
internal data class StakingUiState(
|
||||
val title: TextReference,
|
||||
val clickIntents: StakingClickIntents,
|
||||
val cryptoCurrencyName: String,
|
||||
val currentStep: StakingStep,
|
||||
|
|
@ -25,7 +28,8 @@ internal data class StakingUiState(
|
|||
val confirmationState: StakingStates.ConfirmationState,
|
||||
val isBalanceHidden: Boolean,
|
||||
val bottomSheetConfig: TangemBottomSheetConfig?,
|
||||
val routeType: RouteType,
|
||||
val actionType: StakingActionCommonType,
|
||||
val buttonsState: NavigationButtonsState,
|
||||
val event: StateEvent<StakingEvent>,
|
||||
) {
|
||||
|
||||
|
|
@ -55,17 +59,6 @@ internal sealed class StakingStates {
|
|||
val isStakeMoreAvailable: Boolean,
|
||||
) : InitialInfoState()
|
||||
|
||||
data class InitialInfoItems(
|
||||
val available: String,
|
||||
val onStake: String,
|
||||
val aprRange: TextReference,
|
||||
val unbondingPeriod: String,
|
||||
val minimumRequirement: String,
|
||||
val rewardClaiming: String,
|
||||
val warmupPeriod: String,
|
||||
val rewardSchedule: String,
|
||||
)
|
||||
|
||||
data class Empty(
|
||||
override val isPrimaryButtonEnabled: Boolean = false,
|
||||
) : InitialInfoState()
|
||||
|
|
@ -94,6 +87,7 @@ internal sealed class StakingStates {
|
|||
val notifications: ImmutableList<StakingNotification>,
|
||||
val footerText: String,
|
||||
val transactionDoneState: TransactionDoneState,
|
||||
val pendingActionInProgress: PendingAction? = null,
|
||||
) : ConfirmationState()
|
||||
|
||||
data class Empty(
|
||||
|
|
@ -108,11 +102,4 @@ enum class StakingStep {
|
|||
Amount,
|
||||
Confirmation,
|
||||
Validators,
|
||||
}
|
||||
|
||||
enum class RouteType {
|
||||
STAKE,
|
||||
UNSTAKE,
|
||||
CLAIM,
|
||||
OTHER,
|
||||
}
|
||||
|
|
@ -10,7 +10,6 @@ import com.tangem.domain.appcurrency.model.AppCurrency
|
|||
import com.tangem.domain.staking.model.stakekit.*
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.staking.impl.R
|
||||
import com.tangem.features.staking.impl.presentation.state.BalanceGroupType
|
||||
import com.tangem.features.staking.impl.presentation.state.BalanceGroupedState
|
||||
import com.tangem.features.staking.impl.presentation.state.BalanceState
|
||||
import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState
|
||||
|
|
@ -59,15 +58,18 @@ internal class YieldBalancesConverter(
|
|||
.groupBy { it.type.toGroup() }
|
||||
.mapNotNull { item ->
|
||||
val (title, footer) = getGroupTitle(item.key)
|
||||
val isClickable = getClickableType(item.key)
|
||||
title?.let {
|
||||
BalanceGroupedState(
|
||||
items = item.value.mapBalances().toPersistentList(),
|
||||
footer = footer,
|
||||
title = it,
|
||||
type = item.key,
|
||||
isClickable = isClickable,
|
||||
)
|
||||
}
|
||||
}
|
||||
.toPersistentList()
|
||||
|
||||
private fun List<BalanceItem>.mapBalances(): List<BalanceState> {
|
||||
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
|
||||
|
|
@ -111,31 +113,37 @@ internal class YieldBalancesConverter(
|
|||
}
|
||||
|
||||
private fun BalanceType.toGroup() = when (this) {
|
||||
BalanceType.PREPARING,
|
||||
BalanceType.STAKED,
|
||||
BalanceType.REWARDS,
|
||||
BalanceType.AVAILABLE,
|
||||
BalanceType.LOCKED,
|
||||
-> BalanceGroupType.ACTIVE
|
||||
BalanceType.UNSTAKING,
|
||||
BalanceType.UNLOCKING,
|
||||
BalanceType.UNSTAKED,
|
||||
-> BalanceGroupType.UNSTAKED
|
||||
BalanceType.UNKNOWN,
|
||||
-> BalanceGroupType.UNKNOWN
|
||||
-> BalanceType.UNKNOWN
|
||||
else -> this
|
||||
}
|
||||
|
||||
private fun getGroupTitle(type: BalanceGroupType) = when (type) {
|
||||
BalanceGroupType.ACTIVE -> resourceReference(
|
||||
R.string.staking_active,
|
||||
) to resourceReference(
|
||||
R.string.staking_active_footer,
|
||||
)
|
||||
BalanceGroupType.UNSTAKED -> resourceReference(
|
||||
R.string.staking_unstaked,
|
||||
) to resourceReference(
|
||||
R.string.staking_unstaked_footer,
|
||||
)
|
||||
BalanceGroupType.UNKNOWN -> null to null
|
||||
private fun getGroupTitle(type: BalanceType) = when (type) {
|
||||
BalanceType.STAKED -> resourceReference(R.string.staking_active) to
|
||||
resourceReference(R.string.staking_active_footer)
|
||||
BalanceType.UNSTAKED -> resourceReference(R.string.staking_unstaked) to
|
||||
resourceReference(R.string.staking_unstaked_footer)
|
||||
BalanceType.UNSTAKING -> resourceReference(R.string.staking_unstaking) to null
|
||||
BalanceType.AVAILABLE -> null to null
|
||||
BalanceType.PREPARING -> null to null
|
||||
BalanceType.REWARDS -> null to null
|
||||
BalanceType.LOCKED -> null to null
|
||||
BalanceType.UNLOCKING -> null to null
|
||||
BalanceType.UNKNOWN -> null to null
|
||||
}
|
||||
|
||||
private fun getClickableType(type: BalanceType) = when (type) {
|
||||
BalanceType.STAKED,
|
||||
BalanceType.UNSTAKED,
|
||||
-> true
|
||||
BalanceType.AVAILABLE,
|
||||
BalanceType.UNSTAKING,
|
||||
BalanceType.PREPARING,
|
||||
BalanceType.REWARDS,
|
||||
BalanceType.LOCKED,
|
||||
BalanceType.UNLOCKING,
|
||||
BalanceType.UNKNOWN,
|
||||
-> false
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package com.tangem.features.staking.impl.presentation.state.previewdata
|
|||
import com.tangem.core.ui.components.list.RoundedListWithDividersItemData
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.staking.model.stakekit.BalanceType
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import com.tangem.features.staking.impl.R
|
||||
import com.tangem.features.staking.impl.presentation.state.*
|
||||
|
|
@ -59,11 +60,12 @@ internal object InitialStakingStatePreview {
|
|||
rewardsFiat = "100 $",
|
||||
rewardsCrypto = "100 SOL",
|
||||
isRewardsToClaim = false,
|
||||
balance = listOf(
|
||||
balance = persistentListOf(
|
||||
BalanceGroupedState(
|
||||
title = stringReference("Staked"),
|
||||
footer = null,
|
||||
type = BalanceGroupType.ACTIVE,
|
||||
type = BalanceType.STAKED,
|
||||
isClickable = true,
|
||||
items = persistentListOf(
|
||||
BalanceState(
|
||||
cryptoValue = "100",
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.tangem.features.staking.impl.presentation.state.stub
|
|||
|
||||
import com.tangem.domain.staking.model.stakekit.PendingAction
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
|
||||
import com.tangem.features.staking.impl.presentation.state.BalanceState
|
||||
import com.tangem.features.staking.impl.presentation.state.transformers.InfoType
|
||||
import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents
|
||||
|
|
@ -11,10 +12,14 @@ object StakingClickIntentsStub : StakingClickIntents {
|
|||
|
||||
override fun onBackClick() {}
|
||||
|
||||
override fun onNextClick(pendingActions: ImmutableList<PendingAction>) {}
|
||||
override fun onNextClick(actionType: StakingActionCommonType?, pendingActions: ImmutableList<PendingAction>) {}
|
||||
|
||||
override fun onActionClick(pendingAction: PendingAction?) {}
|
||||
|
||||
override fun onPrevClick() {}
|
||||
|
||||
override fun onInitialInfoBannerClick() {}
|
||||
|
||||
override fun onInfoClick(infoType: InfoType) {}
|
||||
|
||||
override fun onAmountValueChange(value: String) {}
|
||||
|
|
@ -33,8 +38,6 @@ object StakingClickIntentsStub : StakingClickIntents {
|
|||
|
||||
override fun openRewardsValidators() {}
|
||||
|
||||
override fun selectRewardValidator(rewardValue: String) {}
|
||||
|
||||
override fun onExploreClick() {}
|
||||
|
||||
override fun onShareClick() {}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.transformers
|
||||
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.staking.model.stakekit.StakingError
|
||||
import com.tangem.features.staking.impl.presentation.state.*
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
internal class AddStakingErrorTransformer(
|
||||
private val error: StakingError,
|
||||
) : Transformer<StakingUiState> {
|
||||
|
||||
override fun transform(prevState: StakingUiState): StakingUiState {
|
||||
val confirmationState =
|
||||
prevState.confirmationState as? StakingStates.ConfirmationState.Data ?: return prevState
|
||||
|
||||
return prevState.copy(
|
||||
confirmationState = confirmationState.copy(
|
||||
notifications = (confirmationState.notifications + convertToNotification(error)).toPersistentList(),
|
||||
feeState = FeeState.Error,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertToNotification(error: StakingError): StakingNotification {
|
||||
return when (error) {
|
||||
is StakingError.StakedPositionNotFoundError -> StakingNotification.Error.StakedPositionNotFoundError(
|
||||
message = error.toString(),
|
||||
)
|
||||
// TODO staking
|
||||
else -> StakingNotification.Error.Common(
|
||||
subtitle = stringReference(error.toString()),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,232 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.transformers
|
||||
|
||||
import com.tangem.common.ui.navigationButtons.NavigationButton
|
||||
import com.tangem.common.ui.navigationButtons.NavigationButtonsState
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.domain.staking.model.stakekit.PendingAction
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionType
|
||||
import com.tangem.features.staking.impl.presentation.state.*
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
internal class SetButtonsStateTransformer : Transformer<StakingUiState> {
|
||||
|
||||
override fun transform(prevState: StakingUiState): StakingUiState {
|
||||
val confirmState = prevState.confirmationState as? StakingStates.ConfirmationState.Data
|
||||
|
||||
val buttonsState = if (prevState.isButtonsVisible()) {
|
||||
NavigationButtonsState.Data(
|
||||
primaryButton = getPrimaryButton(prevState),
|
||||
prevButton = getPrevButton(prevState),
|
||||
secondaryButton = getSecondaryButton(prevState),
|
||||
extraButtons = getExtraButtons(prevState),
|
||||
txUrl = (confirmState?.transactionDoneState as? TransactionDoneState.Content)?.txUrl,
|
||||
)
|
||||
} else {
|
||||
NavigationButtonsState.Empty
|
||||
}
|
||||
|
||||
return prevState.copy(buttonsState = buttonsState)
|
||||
}
|
||||
|
||||
private fun getPrimaryButton(prevState: StakingUiState): NavigationButton {
|
||||
val confirmState = prevState.confirmationState as? StakingStates.ConfirmationState.Data
|
||||
val innerConfirmState = confirmState?.innerState
|
||||
|
||||
val isPrimaryInProgress =
|
||||
confirmState?.pendingActions?.getPrimaryAction() == confirmState?.pendingActionInProgress
|
||||
val isConfirmation = prevState.currentStep == StakingStep.Confirmation
|
||||
val isInProgress = innerConfirmState == InnerConfirmationStakingState.IN_PROGRESS
|
||||
val isCompleted = innerConfirmState == InnerConfirmationStakingState.COMPLETED
|
||||
|
||||
val isIconVisible = isConfirmation && !isCompleted
|
||||
val isShowProgress = isInProgress && isPrimaryInProgress
|
||||
return NavigationButton(
|
||||
textReference = prevState.getButtonText(),
|
||||
iconRes = R.drawable.ic_tangem_24,
|
||||
isSecondary = false,
|
||||
isIconVisible = isIconVisible,
|
||||
showProgress = isShowProgress,
|
||||
isEnabled = prevState.isButtonEnabled(),
|
||||
onClick = { prevState.onPrimaryClick() },
|
||||
)
|
||||
}
|
||||
|
||||
private fun getSecondaryButton(prevState: StakingUiState): NavigationButton? {
|
||||
val confirmState = prevState.confirmationState as? StakingStates.ConfirmationState.Data
|
||||
val innerConfirmState = confirmState?.innerState
|
||||
|
||||
val isConfirmation = prevState.currentStep == StakingStep.Confirmation
|
||||
val isInProgress = innerConfirmState == InnerConfirmationStakingState.IN_PROGRESS
|
||||
val isCompleted = innerConfirmState == InnerConfirmationStakingState.COMPLETED
|
||||
|
||||
return confirmState?.pendingActions?.getSecondaryAction()?.let { pendingAction ->
|
||||
val isSecondaryInProgress = pendingAction == confirmState.pendingActionInProgress
|
||||
val isShowProgress = isInProgress && isSecondaryInProgress
|
||||
NavigationButton(
|
||||
textReference = getPendingActionTitle(pendingAction.type),
|
||||
iconRes = R.drawable.ic_tangem_24,
|
||||
isSecondary = true,
|
||||
isIconVisible = true,
|
||||
showProgress = isShowProgress,
|
||||
isEnabled = prevState.isButtonEnabled(),
|
||||
onClick = { prevState.clickIntents.onActionClick(pendingAction) },
|
||||
).takeIf { isConfirmation && !isCompleted }
|
||||
}
|
||||
}
|
||||
|
||||
private fun getPrevButton(prevState: StakingUiState): NavigationButton? {
|
||||
return NavigationButton(
|
||||
textReference = TextReference.EMPTY,
|
||||
iconRes = R.drawable.ic_back_24,
|
||||
isSecondary = true,
|
||||
isIconVisible = true,
|
||||
showProgress = false,
|
||||
isEnabled = true,
|
||||
onClick = prevState.clickIntents::onPrevClick,
|
||||
).takeIf { prevState.currentStep.isPrevButtonVisible() }
|
||||
}
|
||||
|
||||
private fun getExtraButtons(prevState: StakingUiState): ImmutableList<NavigationButton> {
|
||||
return persistentListOf(
|
||||
NavigationButton(
|
||||
textReference = resourceReference(R.string.common_explore),
|
||||
iconRes = R.drawable.ic_web_24,
|
||||
isSecondary = true,
|
||||
isIconVisible = true,
|
||||
showProgress = false,
|
||||
isEnabled = true,
|
||||
onClick = prevState.clickIntents::onExploreClick,
|
||||
),
|
||||
NavigationButton(
|
||||
textReference = resourceReference(R.string.common_share),
|
||||
iconRes = R.drawable.ic_share_24,
|
||||
isSecondary = true,
|
||||
isIconVisible = true,
|
||||
showProgress = false,
|
||||
isEnabled = true,
|
||||
onClick = prevState.clickIntents::onShareClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun List<PendingAction>.getPrimaryAction(): PendingAction? = getOrNull(0)
|
||||
|
||||
private fun List<PendingAction>.getSecondaryAction(): PendingAction? = getOrNull(1)
|
||||
|
||||
private fun StakingUiState.isButtonsVisible(): Boolean = when (currentStep) {
|
||||
StakingStep.InitialInfo -> isStakeMoreAvailable()
|
||||
StakingStep.RewardsValidators -> false
|
||||
else -> true
|
||||
}
|
||||
|
||||
private fun StakingUiState.getButtonText(): TextReference {
|
||||
return when (currentStep) {
|
||||
StakingStep.InitialInfo -> {
|
||||
val initialState = initialInfoState as? StakingStates.InitialInfoState.Data
|
||||
if (initialState?.yieldBalance is InnerYieldBalanceState.Data) {
|
||||
resourceReference(R.string.staking_stake_more)
|
||||
} else {
|
||||
resourceReference(R.string.common_next)
|
||||
}
|
||||
}
|
||||
|
||||
StakingStep.Confirmation -> {
|
||||
if (confirmationState is StakingStates.ConfirmationState.Data) {
|
||||
if (confirmationState.innerState == InnerConfirmationStakingState.COMPLETED) {
|
||||
resourceReference(R.string.common_close)
|
||||
} else {
|
||||
when (actionType) {
|
||||
StakingActionCommonType.ENTER -> resourceReference(R.string.common_stake)
|
||||
StakingActionCommonType.EXIT -> resourceReference(R.string.common_unstake)
|
||||
StakingActionCommonType.PENDING_OTHER,
|
||||
StakingActionCommonType.PENDING_REWARDS,
|
||||
-> getPendingActionTitle(confirmationState.pendingActions.firstOrNull()?.type)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
resourceReference(R.string.common_close)
|
||||
}
|
||||
}
|
||||
StakingStep.Validators -> resourceReference(R.string.common_continue)
|
||||
StakingStep.Amount,
|
||||
StakingStep.RewardsValidators,
|
||||
-> resourceReference(R.string.common_next)
|
||||
}
|
||||
}
|
||||
|
||||
private fun StakingUiState.onPrimaryClick() {
|
||||
when (currentStep) {
|
||||
StakingStep.InitialInfo -> {
|
||||
val actionType = StakingActionCommonType.ENTER.takeIf { isStakeMoreAvailable() }
|
||||
clickIntents.onAmountValueChange("") // reset amount state
|
||||
clickIntents.onNextClick(actionType)
|
||||
}
|
||||
StakingStep.Validators,
|
||||
StakingStep.Amount,
|
||||
-> clickIntents.onNextClick()
|
||||
StakingStep.Confirmation -> {
|
||||
if (confirmationState is StakingStates.ConfirmationState.Data) {
|
||||
if (confirmationState.innerState == InnerConfirmationStakingState.COMPLETED) {
|
||||
clickIntents.onBackClick()
|
||||
} else {
|
||||
clickIntents.onActionClick(confirmationState.pendingActions.firstOrNull())
|
||||
}
|
||||
} else {
|
||||
clickIntents.onBackClick()
|
||||
}
|
||||
}
|
||||
StakingStep.RewardsValidators -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
private fun StakingStep.isPrevButtonVisible(): Boolean = when (this) {
|
||||
StakingStep.InitialInfo,
|
||||
StakingStep.RewardsValidators,
|
||||
StakingStep.Confirmation,
|
||||
StakingStep.Validators,
|
||||
-> false
|
||||
StakingStep.Amount,
|
||||
-> true
|
||||
}
|
||||
|
||||
private fun StakingUiState.isButtonEnabled(): Boolean {
|
||||
return when (currentStep) {
|
||||
StakingStep.InitialInfo -> initialInfoState.isPrimaryButtonEnabled
|
||||
StakingStep.Amount -> amountState.isPrimaryButtonEnabled
|
||||
StakingStep.Confirmation -> confirmationState.isPrimaryButtonEnabled
|
||||
StakingStep.RewardsValidators -> rewardsValidatorsState.isPrimaryButtonEnabled
|
||||
StakingStep.Validators -> true
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
private fun getPendingActionTitle(type: StakingActionType?): TextReference = when (type) {
|
||||
StakingActionType.CLAIM_REWARDS -> resourceReference(R.string.common_claim_rewards)
|
||||
StakingActionType.RESTAKE_REWARDS -> resourceReference(R.string.staking_restake_rewards)
|
||||
StakingActionType.WITHDRAW -> resourceReference(R.string.staking_withdraw)
|
||||
StakingActionType.RESTAKE -> resourceReference(R.string.staking_restake)
|
||||
StakingActionType.CLAIM_UNSTAKED -> resourceReference(R.string.staking_claim_unstaked)
|
||||
StakingActionType.UNLOCK_LOCKED -> resourceReference(R.string.staking_unlocked_locked)
|
||||
StakingActionType.STAKE_LOCKED -> resourceReference(R.string.staking_stake_locked)
|
||||
StakingActionType.VOTE -> resourceReference(R.string.staking_vote)
|
||||
StakingActionType.REVOKE -> resourceReference(R.string.staking_revoke)
|
||||
StakingActionType.VOTE_LOCKED -> resourceReference(R.string.staking_vote_locked)
|
||||
StakingActionType.REVOTE -> resourceReference(R.string.staking_revote)
|
||||
StakingActionType.REBOND -> resourceReference(R.string.staking_rebond)
|
||||
StakingActionType.MIGRATE -> resourceReference(R.string.staking_migrate)
|
||||
StakingActionType.STAKE -> resourceReference(R.string.common_stake)
|
||||
StakingActionType.UNSTAKE -> resourceReference(R.string.common_unstake)
|
||||
StakingActionType.UNKNOWN -> TextReference.EMPTY
|
||||
null -> TextReference.EMPTY
|
||||
}
|
||||
|
||||
private fun StakingUiState.isStakeMoreAvailable(): Boolean {
|
||||
val initialState = initialInfoState as? StakingStates.InitialInfoState.Data
|
||||
return initialState?.isStakeMoreAvailable == true || initialState?.yieldBalance is InnerYieldBalanceState.Empty
|
||||
}
|
||||
}
|
||||
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