Updated on 2026-08-14
This commit is contained in:
parent
65d9b4818b
commit
25dc4f27e5
30 changed files with 364 additions and 316 deletions
|
|
@ -20,7 +20,6 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
|||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.SpacerW8
|
||||
import com.tangem.core.ui.components.buttons.common.*
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
|
@ -99,7 +98,7 @@ private fun Button(
|
|||
val iconTint by animateColorAsState(
|
||||
targetValue = when {
|
||||
!config.enabled -> TangemTheme.colors.icon.informative
|
||||
config.dimContent -> TangemTheme.colors.icon.secondary
|
||||
config.dimContent -> TangemTheme.colors.icon.informative
|
||||
else -> TangemTheme.colors.icon.primary1
|
||||
},
|
||||
label = "Update tint color",
|
||||
|
|
@ -117,7 +116,7 @@ private fun Button(
|
|||
val textColor by animateColorAsState(
|
||||
targetValue = when {
|
||||
!config.enabled -> TangemTheme.colors.text.disabled
|
||||
config.dimContent -> TangemTheme.colors.text.secondary
|
||||
config.dimContent -> TangemTheme.colors.text.tertiary
|
||||
else -> TangemTheme.colors.text.primary1
|
||||
},
|
||||
label = "Update text color",
|
||||
|
|
|
|||
|
|
@ -21,8 +21,10 @@ data class CryptoCurrencyStatus(
|
|||
|
||||
/**
|
||||
* Represents the various states a token can have, encapsulating different information based on the state.
|
||||
*
|
||||
* @property isError Indicates whether this status represents an error status.
|
||||
*/
|
||||
sealed class Status {
|
||||
sealed class Status(val isError: Boolean) {
|
||||
|
||||
/** The amount of the token. */
|
||||
open val amount: BigDecimal? = null
|
||||
|
|
@ -47,16 +49,16 @@ data class CryptoCurrencyStatus(
|
|||
}
|
||||
|
||||
/** Represents the Loading state of a token, typically while fetching its details. */
|
||||
object Loading : Status()
|
||||
object Loading : Status(isError = false)
|
||||
|
||||
/** Represents a state where the token is not reachable. */
|
||||
object Unreachable : Status()
|
||||
object Unreachable : Status(isError = true)
|
||||
|
||||
/** Represents a state where the token's derivation is missed. */
|
||||
object MissedDerivation : Status()
|
||||
object MissedDerivation : Status(isError = true)
|
||||
|
||||
/** Represents a state where there is no account associated with the token. */
|
||||
object NoAccount : Status()
|
||||
object NoAccount : Status(isError = false)
|
||||
|
||||
/**
|
||||
* Represents a Loaded state of a token with complete information.
|
||||
|
|
@ -77,7 +79,7 @@ data class CryptoCurrencyStatus(
|
|||
override val hasCurrentNetworkTransactions: Boolean,
|
||||
override val pendingTransactions: Set<TxHistoryItem>,
|
||||
override val networkAddress: NetworkAddress?,
|
||||
) : Status()
|
||||
) : Status(isError = false)
|
||||
|
||||
/**
|
||||
* Represents a Custom state of a token, typically used for user-defined tokens.
|
||||
|
|
@ -98,7 +100,7 @@ data class CryptoCurrencyStatus(
|
|||
override val hasCurrentNetworkTransactions: Boolean,
|
||||
override val pendingTransactions: Set<TxHistoryItem>,
|
||||
override val networkAddress: NetworkAddress?,
|
||||
) : Status()
|
||||
) : Status(isError = false)
|
||||
|
||||
/**
|
||||
* Represents a state where the token is available, but there is no current quote available for it.
|
||||
|
|
@ -113,5 +115,5 @@ data class CryptoCurrencyStatus(
|
|||
override val hasCurrentNetworkTransactions: Boolean,
|
||||
override val pendingTransactions: Set<TxHistoryItem>,
|
||||
override val networkAddress: NetworkAddress?,
|
||||
) : Status()
|
||||
) : Status(isError = false)
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import com.tangem.core.ui.components.transactions.state.TransactionState
|
|||
import com.tangem.core.ui.components.transactions.state.TxHistoryState
|
||||
import com.tangem.core.ui.event.consumed
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.res.TangemColorPalette
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
|
||||
|
|
@ -164,7 +165,7 @@ internal object WalletPreviewData {
|
|||
id = UUID.randomUUID().toString(),
|
||||
icon = tokenIconState,
|
||||
name = "Polygon",
|
||||
fiatAmount = "3 172,14 $",
|
||||
info = stringReference(value = "3 172,14 $"),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -232,7 +233,7 @@ internal object WalletPreviewData {
|
|||
)
|
||||
}
|
||||
|
||||
val divider = DraggableItem.GroupPlaceholder(id = "divider_$networkNumber")
|
||||
val divider = DraggableItem.Placeholder(id = "divider_$networkNumber")
|
||||
|
||||
buildList {
|
||||
add(group)
|
||||
|
|
@ -267,7 +268,7 @@ internal object WalletPreviewData {
|
|||
),
|
||||
dndConfig = OrganizeTokensState.DragAndDropConfig(
|
||||
onItemDragged = { _, _ -> },
|
||||
onDragStart = {},
|
||||
onItemDragStart = {},
|
||||
canDragItemOver = { _, _ -> false },
|
||||
onItemDragEnd = {},
|
||||
),
|
||||
|
|
@ -365,7 +366,7 @@ internal object WalletPreviewData {
|
|||
),
|
||||
),
|
||||
),
|
||||
onOrganizeTokensClick = {},
|
||||
organizeTokensButton = WalletTokensListState.OrganizeTokensButtonState.Visible(isEnabled = true, {}),
|
||||
),
|
||||
pullToRefreshConfig = WalletPullToRefreshConfig(
|
||||
isRefreshing = false,
|
||||
|
|
|
|||
|
|
@ -3,11 +3,15 @@ package com.tangem.feature.wallet.presentation.common.component
|
|||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.composed
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
|
|
@ -25,27 +29,13 @@ import com.tangem.feature.wallet.presentation.common.component.token.icon.TokenI
|
|||
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
|
||||
import org.burnoutcrew.reorderable.ReorderableLazyListState
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
internal fun TokenItem(
|
||||
state: TokenItemState,
|
||||
modifier: Modifier = Modifier,
|
||||
reorderableTokenListState: ReorderableLazyListState? = null,
|
||||
) {
|
||||
val hapticFeedback = LocalHapticFeedback.current
|
||||
val containerModifier: Modifier = remember(state) {
|
||||
when (state) {
|
||||
is TokenItemState.Content -> modifier.combinedClickable(
|
||||
onClick = state.onItemClick,
|
||||
onLongClick = {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
state.onItemLongClick()
|
||||
},
|
||||
)
|
||||
else -> modifier
|
||||
}
|
||||
}
|
||||
BaseContainer(modifier = containerModifier) {
|
||||
BaseContainer(modifier = modifier.tokenClickable(state)) {
|
||||
val (iconRef, cryptoInfoRef, fiatInfoRef) = createRefs()
|
||||
|
||||
TokenIcon(
|
||||
|
|
@ -108,6 +98,31 @@ private fun Modifier.constrainAsOptionsItem(scope: ConstraintLayoutScope, ref: C
|
|||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
private fun Modifier.tokenClickable(state: TokenItemState): Modifier = composed {
|
||||
when (state) {
|
||||
is TokenItemState.Content -> {
|
||||
val hapticFeedback = LocalHapticFeedback.current
|
||||
val onLongClick = remember(state) {
|
||||
{
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
state.onItemLongClick()
|
||||
}
|
||||
}
|
||||
|
||||
this.combinedClickable(
|
||||
onClick = state.onItemClick,
|
||||
onLongClick = onLongClick,
|
||||
)
|
||||
}
|
||||
is TokenItemState.Draggable,
|
||||
is TokenItemState.Unreachable,
|
||||
is TokenItemState.Loading,
|
||||
is TokenItemState.Locked,
|
||||
-> this
|
||||
}
|
||||
}
|
||||
|
||||
// region preview
|
||||
@Preview
|
||||
@Composable
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.composed
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemTypography
|
||||
import com.tangem.feature.wallet.impl.R
|
||||
|
|
@ -40,7 +41,7 @@ private fun ContentBlock(state: TokenItemState.ContentState, modifier: Modifier
|
|||
AmountText(
|
||||
amount = when (state) {
|
||||
is TokenItemState.Content -> if (state.tokenOptions is TokenOptionsState.Hidden) DOTS else state.amount
|
||||
is TokenItemState.Draggable -> state.fiatAmount
|
||||
is TokenItemState.Draggable -> state.info.resolveReference()
|
||||
is TokenItemState.Unreachable -> null
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import androidx.annotation.DrawableRes
|
|||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeConfig
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
/** Token item state */
|
||||
@Immutable
|
||||
|
|
@ -19,6 +20,7 @@ internal sealed interface TokenItemState {
|
|||
data class Locked(override val id: String) : TokenItemState
|
||||
|
||||
/** Content state */
|
||||
@Immutable
|
||||
sealed class ContentState : TokenItemState {
|
||||
|
||||
abstract val icon: IconState
|
||||
|
|
@ -54,13 +56,13 @@ internal sealed interface TokenItemState {
|
|||
* @property id unique id
|
||||
* @property icon token icon state
|
||||
* @property name token name
|
||||
* @property fiatAmount fiat amount of token
|
||||
* @property info token info (e.g. fiat balance or status)
|
||||
*/
|
||||
data class Draggable(
|
||||
override val id: String,
|
||||
override val icon: IconState,
|
||||
override val name: String,
|
||||
val fiatAmount: String,
|
||||
val info: TextReference,
|
||||
) : ContentState()
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -3,28 +3,32 @@ package com.tangem.feature.wallet.presentation.common.utils
|
|||
import com.tangem.core.ui.extensions.getTintForTokenIcon
|
||||
import com.tangem.core.ui.extensions.networkIconResId
|
||||
import com.tangem.core.ui.extensions.tryGetBackgroundForTokenIcon
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.models.CryptoCurrency
|
||||
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal class CryptoCurrencyToIconStateConverter : Converter<CryptoCurrency, TokenItemState.IconState> {
|
||||
internal class CryptoCurrencyToIconStateConverter : Converter<CryptoCurrencyStatus, TokenItemState.IconState> {
|
||||
|
||||
override fun convert(value: CryptoCurrency): TokenItemState.IconState {
|
||||
return when (value) {
|
||||
is CryptoCurrency.Coin -> getIconStateForCoin(value)
|
||||
is CryptoCurrency.Token -> getIconStateForToken(value)
|
||||
override fun convert(value: CryptoCurrencyStatus): TokenItemState.IconState {
|
||||
return when (val currency = value.currency) {
|
||||
is CryptoCurrency.Coin -> getIconStateForCoin(currency, value.value.isError)
|
||||
is CryptoCurrency.Token -> getIconStateForToken(currency, value.value.isError)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getIconStateForCoin(coin: CryptoCurrency.Coin): TokenItemState.IconState.CoinIcon {
|
||||
private fun getIconStateForCoin(
|
||||
coin: CryptoCurrency.Coin,
|
||||
isUnreachable: Boolean,
|
||||
): TokenItemState.IconState.CoinIcon {
|
||||
return TokenItemState.IconState.CoinIcon(
|
||||
url = coin.iconUrl,
|
||||
fallbackResId = coin.networkIconResId,
|
||||
isGrayscale = coin.network.isTestnet,
|
||||
isGrayscale = coin.network.isTestnet || isUnreachable,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getIconStateForToken(token: CryptoCurrency.Token): TokenItemState.IconState {
|
||||
private fun getIconStateForToken(token: CryptoCurrency.Token, isErrorStatus: Boolean): TokenItemState.IconState {
|
||||
val background = token.tryGetBackgroundForTokenIcon()
|
||||
val tint = getTintForTokenIcon(background)
|
||||
|
||||
|
|
@ -33,13 +37,13 @@ internal class CryptoCurrencyToIconStateConverter : Converter<CryptoCurrency, To
|
|||
tint = tint,
|
||||
background = background,
|
||||
networkBadgeIconResId = token.networkIconResId,
|
||||
isGrayscale = token.network.isTestnet,
|
||||
isGrayscale = token.network.isTestnet || isErrorStatus,
|
||||
)
|
||||
} else {
|
||||
TokenItemState.IconState.TokenIcon(
|
||||
url = token.iconUrl,
|
||||
networkBadgeIconResId = token.networkIconResId,
|
||||
isGrayscale = token.network.isTestnet,
|
||||
isGrayscale = token.network.isTestnet || isErrorStatus,
|
||||
fallbackTint = tint,
|
||||
fallbackBackground = background,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
package com.tangem.feature.wallet.presentation.organizetokens
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.core.LinearOutSlowInEasing
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.*
|
||||
|
|
@ -21,10 +18,13 @@ import androidx.compose.ui.draw.shadow
|
|||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
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.datasource.CollectionPreviewParameterProvider
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.components.SecondaryButton
|
||||
import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig
|
||||
|
|
@ -96,27 +96,28 @@ private fun TokenList(
|
|||
canDragOver = dndConfig.canDragItemOver,
|
||||
onDragEnd = onDragEnd,
|
||||
)
|
||||
val items = state.items
|
||||
|
||||
val listContentPadding = PaddingValues(
|
||||
top = TangemTheme.dimens.spacing12,
|
||||
bottom = TangemTheme.dimens.spacing92,
|
||||
start = TangemTheme.dimens.spacing16,
|
||||
end = TangemTheme.dimens.spacing16,
|
||||
)
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.reorderable(reorderableListState)
|
||||
.align(Alignment.TopCenter)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.fillMaxSize(),
|
||||
.reorderable(reorderableListState),
|
||||
state = reorderableListState.listState,
|
||||
contentPadding = PaddingValues(
|
||||
top = TangemTheme.dimens.spacing12,
|
||||
bottom = TangemTheme.dimens.spacing92,
|
||||
),
|
||||
contentPadding = listContentPadding,
|
||||
) {
|
||||
itemsIndexed(
|
||||
items = items,
|
||||
items = state.items,
|
||||
key = { _, item -> item.id },
|
||||
) { index, item ->
|
||||
|
||||
val onDragStart = remember(item) {
|
||||
{ dndConfig.onDragStart(item) }
|
||||
{ dndConfig.onItemDragStart(item) }
|
||||
}
|
||||
|
||||
DraggableItem(
|
||||
|
|
@ -132,7 +133,6 @@ private fun TokenList(
|
|||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
private fun LazyItemScope.DraggableItem(
|
||||
index: Int,
|
||||
|
|
@ -140,15 +140,18 @@ private fun LazyItemScope.DraggableItem(
|
|||
reorderableState: ReorderableLazyListState,
|
||||
onDragStart: () -> Unit,
|
||||
) {
|
||||
var isDragging by remember {
|
||||
mutableStateOf(value = false)
|
||||
}
|
||||
|
||||
val itemModifier = Modifier.applyShapeAndShadow(item.roundingMode, item.showShadow)
|
||||
|
||||
ReorderableItem(
|
||||
defaultDraggingModifier = Modifier.animateItemPlacement(
|
||||
animationSpec = tween(easing = LinearOutSlowInEasing),
|
||||
),
|
||||
state = reorderableState,
|
||||
reorderableState = reorderableState,
|
||||
index = index,
|
||||
key = item.id,
|
||||
) { isDragging ->
|
||||
val itemModifier = Modifier.applyShapeAndShadow(item.roundingMode, item.showShadow)
|
||||
) { isItemDragging ->
|
||||
isDragging = isItemDragging
|
||||
|
||||
when (item) {
|
||||
is DraggableItem.GroupHeader -> DraggableNetworkGroupItem(
|
||||
|
|
@ -162,10 +165,12 @@ private fun LazyItemScope.DraggableItem(
|
|||
reorderableTokenListState = reorderableState,
|
||||
)
|
||||
// Should be presented in the list but remain invisible
|
||||
is DraggableItem.GroupPlaceholder -> Box(modifier = Modifier.fillMaxWidth())
|
||||
is DraggableItem.Placeholder -> Box(modifier = Modifier.fillMaxWidth())
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(isDragging) {
|
||||
DisposableEffect(isDragging) {
|
||||
onDispose {
|
||||
if (isDragging) {
|
||||
onDragStart()
|
||||
}
|
||||
|
|
@ -288,57 +293,71 @@ private fun Actions(config: OrganizeTokensState.ActionsConfig, modifier: Modifie
|
|||
}
|
||||
}
|
||||
|
||||
private fun Modifier.applyShapeAndShadow(roundingMode: DraggableItem.RoundingMode, showShadow: Boolean): Modifier =
|
||||
composed {
|
||||
private fun Modifier.applyShapeAndShadow(roundingMode: DraggableItem.RoundingMode, showShadow: Boolean): Modifier {
|
||||
return composed {
|
||||
val radius by animateDpAsState(
|
||||
targetValue = if (roundingMode !is DraggableItem.RoundingMode.None) {
|
||||
TangemTheme.dimens.radius16
|
||||
} else {
|
||||
TangemTheme.dimens.radius0
|
||||
targetValue = when (roundingMode) {
|
||||
is DraggableItem.RoundingMode.None -> TangemTheme.dimens.radius0
|
||||
is DraggableItem.RoundingMode.All -> TangemTheme.dimens.radius12
|
||||
is DraggableItem.RoundingMode.Bottom,
|
||||
is DraggableItem.RoundingMode.Top,
|
||||
-> TangemTheme.dimens.radius16
|
||||
},
|
||||
label = "item_shape_radius",
|
||||
)
|
||||
val shape = when (roundingMode) {
|
||||
is DraggableItem.RoundingMode.None -> RectangleShape
|
||||
is DraggableItem.RoundingMode.Top -> RoundedCornerShape(
|
||||
topStart = radius,
|
||||
topEnd = radius,
|
||||
)
|
||||
is DraggableItem.RoundingMode.Bottom -> RoundedCornerShape(
|
||||
bottomStart = radius,
|
||||
bottomEnd = radius,
|
||||
)
|
||||
is DraggableItem.RoundingMode.All -> RoundedCornerShape(
|
||||
size = radius,
|
||||
)
|
||||
}
|
||||
|
||||
val paddingValue = TangemTheme.dimens.spacing4
|
||||
val padding = if (roundingMode.showGap) {
|
||||
when (roundingMode) {
|
||||
is DraggableItem.RoundingMode.None -> null
|
||||
is DraggableItem.RoundingMode.All -> PaddingValues(vertical = paddingValue)
|
||||
is DraggableItem.RoundingMode.Top -> PaddingValues(top = paddingValue)
|
||||
is DraggableItem.RoundingMode.Bottom -> PaddingValues(bottom = paddingValue)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val elevation by animateDpAsState(
|
||||
targetValue = if (showShadow) {
|
||||
TangemTheme.dimens.elevation8
|
||||
} else {
|
||||
TangemTheme.dimens.elevation0
|
||||
},
|
||||
label = "item_elevation",
|
||||
)
|
||||
|
||||
this
|
||||
.let {
|
||||
if (padding != null) {
|
||||
it.padding(padding)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
.padding(paddingValues = getItemGap(roundingMode))
|
||||
.shadow(
|
||||
elevation = if (showShadow) TangemTheme.dimens.elevation12 else TangemTheme.dimens.elevation0,
|
||||
shape = shape,
|
||||
elevation = elevation,
|
||||
shape = getItemShape(roundingMode, radius),
|
||||
clip = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
private fun getItemGap(roundingMode: DraggableItem.RoundingMode): PaddingValues {
|
||||
val paddingValue = TangemTheme.dimens.spacing4
|
||||
|
||||
return if (roundingMode.showGap) {
|
||||
when (roundingMode) {
|
||||
is DraggableItem.RoundingMode.None -> PaddingValues(all = 0.dp)
|
||||
is DraggableItem.RoundingMode.All -> PaddingValues(vertical = paddingValue)
|
||||
is DraggableItem.RoundingMode.Top -> PaddingValues(top = paddingValue)
|
||||
is DraggableItem.RoundingMode.Bottom -> PaddingValues(bottom = paddingValue)
|
||||
}
|
||||
} else {
|
||||
PaddingValues(all = 0.dp)
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
private fun getItemShape(roundingMode: DraggableItem.RoundingMode, radius: Dp): Shape {
|
||||
return when (roundingMode) {
|
||||
is DraggableItem.RoundingMode.None -> RectangleShape
|
||||
is DraggableItem.RoundingMode.Top -> RoundedCornerShape(
|
||||
topStart = radius,
|
||||
topEnd = radius,
|
||||
)
|
||||
is DraggableItem.RoundingMode.Bottom -> RoundedCornerShape(
|
||||
bottomStart = radius,
|
||||
bottomEnd = radius,
|
||||
)
|
||||
is DraggableItem.RoundingMode.All -> RoundedCornerShape(
|
||||
size = radius,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// region Preview
|
||||
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ internal class OrganizeTokensStateHolder(
|
|||
),
|
||||
dndConfig = OrganizeTokensState.DragAndDropConfig(
|
||||
onItemDragged = dragAndDropIntents::onItemDragged,
|
||||
onDragStart = dragAndDropIntents::onItemDraggingStart,
|
||||
onItemDragStart = dragAndDropIntents::onItemDraggingStart,
|
||||
onItemDragEnd = dragAndDropIntents::onItemDraggingEnd,
|
||||
canDragItemOver = dragAndDropIntents::canDragItemOver,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -20,13 +20,14 @@ import com.tangem.feature.wallet.presentation.organizetokens.utils.common.disabl
|
|||
import com.tangem.feature.wallet.presentation.organizetokens.utils.dnd.DragAndDropAdapter
|
||||
import com.tangem.feature.wallet.presentation.router.InnerWalletRouter
|
||||
import com.tangem.feature.wallet.presentation.router.WalletRoute
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@HiltViewModel
|
||||
internal class OrganizeTokensViewModel @Inject constructor(
|
||||
private val getTokenListUseCase: GetTokenListUseCase,
|
||||
|
|
@ -34,6 +35,7 @@ internal class OrganizeTokensViewModel @Inject constructor(
|
|||
private val toggleTokenListSortingUseCase: ToggleTokenListSortingUseCase,
|
||||
private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase,
|
||||
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
) : ViewModel(), OrganizeTokensIntents {
|
||||
|
||||
|
|
@ -43,7 +45,6 @@ internal class OrganizeTokensViewModel @Inject constructor(
|
|||
|
||||
private val dragAndDropAdapter = DragAndDropAdapter(
|
||||
listStateProvider = Provider { uiState.value.itemsState },
|
||||
scope = viewModelScope,
|
||||
)
|
||||
|
||||
private val stateHolder = OrganizeTokensStateHolder(
|
||||
|
|
@ -72,7 +73,7 @@ internal class OrganizeTokensViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onSortClick() {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
viewModelScope.launch(dispatchers.default) {
|
||||
val list = tokenList ?: return@launch
|
||||
|
||||
toggleTokenListSortingUseCase(list).fold(
|
||||
|
|
@ -86,7 +87,7 @@ internal class OrganizeTokensViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onGroupClick() {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
viewModelScope.launch(dispatchers.default) {
|
||||
val list = tokenList ?: return@launch
|
||||
|
||||
toggleTokenListGroupingUseCase(list).fold(
|
||||
|
|
@ -100,7 +101,7 @@ internal class OrganizeTokensViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onApplyClick() {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
viewModelScope.launch(dispatchers.default) {
|
||||
stateHolder.updateStateToDisplayProgress()
|
||||
|
||||
val listState = uiState.value.itemsState
|
||||
|
|
@ -117,7 +118,9 @@ internal class OrganizeTokensViewModel @Inject constructor(
|
|||
ifLeft = stateHolder::updateStateWithError,
|
||||
ifRight = {
|
||||
stateHolder.updateStateToHideProgress()
|
||||
withContext(Dispatchers.Main) { router.popBackStack() }
|
||||
withContext(
|
||||
dispatchers.main,
|
||||
) { router.popBackStack() }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -128,9 +131,9 @@ internal class OrganizeTokensViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun bootstrapTokenList() {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
viewModelScope.launch(dispatchers.default) {
|
||||
val maybeTokenList = getTokenListUseCase(userWalletId)
|
||||
.first { it.getOrNull()?.totalFiatBalance is TokenList.FiatBalance.Loaded }
|
||||
.first { it.getOrNull()?.totalFiatBalance !is TokenList.FiatBalance.Loading }
|
||||
|
||||
maybeTokenList.fold(
|
||||
ifLeft = stateHolder::updateStateWithError,
|
||||
|
|
|
|||
|
|
@ -51,12 +51,11 @@ internal sealed class DraggableItem {
|
|||
}
|
||||
|
||||
/**
|
||||
* Helper item used to detect possible positions where a network group can be placed.
|
||||
* Used only on [OrganizeTokensListState.GroupedByNetwork] and placed between network groups.
|
||||
* Helper item used to detect possible positions where a draggable item can be placed.
|
||||
*
|
||||
* @property id ID of the placeholder
|
||||
* */
|
||||
data class GroupPlaceholder(
|
||||
data class Placeholder(
|
||||
override val id: String,
|
||||
) : DraggableItem() {
|
||||
override val showShadow: Boolean = false
|
||||
|
|
@ -109,7 +108,7 @@ internal sealed class DraggableItem {
|
|||
* @return updated [DraggableItem]
|
||||
* */
|
||||
fun updateRoundingMode(mode: RoundingMode): DraggableItem = when (this) {
|
||||
is GroupPlaceholder -> this
|
||||
is Placeholder -> this
|
||||
is GroupHeader -> this.copy(roundingMode = mode)
|
||||
is Token -> this.copy(roundingMode = mode)
|
||||
}
|
||||
|
|
@ -122,7 +121,7 @@ internal sealed class DraggableItem {
|
|||
* @return updated [DraggableItem]
|
||||
* */
|
||||
fun updateShadowVisibility(show: Boolean): DraggableItem = when (this) {
|
||||
is GroupPlaceholder -> this
|
||||
is Placeholder -> this
|
||||
is GroupHeader -> this.copy(showShadow = show)
|
||||
is Token -> this.copy(showShadow = show)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ internal sealed class OrganizeTokensListState {
|
|||
) : OrganizeTokensListState()
|
||||
|
||||
data class Ungrouped(
|
||||
override val items: PersistentList<DraggableItem.Token>,
|
||||
override val items: PersistentList<DraggableItem>,
|
||||
) : OrganizeTokensListState()
|
||||
|
||||
object Empty : OrganizeTokensListState() {
|
||||
|
|
|
|||
|
|
@ -33,6 +33,6 @@ internal data class OrganizeTokensState(
|
|||
val onItemDragged: (ItemPosition, ItemPosition) -> Unit,
|
||||
val canDragItemOver: (ItemPosition, ItemPosition) -> Boolean,
|
||||
val onItemDragEnd: () -> Unit,
|
||||
val onDragStart: (DraggableItem) -> Unit,
|
||||
val onItemDragStart: (DraggableItem) -> Unit,
|
||||
)
|
||||
}
|
||||
|
|
@ -11,7 +11,7 @@ internal class CryptoCurrenciesIdsResolver {
|
|||
val draggableTokens = when (listState) {
|
||||
is OrganizeTokensListState.Empty -> return emptyList()
|
||||
is OrganizeTokensListState.GroupedByNetwork -> listState.items.filterIsInstance<DraggableItem.Token>()
|
||||
is OrganizeTokensListState.Ungrouped -> listState.items
|
||||
is OrganizeTokensListState.Ungrouped -> listState.items.filterIsInstance<DraggableItem.Token>()
|
||||
}
|
||||
val currenciesStatuses = when (tokenList) {
|
||||
is TokenList.GroupedByNetwork -> tokenList.groups.flatMap { it.currencies }
|
||||
|
|
|
|||
|
|
@ -2,6 +2,6 @@ package com.tangem.feature.wallet.presentation.organizetokens.utils.common
|
|||
|
||||
import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem
|
||||
|
||||
internal fun getGroupPlaceholder(index: Int): DraggableItem.GroupPlaceholder {
|
||||
return DraggableItem.GroupPlaceholder(id = "placeholder_${index.inc()}")
|
||||
internal fun getGroupPlaceholder(index: Int): DraggableItem.Placeholder {
|
||||
return DraggableItem.Placeholder(id = "placeholder_${index.inc()}")
|
||||
}
|
||||
|
|
@ -3,20 +3,22 @@ package com.tangem.feature.wallet.presentation.organizetokens.utils.common
|
|||
import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem
|
||||
|
||||
internal fun List<DraggableItem>.uniteItems(): List<DraggableItem> {
|
||||
val lastItemIndex = this.lastIndex
|
||||
val items = prepareItems()
|
||||
val lastItemIndex = items.lastIndex
|
||||
|
||||
return this.mapIndexed { index, item ->
|
||||
return prepareItems().mapIndexed { index, item ->
|
||||
val mode = when (index) {
|
||||
0 -> DraggableItem.RoundingMode.Top()
|
||||
// 1 index is used because the first item is always a placeholder, check `prepareItems()` function
|
||||
1 -> DraggableItem.RoundingMode.Top()
|
||||
lastItemIndex -> DraggableItem.RoundingMode.Bottom()
|
||||
else -> when (item) {
|
||||
is DraggableItem.Placeholder -> DraggableItem.RoundingMode.None
|
||||
is DraggableItem.GroupHeader -> DraggableItem.RoundingMode.Top(showGap = true)
|
||||
is DraggableItem.Token -> if (this[index + 1] is DraggableItem.GroupPlaceholder) {
|
||||
is DraggableItem.Token -> if (items[index + 1] is DraggableItem.Placeholder) {
|
||||
DraggableItem.RoundingMode.Bottom(showGap = true)
|
||||
} else {
|
||||
DraggableItem.RoundingMode.None
|
||||
}
|
||||
is DraggableItem.GroupPlaceholder -> DraggableItem.RoundingMode.None
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -24,4 +26,49 @@ internal fun List<DraggableItem>.uniteItems(): List<DraggableItem> {
|
|||
.updateRoundingMode(mode)
|
||||
.updateShadowVisibility(show = false)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun List<DraggableItem>.divideMovingItem(movingItem: DraggableItem): List<DraggableItem> {
|
||||
val mutableList = this.toMutableList()
|
||||
val listIterator = mutableList.listIterator()
|
||||
|
||||
while (listIterator.hasNext()) {
|
||||
val item = listIterator.next()
|
||||
|
||||
if (item.id == movingItem.id) {
|
||||
val dividedItem = movingItem
|
||||
.updateRoundingMode(DraggableItem.RoundingMode.All())
|
||||
.updateShadowVisibility(show = true)
|
||||
|
||||
listIterator.set(dividedItem)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return mutableList
|
||||
}
|
||||
|
||||
/**
|
||||
* !!! Workaround !!!
|
||||
*
|
||||
* We need to add a [DraggableItem.Placeholder] (since it's not draggable) as the first item of the list, because the
|
||||
* [DND library](https://github.com/aclassen/ComposeReorderable) glitches when a user tries to drag the first item.
|
||||
*
|
||||
* @since 07.09.2023
|
||||
* */
|
||||
private fun List<DraggableItem>.prepareItems(): List<DraggableItem> {
|
||||
val firstPlaceholderId = "initial_placeholder"
|
||||
val items = this
|
||||
|
||||
return mutableListOf<DraggableItem>().apply {
|
||||
add(DraggableItem.Placeholder(firstPlaceholderId))
|
||||
|
||||
val itemsWithoutFirstPlaceholder = if (items.firstOrNull()?.id == firstPlaceholderId) {
|
||||
items.drop(n = 1)
|
||||
} else {
|
||||
items
|
||||
}
|
||||
|
||||
addAll(itemsWithoutFirstPlaceholder)
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,6 @@ import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeToken
|
|||
import kotlinx.collections.immutable.PersistentList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
internal inline fun OrganizeTokensListState.updateItems(
|
||||
update: (PersistentList<DraggableItem>) -> List<DraggableItem>,
|
||||
): OrganizeTokensListState {
|
||||
|
|
@ -13,7 +12,7 @@ internal inline fun OrganizeTokensListState.updateItems(
|
|||
|
||||
return when (this) {
|
||||
is OrganizeTokensListState.GroupedByNetwork -> copy(items = updatedItems)
|
||||
is OrganizeTokensListState.Ungrouped -> copy(items = updatedItems as PersistentList<DraggableItem.Token>)
|
||||
is OrganizeTokensListState.Ungrouped -> copy(items = updatedItems)
|
||||
is OrganizeTokensListState.Empty -> this
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,12 @@
|
|||
package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items
|
||||
|
||||
import com.tangem.common.Provider
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.feature.wallet.impl.R
|
||||
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
|
||||
import com.tangem.feature.wallet.presentation.common.utils.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem
|
||||
|
|
@ -45,9 +48,13 @@ internal class CryptoCurrencyToDraggableItemConverter(
|
|||
|
||||
return TokenItemState.Draggable(
|
||||
id = getTokenItemId(currency.id),
|
||||
icon = iconStateConverter.convert(currency),
|
||||
icon = iconStateConverter.convert(currencyStatus),
|
||||
name = currency.name,
|
||||
fiatAmount = getFormattedFiatAmount(currencyStatus, appCurrency),
|
||||
info = if (currencyStatus.value.isError) {
|
||||
resourceReference(id = R.string.common_unreachable)
|
||||
} else {
|
||||
stringReference(getFormattedFiatAmount(currencyStatus, appCurrency))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -36,6 +36,6 @@ internal class NetworkGroupToDraggableItemsConverter(
|
|||
)
|
||||
|
||||
private fun createTokens(group: NetworkGroup): List<DraggableItem.Token> {
|
||||
return itemConverter.convertList(group.currencies.toList())
|
||||
return itemConverter.convertList(group.currencies)
|
||||
}
|
||||
}
|
||||
|
|
@ -4,21 +4,17 @@ import com.tangem.common.Provider
|
|||
import com.tangem.feature.wallet.presentation.organizetokens.DragAndDropIntents
|
||||
import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem
|
||||
import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState
|
||||
import com.tangem.feature.wallet.presentation.organizetokens.utils.common.divideMovingItem
|
||||
import com.tangem.feature.wallet.presentation.organizetokens.utils.common.uniteItems
|
||||
import com.tangem.feature.wallet.presentation.organizetokens.utils.common.updateItems
|
||||
import kotlinx.collections.immutable.mutate
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import org.burnoutcrew.reorderable.ItemPosition
|
||||
|
||||
internal class DragAndDropAdapter(
|
||||
private val listStateProvider: Provider<OrganizeTokensListState>,
|
||||
private val scope: CoroutineScope,
|
||||
) : DragAndDropIntents {
|
||||
|
||||
private val draggableGroupsOperations = DraggableGroupsOperations()
|
||||
|
|
@ -37,9 +33,12 @@ internal class DragAndDropAdapter(
|
|||
get() = listStateFlowInternal
|
||||
|
||||
override fun canDragItemOver(dragOver: ItemPosition, dragging: ItemPosition): Boolean {
|
||||
val items = (currentListState as? OrganizeTokensListState.GroupedByNetwork)
|
||||
?.items
|
||||
?: return true // If ungrouped then item can be moved anywhere
|
||||
val items = when (val listState = currentListState) {
|
||||
is OrganizeTokensListState.GroupedByNetwork -> listState.items
|
||||
is OrganizeTokensListState.Empty,
|
||||
is OrganizeTokensListState.Ungrouped,
|
||||
-> return true // If ungrouped then item can be moved anywhere
|
||||
}
|
||||
|
||||
val (dragOverItem, draggingItem) = findItemsToMove(
|
||||
items = items,
|
||||
|
|
@ -54,7 +53,7 @@ internal class DragAndDropAdapter(
|
|||
return when (draggingItem) {
|
||||
is DraggableItem.GroupHeader -> checkCanMoveHeaderOver(dragOver, dragOverItem, items.lastIndex)
|
||||
is DraggableItem.Token -> checkCanMoveTokenOver(draggingItem, dragOverItem)
|
||||
is DraggableItem.GroupPlaceholder -> false
|
||||
is DraggableItem.Placeholder -> false
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -64,11 +63,11 @@ internal class DragAndDropAdapter(
|
|||
|
||||
updateListState {
|
||||
when (item) {
|
||||
is DraggableItem.GroupPlaceholder -> items
|
||||
is DraggableItem.Placeholder -> items
|
||||
is DraggableItem.GroupHeader -> draggableGroupsOperations.collapseGroup(items, item)
|
||||
is DraggableItem.Token -> when (this) {
|
||||
is OrganizeTokensListState.GroupedByNetwork -> draggableGroupsOperations.divideGroups(items, item)
|
||||
is OrganizeTokensListState.Ungrouped -> divideTokens(items, item)
|
||||
is OrganizeTokensListState.GroupedByNetwork -> items.divideMovingItem(item)
|
||||
is OrganizeTokensListState.Ungrouped -> items.divideMovingItem(item)
|
||||
is OrganizeTokensListState.Empty -> items
|
||||
}
|
||||
}
|
||||
|
|
@ -76,21 +75,17 @@ internal class DragAndDropAdapter(
|
|||
}
|
||||
|
||||
override fun onItemDraggingEnd() {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
val draggingItem = currentDraggingItem ?: return@launch
|
||||
val draggingItem = currentDraggingItem ?: return
|
||||
|
||||
delay(FINISH_DRAGGING_DELAY_MILLIS)
|
||||
|
||||
updateListState {
|
||||
when (draggingItem) {
|
||||
is DraggableItem.GroupHeader -> draggableGroupsOperations.expandGroups(items)
|
||||
is DraggableItem.Token -> items.uniteItems()
|
||||
is DraggableItem.GroupPlaceholder -> items
|
||||
}
|
||||
updateListState {
|
||||
when (draggingItem) {
|
||||
is DraggableItem.GroupHeader -> draggableGroupsOperations.expandGroups(items)
|
||||
is DraggableItem.Token -> items.uniteItems()
|
||||
is DraggableItem.Placeholder -> items
|
||||
}
|
||||
|
||||
currentDraggingItem = null
|
||||
}
|
||||
|
||||
currentDraggingItem = null
|
||||
}
|
||||
|
||||
override fun onItemDragged(from: ItemPosition, to: ItemPosition) = updateListState {
|
||||
|
|
@ -137,7 +132,7 @@ internal class DragAndDropAdapter(
|
|||
return when {
|
||||
moveOverItemPosition.index == 0 -> true
|
||||
moveOverItemPosition.index == lastItemIndex -> true
|
||||
moveOverItem is DraggableItem.GroupPlaceholder -> true
|
||||
moveOverItem is DraggableItem.Placeholder -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
|
@ -147,23 +142,7 @@ internal class DragAndDropAdapter(
|
|||
return when (moveOverItem) {
|
||||
is DraggableItem.GroupHeader -> false // Token item can not be moved to group item
|
||||
is DraggableItem.Token -> item.groupId == moveOverItem.groupId // Token item can not be moved over its group
|
||||
is DraggableItem.GroupPlaceholder -> false
|
||||
is DraggableItem.Placeholder -> false
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST") // Erased type
|
||||
private fun divideTokens(
|
||||
items: List<DraggableItem.Token>,
|
||||
movingItem: DraggableItem.Token,
|
||||
): List<DraggableItem.Token> {
|
||||
return items.map { token ->
|
||||
token
|
||||
.updateRoundingMode(DraggableItem.RoundingMode.All(showGap = true))
|
||||
.updateShadowVisibility(show = token.id == movingItem.id)
|
||||
} as List<DraggableItem.Token>
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val FINISH_DRAGGING_DELAY_MILLIS = 200L
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.feature.wallet.presentation.organizetokens.utils.dnd
|
||||
|
||||
import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem
|
||||
import com.tangem.feature.wallet.presentation.organizetokens.utils.common.divideMovingItem
|
||||
import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getGroupPlaceholder
|
||||
import com.tangem.feature.wallet.presentation.organizetokens.utils.common.uniteItems
|
||||
|
||||
|
|
@ -20,7 +21,7 @@ internal class DraggableGroupsOperations {
|
|||
it is DraggableItem.Token && it.groupId == movingGroup.id
|
||||
}
|
||||
|
||||
return divideGroups(itemsWithoutGroupTokens, movingGroup)
|
||||
return itemsWithoutGroupTokens.divideMovingItem(movingGroup)
|
||||
}
|
||||
|
||||
fun expandGroups(items: List<DraggableItem>): List<DraggableItem> {
|
||||
|
|
@ -45,62 +46,4 @@ internal class DraggableGroupsOperations {
|
|||
|
||||
return expandedGroups
|
||||
}
|
||||
|
||||
fun divideGroups(items: List<DraggableItem>, movingItem: DraggableItem): List<DraggableItem> {
|
||||
val lastItemIndex = items.lastIndex
|
||||
|
||||
return items.mapIndexed { index, item ->
|
||||
when {
|
||||
// Case when current item is the moving item
|
||||
item.id == movingItem.id -> {
|
||||
item
|
||||
.updateRoundingMode(DraggableItem.RoundingMode.All(showGap = true))
|
||||
.updateShadowVisibility(show = true)
|
||||
}
|
||||
// Case when moving item is a token and current item is the group of the moving token
|
||||
movingItem is DraggableItem.Token && item.id == movingItem.groupId -> {
|
||||
item
|
||||
.updateRoundingMode(DraggableItem.RoundingMode.All(showGap = true))
|
||||
.updateShadowVisibility(show = true)
|
||||
}
|
||||
// Case when both moving item and current item are tokens and belong to the same group
|
||||
movingItem is DraggableItem.Token &&
|
||||
item is DraggableItem.Token && item.groupId == movingItem.groupId -> {
|
||||
item
|
||||
.updateRoundingMode(DraggableItem.RoundingMode.All(showGap = true))
|
||||
.updateShadowVisibility(show = false)
|
||||
}
|
||||
// Case when current item is the first item in the list
|
||||
index == 0 -> {
|
||||
item
|
||||
.updateRoundingMode(DraggableItem.RoundingMode.Top())
|
||||
.updateShadowVisibility(show = false)
|
||||
}
|
||||
// Case when current item is the last item in the list
|
||||
index == lastItemIndex -> {
|
||||
item
|
||||
.updateRoundingMode(DraggableItem.RoundingMode.Bottom())
|
||||
.updateShadowVisibility(show = false)
|
||||
}
|
||||
// Case when previous item is a GroupPlaceholder
|
||||
items[index - 1] is DraggableItem.GroupPlaceholder -> {
|
||||
item
|
||||
.updateRoundingMode(DraggableItem.RoundingMode.Top(showGap = true))
|
||||
.updateShadowVisibility(show = false)
|
||||
}
|
||||
// Case when next item is a GroupPlaceholder
|
||||
items[index + 1] is DraggableItem.GroupPlaceholder -> {
|
||||
item
|
||||
.updateRoundingMode(DraggableItem.RoundingMode.Bottom(showGap = true))
|
||||
.updateShadowVisibility(show = false)
|
||||
}
|
||||
// Default case when none of the above conditions are met
|
||||
else -> {
|
||||
item
|
||||
.updateRoundingMode(DraggableItem.RoundingMode.None)
|
||||
.updateShadowVisibility(show = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import com.tangem.feature.wallet.impl.R
|
|||
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import javax.annotation.concurrent.Immutable
|
||||
|
||||
/**
|
||||
* Wallet tokens list state
|
||||
|
|
@ -20,11 +21,10 @@ internal sealed class WalletTokensListState {
|
|||
* Wallet content token list state
|
||||
*
|
||||
* @property items content items
|
||||
* @property onOrganizeTokensClick lambda be invoked when organize tokens button is clicked
|
||||
*/
|
||||
sealed class ContentState(
|
||||
open val items: ImmutableList<TokensListItemState>,
|
||||
open val onOrganizeTokensClick: (() -> Unit)?,
|
||||
open val organizeTokensButton: OrganizeTokensButtonState,
|
||||
) : WalletTokensListState()
|
||||
|
||||
/**
|
||||
|
|
@ -37,18 +37,18 @@ internal sealed class WalletTokensListState {
|
|||
TokensListItemState.Token(state = TokenItemState.Loading(id = FIRST_LOADING_TOKEN_ID)),
|
||||
TokensListItemState.Token(state = TokenItemState.Loading(id = SECOND_LOADING_TOKEN_ID)),
|
||||
),
|
||||
) : ContentState(items = items, onOrganizeTokensClick = null)
|
||||
) : ContentState(items = items, organizeTokensButton = OrganizeTokensButtonState.Hidden)
|
||||
|
||||
/**
|
||||
* Content state
|
||||
*
|
||||
* @property items content items
|
||||
* @property onOrganizeTokensClick lambda be invoked when organize tokens button is clicked
|
||||
* @property organizeTokensButton represents the state of the 'Organize Tokens' button
|
||||
*/
|
||||
data class Content(
|
||||
override val items: ImmutableList<TokensListItemState>,
|
||||
override val onOrganizeTokensClick: (() -> Unit)?,
|
||||
) : ContentState(items, onOrganizeTokensClick)
|
||||
override val organizeTokensButton: OrganizeTokensButtonState,
|
||||
) : ContentState(items, organizeTokensButton)
|
||||
|
||||
/** Locked content state */
|
||||
object Locked : ContentState(
|
||||
|
|
@ -56,10 +56,32 @@ internal sealed class WalletTokensListState {
|
|||
TokensListItemState.NetworkGroupTitle(value = TextReference.Res(id = R.string.main_tokens)),
|
||||
TokensListItemState.Token(state = TokenItemState.Locked(id = LOCKED_TOKEN_ID)),
|
||||
),
|
||||
onOrganizeTokensClick = null,
|
||||
organizeTokensButton = OrganizeTokensButtonState.Hidden,
|
||||
)
|
||||
|
||||
/**
|
||||
* Represents the state of the 'Organize Tokens' button.
|
||||
*/
|
||||
@Immutable
|
||||
sealed class OrganizeTokensButtonState {
|
||||
|
||||
/** Represents the state where the 'Organize Tokens' button is hidden. */
|
||||
object Hidden : OrganizeTokensButtonState()
|
||||
|
||||
/**
|
||||
* Represents the state where the 'Organize Tokens' button is visible.
|
||||
*
|
||||
* @property isEnabled Indicates if the button is enabled or not.
|
||||
* @property onClick Callback to be executed when the button is clicked.
|
||||
*/
|
||||
data class Visible(
|
||||
val isEnabled: Boolean,
|
||||
val onClick: () -> Unit,
|
||||
) : OrganizeTokensButtonState()
|
||||
}
|
||||
|
||||
/** Tokens list item state */
|
||||
@Immutable
|
||||
sealed interface TokensListItemState {
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -7,14 +7,12 @@ import com.tangem.feature.wallet.presentation.wallet.state.WalletState
|
|||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletManageButton
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletPullToRefreshConfig
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState
|
||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
import kotlinx.collections.immutable.mutate
|
||||
|
||||
internal class WalletRefreshStateConverter(
|
||||
private val currentStateProvider: Provider<WalletState>,
|
||||
private val intents: WalletClickIntents,
|
||||
) : Converter<Boolean, WalletState> {
|
||||
|
||||
override fun convert(value: Boolean): WalletState {
|
||||
|
|
@ -77,16 +75,21 @@ internal class WalletRefreshStateConverter(
|
|||
}
|
||||
|
||||
private fun WalletMultiCurrencyState.updateTokenListState(isRefreshing: Boolean): WalletTokensListState {
|
||||
return when (val state = tokensListState) {
|
||||
return when (val listState = tokensListState) {
|
||||
is WalletTokensListState.Content -> {
|
||||
val onOrganizeTokensClick = if (isRefreshing) null else intents::onOrganizeTokensClick
|
||||
|
||||
state.copy(onOrganizeTokensClick = onOrganizeTokensClick)
|
||||
when (listState.organizeTokensButton) {
|
||||
is WalletTokensListState.OrganizeTokensButtonState.Hidden -> listState
|
||||
is WalletTokensListState.OrganizeTokensButtonState.Visible -> listState.copy(
|
||||
organizeTokensButton = listState.organizeTokensButton.copy(
|
||||
isEnabled = !isRefreshing,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
is WalletTokensListState.Locked,
|
||||
is WalletTokensListState.Loading,
|
||||
is WalletTokensListState.Empty,
|
||||
-> state
|
||||
-> listState
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -105,10 +105,7 @@ internal class WalletStateFactory(
|
|||
}
|
||||
|
||||
private val refreshStateConverter by lazy {
|
||||
WalletRefreshStateConverter(
|
||||
currentStateProvider = currentStateProvider,
|
||||
intents = clickIntents,
|
||||
)
|
||||
WalletRefreshStateConverter(currentStateProvider)
|
||||
}
|
||||
|
||||
private val cryptoCurrencyActionsConverter by lazy {
|
||||
|
|
|
|||
|
|
@ -26,10 +26,11 @@ import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencySt
|
|||
import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState.OrganizeTokensButtonState
|
||||
import com.tangem.feature.wallet.presentation.wallet.ui.components.TokenActionsBottomSheet
|
||||
import com.tangem.feature.wallet.presentation.wallet.ui.components.WalletsList
|
||||
import com.tangem.feature.wallet.presentation.wallet.ui.components.common.*
|
||||
import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency.organizeButton
|
||||
import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency.organizeTokensButton
|
||||
import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.controlButtons
|
||||
import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.marketPriceBlock
|
||||
import com.tangem.feature.wallet.presentation.wallet.ui.utils.changeWalletAnimator
|
||||
|
|
@ -110,9 +111,15 @@ private fun WalletContent(state: WalletState.ContentState) {
|
|||
contentItems(state = state, txHistoryItems = txHistoryItems, modifier = movableItemModifier)
|
||||
|
||||
if (state is WalletMultiCurrencyState) {
|
||||
val tokensListState = state.tokensListState
|
||||
if (tokensListState is WalletTokensListState.ContentState) {
|
||||
organizeButton(onClick = tokensListState.onOrganizeTokensClick, modifier = itemModifier)
|
||||
val contentTokenListState = state.tokensListState as? WalletTokensListState.ContentState
|
||||
val organizeTokensButton = contentTokenListState?.organizeTokensButton
|
||||
|
||||
if (organizeTokensButton is OrganizeTokensButtonState.Visible) {
|
||||
organizeTokensButton(
|
||||
modifier = itemModifier,
|
||||
isEnabled = organizeTokensButton.isEnabled,
|
||||
onClick = organizeTokensButton.onClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig
|
||||
import com.tangem.core.ui.components.buttons.actions.RoundedActionButton
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.feature.wallet.impl.R
|
||||
|
||||
private const val ORGANIZE_BUTTON_CONTENT_TYPE = "OrganizeTokensButton"
|
||||
|
||||
|
|
@ -14,9 +17,20 @@ private const val ORGANIZE_BUTTON_CONTENT_TYPE = "OrganizeTokensButton"
|
|||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
internal fun LazyListScope.organizeButton(onClick: (() -> Unit)?, modifier: Modifier = Modifier) {
|
||||
internal fun LazyListScope.organizeTokensButton(
|
||||
isEnabled: Boolean,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
item(key = ORGANIZE_BUTTON_CONTENT_TYPE, contentType = ORGANIZE_BUTTON_CONTENT_TYPE) {
|
||||
OrganizeTokensButton(onClick = onClick, modifier = modifier.animateItemPlacement())
|
||||
RoundedActionButton(
|
||||
modifier = modifier,
|
||||
config = ActionButtonConfig(
|
||||
text = resourceReference(id = R.string.organize_tokens_title),
|
||||
iconResId = R.drawable.ic_filter_24,
|
||||
onClick = onClick,
|
||||
enabled = isEnabled,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig
|
||||
import com.tangem.core.ui.components.buttons.actions.RoundedActionButton
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.feature.wallet.impl.R
|
||||
|
||||
/**
|
||||
* Organize tokens button
|
||||
*
|
||||
* @param onClick callback, if null button is disabled
|
||||
* @param modifier modifier
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
internal fun OrganizeTokensButton(onClick: (() -> Unit)?, modifier: Modifier = Modifier) {
|
||||
RoundedActionButton(
|
||||
config = ActionButtonConfig(
|
||||
text = TextReference.Res(id = R.string.organize_tokens_title),
|
||||
iconResId = R.drawable.ic_filter_24,
|
||||
onClick = onClick ?: {},
|
||||
enabled = onClick != null,
|
||||
),
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
|
@ -38,7 +38,7 @@ internal class CryptoCurrencyStatusToTokenItemConverter(
|
|||
return TokenItemState.Content(
|
||||
id = currency.id.value,
|
||||
name = currency.name,
|
||||
icon = iconStateConverter.convert(currency),
|
||||
icon = iconStateConverter.convert(value = this),
|
||||
amount = getFormattedAmount(),
|
||||
hasPending = value.hasCurrentNetworkTransactions,
|
||||
tokenOptions = if (isWalletContentHidden) {
|
||||
|
|
@ -70,7 +70,7 @@ internal class CryptoCurrencyStatusToTokenItemConverter(
|
|||
private fun CryptoCurrencyStatus.mapToUnreachableTokenItemState() = TokenItemState.Unreachable(
|
||||
id = currency.id.value,
|
||||
name = currency.name,
|
||||
icon = iconStateConverter.convert(currency),
|
||||
icon = iconStateConverter.convert(value = this),
|
||||
)
|
||||
|
||||
private fun CryptoCurrencyStatus.getPriceChangeConfig(): PriceChangeConfig {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ internal class TokenListErrorConverter(
|
|||
state.copy(
|
||||
tokensListState = WalletTokensListState.Content(
|
||||
items = persistentListOf(),
|
||||
onOrganizeTokensClick = null,
|
||||
organizeTokensButton = WalletTokensListState.OrganizeTokensButtonState.Hidden,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
|||
import com.tangem.domain.tokens.model.NetworkGroup
|
||||
import com.tangem.domain.tokens.model.TokenList
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState.OrganizeTokensButtonState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState.TokensListItemState
|
||||
import com.tangem.feature.wallet.presentation.wallet.utils.LoadingItemsProvider.getLoadingMultiCurrencyTokens
|
||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
|
|
@ -28,26 +28,15 @@ internal class TokenListToContentItemsConverter(
|
|||
)
|
||||
|
||||
override fun convert(value: TokenList): WalletTokensListState {
|
||||
val isEmptyList = when (value) {
|
||||
is TokenList.GroupedByNetwork -> value.groups.isEmpty()
|
||||
is TokenList.NotInitialized -> false
|
||||
is TokenList.Ungrouped -> value.currencies.isEmpty()
|
||||
}
|
||||
|
||||
return if (isEmptyList) {
|
||||
WalletTokensListState.Empty
|
||||
} else {
|
||||
WalletTokensListState.Content(
|
||||
items = when (value) {
|
||||
is TokenList.GroupedByNetwork -> value.mapToMultiCurrencyItems()
|
||||
is TokenList.Ungrouped -> value.mapToMultiCurrencyItems()
|
||||
is TokenList.NotInitialized -> getLoadingMultiCurrencyTokens()
|
||||
},
|
||||
onOrganizeTokensClick = if (value.totalFiatBalance is TokenList.FiatBalance.Loaded) {
|
||||
clickIntents::onOrganizeTokensClick
|
||||
} else {
|
||||
null
|
||||
},
|
||||
return when (value) {
|
||||
is TokenList.NotInitialized -> WalletTokensListState.Loading()
|
||||
is TokenList.GroupedByNetwork -> WalletTokensListState.Content(
|
||||
items = value.mapToMultiCurrencyItems(),
|
||||
organizeTokensButton = value.mapToOrganizeTokensButtonState(),
|
||||
)
|
||||
is TokenList.Ungrouped -> WalletTokensListState.Content(
|
||||
items = value.mapToMultiCurrencyItems(),
|
||||
organizeTokensButton = value.mapToOrganizeTokensButtonState(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -64,6 +53,20 @@ internal class TokenListToContentItemsConverter(
|
|||
}
|
||||
}
|
||||
|
||||
private fun TokenList.GroupedByNetwork.mapToOrganizeTokensButtonState(): OrganizeTokensButtonState {
|
||||
return getOrganizeTokensButtonState(
|
||||
isLoading = totalFiatBalance is TokenList.FiatBalance.Loading,
|
||||
currenciesSize = groups.flatMap(NetworkGroup::currencies).size,
|
||||
)
|
||||
}
|
||||
|
||||
private fun TokenList.Ungrouped.mapToOrganizeTokensButtonState(): OrganizeTokensButtonState {
|
||||
return getOrganizeTokensButtonState(
|
||||
isLoading = totalFiatBalance is TokenList.FiatBalance.Loading,
|
||||
currenciesSize = currencies.size,
|
||||
)
|
||||
}
|
||||
|
||||
private fun MutableList<TokensListItemState>.addGroup(group: NetworkGroup): List<TokensListItemState> {
|
||||
this.add(TokensListItemState.NetworkGroupTitle(TextReference.Str(group.network.name)))
|
||||
|
||||
|
|
@ -81,4 +84,15 @@ internal class TokenListToContentItemsConverter(
|
|||
|
||||
return this
|
||||
}
|
||||
|
||||
private fun getOrganizeTokensButtonState(isLoading: Boolean, currenciesSize: Int): OrganizeTokensButtonState {
|
||||
return if (currenciesSize > 1) {
|
||||
OrganizeTokensButtonState.Visible(
|
||||
isEnabled = !isLoading,
|
||||
onClick = clickIntents::onOrganizeTokensClick,
|
||||
)
|
||||
} else {
|
||||
OrganizeTokensButtonState.Hidden
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue