Updated on 2026-08-14
This commit is contained in:
parent
c7b11501e7
commit
f089d31106
14 changed files with 475 additions and 209 deletions
|
|
@ -0,0 +1,23 @@
|
|||
package com.tangem.core.ui.event
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.NonRestartableComposable
|
||||
|
||||
/**
|
||||
* A Composable function that reacts to a given [StateEvent], executing the provided action only once when the event
|
||||
* is triggered.
|
||||
*
|
||||
* @param event The [StateEvent] to listen to.
|
||||
* @param onTrigger The action to execute when the event is triggered.
|
||||
*/
|
||||
@Composable
|
||||
@NonRestartableComposable
|
||||
fun EventEffect(event: StateEvent, onTrigger: suspend () -> Unit) {
|
||||
LaunchedEffect(event) {
|
||||
if (event is StateEvent.Triggered) {
|
||||
onTrigger()
|
||||
event.consume()
|
||||
}
|
||||
}
|
||||
}
|
||||
51
core/ui/src/main/java/com/tangem/core/ui/event/StateEvent.kt
Normal file
51
core/ui/src/main/java/com/tangem/core/ui/event/StateEvent.kt
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
package com.tangem.core.ui.event
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
/**
|
||||
* Represents compose state event, which can be consumed or triggered.
|
||||
*
|
||||
* This is especially useful for handling one-off UI events like showing snack bars or navigation which should not be
|
||||
* re-triggered on recompositions or state changes.
|
||||
*/
|
||||
@Immutable
|
||||
sealed class StateEvent {
|
||||
|
||||
/** Defines the action to be executed when the event is consumed. */
|
||||
protected abstract val onConsume: () -> Unit
|
||||
|
||||
/**
|
||||
* Represents an already consumed state event.
|
||||
* Events of this type will not trigger any further actions.
|
||||
*/
|
||||
object Consumed : StateEvent() {
|
||||
override val onConsume: () -> Unit = {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a state event that has been triggered but not yet consumed.
|
||||
*
|
||||
* @property onConsume The action to be executed when the event is consumed.
|
||||
*/
|
||||
data class Triggered(override val onConsume: () -> Unit) : StateEvent()
|
||||
|
||||
/**
|
||||
* Consumes the event, triggering any associated action.
|
||||
*/
|
||||
fun consume() {
|
||||
onConsume()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a [StateEvent.Triggered] instance.
|
||||
*
|
||||
* @param onConsume The action to be executed when the event is consumed.
|
||||
* @return A triggered state event.
|
||||
*/
|
||||
fun triggered(onConsume: () -> Unit): StateEvent.Triggered = StateEvent.Triggered(onConsume)
|
||||
|
||||
/**
|
||||
* Represents a statically defined [StateEvent.Consumed] event.
|
||||
*/
|
||||
val consumed: StateEvent.Consumed = StateEvent.Consumed
|
||||
|
|
@ -6,6 +6,7 @@ import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
|
|||
import com.tangem.core.ui.components.marketprice.PriceChangeConfig
|
||||
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.domain.wallets.models.UserWalletId
|
||||
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
|
||||
|
|
@ -145,7 +146,7 @@ internal object WalletPreviewData {
|
|||
|
||||
private const val networksSize = 10
|
||||
private const val tokensSize = 3
|
||||
val draggableItems by lazy {
|
||||
private val draggableItems by lazy {
|
||||
List(networksSize) { it }
|
||||
.flatMap { index ->
|
||||
val lastNetworkIndex = networksSize - 1
|
||||
|
|
@ -194,7 +195,7 @@ internal object WalletPreviewData {
|
|||
.toPersistentList()
|
||||
}
|
||||
|
||||
val draggableTokens by lazy {
|
||||
private val draggableTokens by lazy {
|
||||
draggableItems
|
||||
.filterIsInstance<DraggableItem.Token>()
|
||||
.toMutableList()
|
||||
|
|
@ -224,6 +225,7 @@ internal object WalletPreviewData {
|
|||
onApplyClick = {},
|
||||
onCancelClick = {},
|
||||
),
|
||||
scrollListToTop = consumed,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
package com.tangem.feature.wallet.presentation.organizetokens
|
||||
|
||||
import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem
|
||||
import org.burnoutcrew.reorderable.ItemPosition
|
||||
|
||||
internal interface OrganizeTokensIntents {
|
||||
|
||||
fun onBackClick()
|
||||
|
||||
fun onSortClick()
|
||||
|
||||
fun onGroupClick()
|
||||
|
||||
fun onApplyClick()
|
||||
|
||||
fun onCancelClick()
|
||||
}
|
||||
|
||||
internal interface DragAndDropIntents {
|
||||
|
||||
fun onItemDragged(from: ItemPosition, to: ItemPosition)
|
||||
|
||||
fun canDragItemOver(dragOver: ItemPosition, dragging: ItemPosition): Boolean
|
||||
|
||||
fun onItemDraggingStart(item: DraggableItem)
|
||||
|
||||
fun onItemDraggingEnd()
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
package com.tangem.feature.wallet.presentation.organizetokens
|
||||
|
||||
internal interface OrganizeTokensIntents {
|
||||
|
||||
fun onBackClick()
|
||||
|
||||
fun onSortClick()
|
||||
|
||||
fun onGroupClick()
|
||||
|
||||
fun onApplyClick()
|
||||
|
||||
fun onCancelClick()
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.organizetokens
|
|||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.*
|
||||
|
|
@ -26,6 +27,7 @@ import com.tangem.core.ui.components.PrimaryButton
|
|||
import com.tangem.core.ui.components.SecondaryButton
|
||||
import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig
|
||||
import com.tangem.core.ui.components.buttons.actions.RoundedActionButton
|
||||
import com.tangem.core.ui.event.EventEffect
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.feature.wallet.impl.R
|
||||
|
|
@ -64,6 +66,10 @@ internal fun OrganizeTokensScreen(state: OrganizeTokensState, modifier: Modifier
|
|||
},
|
||||
containerColor = TangemTheme.colors.background.secondary,
|
||||
)
|
||||
|
||||
EventEffect(state.scrollListToTop) {
|
||||
tokensListState.animateScrollToItem(index = 0)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
@ -74,11 +80,16 @@ private fun TokenList(
|
|||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(modifier = modifier) {
|
||||
val onDragEnd: (Int, Int) -> Unit = remember {
|
||||
{ _, _ ->
|
||||
dndConfig.onItemDragEnd()
|
||||
}
|
||||
}
|
||||
val reorderableListState = rememberReorderableLazyListState(
|
||||
onMove = dndConfig.onItemDragged,
|
||||
listState = listState,
|
||||
canDragOver = dndConfig.canDragItemOver,
|
||||
onDragEnd = { _, _ -> dndConfig.onItemDragEnd() },
|
||||
onDragEnd = onDragEnd,
|
||||
)
|
||||
val items = state.items
|
||||
|
||||
|
|
@ -109,11 +120,6 @@ private fun TokenList(
|
|||
reorderableState = reorderableListState,
|
||||
onDragStart = onDragStart,
|
||||
)
|
||||
|
||||
if (item is DraggableItem.GroupPlaceholder) {
|
||||
// This item should be displayed in the list but remain invisible
|
||||
Box(modifier = Modifier.fillMaxWidth())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -121,6 +127,7 @@ private fun TokenList(
|
|||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
private fun LazyItemScope.DraggableItem(
|
||||
index: Int,
|
||||
|
|
@ -129,15 +136,13 @@ private fun LazyItemScope.DraggableItem(
|
|||
onDragStart: () -> Unit,
|
||||
) {
|
||||
ReorderableItem(
|
||||
reorderableState = reorderableState,
|
||||
defaultDraggingModifier = Modifier.animateItemPlacement(
|
||||
animationSpec = tween(easing = LinearOutSlowInEasing),
|
||||
),
|
||||
state = reorderableState,
|
||||
index = index,
|
||||
key = item.id,
|
||||
) { isDragging ->
|
||||
|
||||
if (isDragging) {
|
||||
onDragStart()
|
||||
}
|
||||
|
||||
val itemModifier = Modifier.applyShapeAndShadow(item.roundingMode, item.showShadow)
|
||||
|
||||
when (item) {
|
||||
|
|
@ -151,7 +156,14 @@ private fun LazyItemScope.DraggableItem(
|
|||
state = item.tokenItemState,
|
||||
reorderableTokenListState = reorderableState,
|
||||
)
|
||||
is DraggableItem.GroupPlaceholder -> Unit
|
||||
// Should be presented in the list but remain invisible
|
||||
is DraggableItem.GroupPlaceholder -> Box(modifier = Modifier.fillMaxWidth())
|
||||
}
|
||||
|
||||
LaunchedEffect(isDragging) {
|
||||
if (isDragging) {
|
||||
onDragStart()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package com.tangem.feature.wallet.presentation.organizetokens
|
||||
|
||||
import com.tangem.common.Provider
|
||||
import com.tangem.core.ui.event.consumed
|
||||
import com.tangem.core.ui.event.triggered
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.error.TokenListError
|
||||
import com.tangem.domain.tokens.error.TokenListSortingError
|
||||
|
|
@ -19,6 +21,7 @@ import kotlinx.coroutines.flow.*
|
|||
|
||||
internal class OrganizeTokensStateHolder(
|
||||
private val intents: OrganizeTokensIntents,
|
||||
private val dragAndDropIntents: DragAndDropIntents,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val onSubscription: () -> Unit,
|
||||
stateFlowScope: CoroutineScope,
|
||||
|
|
@ -60,6 +63,14 @@ internal class OrganizeTokensStateHolder(
|
|||
updateState { tokenListConverter.convert(tokenList) }
|
||||
}
|
||||
|
||||
fun updateStateAfterTokenListSorting(tokenList: TokenList) {
|
||||
updateState {
|
||||
tokenListConverter.convert(tokenList).copy(
|
||||
scrollListToTop = triggered(::consumeScrollListToTopEvent),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateStateToDisplayProgress() {
|
||||
updateState { inProgressStateConverter.convert(value = this) }
|
||||
}
|
||||
|
|
@ -68,6 +79,15 @@ internal class OrganizeTokensStateHolder(
|
|||
updateState { inProgressStateConverter.convertBack(value = this) }
|
||||
}
|
||||
|
||||
fun updateStateWithManualSorting(itemsState: OrganizeTokensListState) {
|
||||
updateState {
|
||||
copy(
|
||||
header = header.copy(isSortedByBalance = false),
|
||||
itemsState = itemsState,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateStateWithError(error: TokenListError) {
|
||||
updateState { tokenListErrorConverter.convert(error) }
|
||||
}
|
||||
|
|
@ -88,17 +108,21 @@ internal class OrganizeTokensStateHolder(
|
|||
onApplyClick = intents::onApplyClick,
|
||||
onCancelClick = intents::onCancelClick,
|
||||
),
|
||||
// TODO: Will be added in next MR
|
||||
dndConfig = OrganizeTokensState.DragAndDropConfig(
|
||||
onItemDragged = { _, _ -> },
|
||||
onDragStart = { },
|
||||
onItemDragEnd = { },
|
||||
canDragItemOver = { _, _ -> false },
|
||||
onItemDragged = dragAndDropIntents::onItemDragged,
|
||||
onDragStart = dragAndDropIntents::onItemDraggingStart,
|
||||
onItemDragEnd = dragAndDropIntents::onItemDraggingEnd,
|
||||
canDragItemOver = dragAndDropIntents::canDragItemOver,
|
||||
),
|
||||
scrollListToTop = consumed,
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateState(block: OrganizeTokensState.() -> OrganizeTokensState) {
|
||||
stateFlowInternal.update(block)
|
||||
}
|
||||
|
||||
private fun consumeScrollListToTopEvent() {
|
||||
updateState { copy(scrollListToTop = consumed) }
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,8 @@ import com.tangem.domain.wallets.models.UserWalletId
|
|||
import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState
|
||||
import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState
|
||||
import com.tangem.feature.wallet.presentation.organizetokens.utils.CryptoCurrenciesIdsResolver
|
||||
import com.tangem.feature.wallet.presentation.organizetokens.utils.common.disableSortingByBalance
|
||||
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 dagger.hilt.android.lifecycle.HiltViewModel
|
||||
|
|
@ -39,12 +41,19 @@ internal class OrganizeTokensViewModel @Inject constructor(
|
|||
|
||||
private val selectedAppCurrencyFlow = createSelectedAppCurrencyFlow()
|
||||
|
||||
private val dragAndDropAdapter = DragAndDropAdapter(
|
||||
listStateProvider = Provider { uiState.value.itemsState },
|
||||
scope = viewModelScope,
|
||||
)
|
||||
|
||||
private val stateHolder = OrganizeTokensStateHolder(
|
||||
stateFlowScope = viewModelScope,
|
||||
intents = this,
|
||||
dragAndDropIntents = dragAndDropAdapter,
|
||||
appCurrencyProvider = Provider(selectedAppCurrencyFlow::value),
|
||||
onSubscription = {
|
||||
bootstrapTokenList()
|
||||
bootstrapDragAndDropUpdates()
|
||||
},
|
||||
)
|
||||
|
||||
|
|
@ -69,7 +78,7 @@ internal class OrganizeTokensViewModel @Inject constructor(
|
|||
toggleTokenListSortingUseCase(list).fold(
|
||||
ifLeft = stateHolder::updateStateWithError,
|
||||
ifRight = {
|
||||
stateHolder.updateStateWithTokenList(it)
|
||||
stateHolder.updateStateAfterTokenListSorting(it)
|
||||
tokenList = it
|
||||
},
|
||||
)
|
||||
|
|
@ -83,7 +92,7 @@ internal class OrganizeTokensViewModel @Inject constructor(
|
|||
toggleTokenListGroupingUseCase(list).fold(
|
||||
ifLeft = stateHolder::updateStateWithError,
|
||||
ifRight = {
|
||||
stateHolder.updateStateWithTokenList(it)
|
||||
stateHolder.updateStateAfterTokenListSorting(it)
|
||||
tokenList = it
|
||||
},
|
||||
)
|
||||
|
|
@ -133,6 +142,16 @@ internal class OrganizeTokensViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun bootstrapDragAndDropUpdates() {
|
||||
dragAndDropAdapter.stateFlow
|
||||
.distinctUntilChanged()
|
||||
.onEach {
|
||||
stateHolder.updateStateWithManualSorting(it)
|
||||
tokenList = tokenList?.disableSortingByBalance()
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
private fun createSelectedAppCurrencyFlow(): StateFlow<AppCurrency> {
|
||||
return getSelectedAppCurrencyUseCase()
|
||||
.map { maybeAppCurrency ->
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.feature.wallet.presentation.organizetokens.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.event.StateEvent
|
||||
import org.burnoutcrew.reorderable.ItemPosition
|
||||
|
||||
@Immutable
|
||||
|
|
@ -10,6 +11,7 @@ internal data class OrganizeTokensState(
|
|||
val header: HeaderConfig,
|
||||
val actions: ActionsConfig,
|
||||
val dndConfig: DragAndDropConfig,
|
||||
val scrollListToTop: StateEvent,
|
||||
) {
|
||||
|
||||
data class HeaderConfig(
|
||||
|
|
|
|||
|
|
@ -1,184 +1,27 @@
|
|||
package com.tangem.feature.wallet.presentation.organizetokens.utils.common
|
||||
|
||||
import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
import org.burnoutcrew.reorderable.ItemPosition
|
||||
|
||||
internal fun List<DraggableItem>.findItemsToMove(
|
||||
moveOverItemKey: Any?,
|
||||
movedItemKey: Any?,
|
||||
): Pair<DraggableItem?, DraggableItem?> {
|
||||
var moveOverItem: DraggableItem? = null
|
||||
var movedItem: DraggableItem? = null
|
||||
|
||||
for (item in this) {
|
||||
if (item.id == moveOverItemKey) {
|
||||
moveOverItem = item
|
||||
}
|
||||
if (item.id == movedItemKey) {
|
||||
movedItem = item
|
||||
}
|
||||
if (moveOverItem != null && movedItem != null) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return Pair(moveOverItem, movedItem)
|
||||
}
|
||||
|
||||
internal fun checkCanMoveHeaderOver(
|
||||
moveOverItemPosition: ItemPosition,
|
||||
moveOverItem: DraggableItem,
|
||||
lastItemIndex: Int,
|
||||
): Boolean {
|
||||
// Group item can be moved only to group divider or to ages of the items list
|
||||
return when {
|
||||
moveOverItemPosition.index == 0 -> true
|
||||
moveOverItemPosition.index == lastItemIndex -> true
|
||||
moveOverItem is DraggableItem.GroupPlaceholder -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
internal fun checkCanMoveTokenOver(item: DraggableItem.Token, moveOverItem: DraggableItem): Boolean {
|
||||
// Token item can be moved only in its group
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
internal fun PersistentList<DraggableItem>.moveItem(fromIndex: Int, toIndex: Int): PersistentList<DraggableItem> {
|
||||
val fromItem = this[fromIndex]
|
||||
return this
|
||||
.removeAt(fromIndex)
|
||||
.add(toIndex, fromItem)
|
||||
}
|
||||
|
||||
internal fun List<DraggableItem>.divideItems(movingItem: DraggableItem): List<DraggableItem> {
|
||||
return this.map {
|
||||
it
|
||||
.updateRoundingMode(DraggableItem.RoundingMode.All(showGap = true))
|
||||
.updateShadowVisibility(show = it.id == movingItem.id)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST") // Erased type
|
||||
internal fun <T : DraggableItem> List<T>.uniteItems(): List<T> {
|
||||
internal fun List<DraggableItem>.uniteItems(): List<DraggableItem> {
|
||||
val lastItemIndex = this.lastIndex
|
||||
|
||||
return this.mapIndexed { index, item ->
|
||||
val mode = when (index) {
|
||||
0 -> DraggableItem.RoundingMode.Top()
|
||||
lastItemIndex -> DraggableItem.RoundingMode.Bottom()
|
||||
else -> DraggableItem.RoundingMode.None
|
||||
else -> when (item) {
|
||||
is DraggableItem.GroupHeader -> DraggableItem.RoundingMode.Top(showGap = true)
|
||||
is DraggableItem.Token -> if (this[index + 1] is DraggableItem.GroupPlaceholder) {
|
||||
DraggableItem.RoundingMode.Bottom(showGap = true)
|
||||
} else {
|
||||
DraggableItem.RoundingMode.None
|
||||
}
|
||||
is DraggableItem.GroupPlaceholder -> DraggableItem.RoundingMode.None
|
||||
}
|
||||
}
|
||||
|
||||
item
|
||||
.updateRoundingMode(mode)
|
||||
.updateShadowVisibility(show = false)
|
||||
} as List<T>
|
||||
}
|
||||
|
||||
// TODO: Move to domain
|
||||
@Volatile
|
||||
private var groupIdToTokens: Map<String, List<DraggableItem.Token>>? = null
|
||||
|
||||
internal fun List<DraggableItem>.collapseGroup(group: DraggableItem.GroupHeader): List<DraggableItem> {
|
||||
if (!groupIdToTokens.isNullOrEmpty()) return this
|
||||
|
||||
groupIdToTokens = this
|
||||
.asSequence()
|
||||
.filterIsInstance<DraggableItem.Token>()
|
||||
.groupBy { it.groupId }
|
||||
|
||||
return this
|
||||
.filterNot { it is DraggableItem.Token && it.groupId == group.id }
|
||||
.divideGroups(group)
|
||||
}
|
||||
|
||||
internal fun List<DraggableItem>.expandGroups(): List<DraggableItem> {
|
||||
if (groupIdToTokens.isNullOrEmpty()) return this
|
||||
|
||||
val currentGroups = this.filterIsInstance<DraggableItem.GroupHeader>()
|
||||
val lastGroupIndex = currentGroups.lastIndex
|
||||
|
||||
return currentGroups
|
||||
.flatMapIndexed { index, group ->
|
||||
buildList {
|
||||
add(group)
|
||||
addAll(groupIdToTokens?.get(group.id).orEmpty())
|
||||
if (index != lastGroupIndex) {
|
||||
add(DraggableItem.GroupPlaceholder(id = "group_divider_$index"))
|
||||
}
|
||||
}
|
||||
}
|
||||
.uniteItems()
|
||||
.also { groupIdToTokens = null }
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the correct [DraggableItem.RoundingMode] and shadow status to each item in the list,
|
||||
* based on the relationship of each item to the [movingItem] and its position in the list.
|
||||
*
|
||||
* @param movingItem The item that is being dragged/moved.
|
||||
* @return A list of [DraggableItem]s with updated rounding modes and shadow statuses.
|
||||
*/
|
||||
internal fun List<DraggableItem>.divideGroups(movingItem: DraggableItem): List<DraggableItem> {
|
||||
val lastItemIndex = this.lastIndex
|
||||
|
||||
return this.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
|
||||
this[index - 1] is DraggableItem.GroupPlaceholder -> {
|
||||
item
|
||||
.updateRoundingMode(DraggableItem.RoundingMode.Top(showGap = true))
|
||||
.updateShadowVisibility(show = false)
|
||||
}
|
||||
// Case when next item is a GroupPlaceholder
|
||||
this[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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,12 +3,10 @@ package com.tangem.feature.wallet.presentation.organizetokens.utils.common
|
|||
import com.tangem.domain.tokens.model.TokenList
|
||||
import com.tangem.domain.tokens.model.TokenList.SortType
|
||||
|
||||
internal fun TokenList.updateSorting(isSortedByBalance: Boolean): TokenList {
|
||||
val sortType = if (isSortedByBalance) SortType.BALANCE else SortType.NONE
|
||||
|
||||
internal fun TokenList.disableSortingByBalance(): TokenList {
|
||||
return when (this) {
|
||||
is TokenList.GroupedByNetwork -> this.copy(sortedBy = sortType)
|
||||
is TokenList.Ungrouped -> this.copy(sortedBy = sortType)
|
||||
is TokenList.GroupedByNetwork -> this.copy(sortedBy = SortType.NONE)
|
||||
is TokenList.Ungrouped -> this.copy(sortedBy = SortType.NONE)
|
||||
is TokenList.NotInitialized -> this
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,11 @@
|
|||
package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items
|
||||
|
||||
import com.tangem.domain.tokens.model.TokenList
|
||||
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.uniteItems
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
internal class TokenListToListStateConverter(
|
||||
|
|
@ -28,11 +30,12 @@ internal class TokenListToListStateConverter(
|
|||
)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST") // Erased type
|
||||
private fun createListState(tokenList: TokenList.Ungrouped): OrganizeTokensListState.Ungrouped {
|
||||
return OrganizeTokensListState.Ungrouped(
|
||||
items = tokensConverter.convertList(tokenList.currencies)
|
||||
.uniteItems()
|
||||
.toPersistentList(),
|
||||
.toPersistentList() as PersistentList<DraggableItem.Token>,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,169 @@
|
|||
package com.tangem.feature.wallet.presentation.organizetokens.utils.dnd
|
||||
|
||||
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.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()
|
||||
|
||||
private val currentListState: OrganizeTokensListState
|
||||
get() = listStateProvider.invoke()
|
||||
|
||||
private val listStateFlowInternal: MutableSharedFlow<OrganizeTokensListState> = MutableSharedFlow(
|
||||
replay = 1,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST,
|
||||
)
|
||||
|
||||
private var currentDraggingItem: DraggableItem? = null
|
||||
|
||||
val stateFlow: Flow<OrganizeTokensListState>
|
||||
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 (dragOverItem, draggingItem) = findItemsToMove(
|
||||
items = items,
|
||||
moveOverItemKey = dragOver.key,
|
||||
movedItemKey = dragging.key,
|
||||
)
|
||||
|
||||
if (dragOverItem == null || draggingItem == null) {
|
||||
return false
|
||||
}
|
||||
|
||||
return when (draggingItem) {
|
||||
is DraggableItem.GroupHeader -> checkCanMoveHeaderOver(dragOver, dragOverItem, items.lastIndex)
|
||||
is DraggableItem.Token -> checkCanMoveTokenOver(draggingItem, dragOverItem)
|
||||
is DraggableItem.GroupPlaceholder -> false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemDraggingStart(item: DraggableItem) {
|
||||
if (currentDraggingItem != null) return
|
||||
currentDraggingItem = item
|
||||
|
||||
updateListState {
|
||||
when (item) {
|
||||
is DraggableItem.GroupPlaceholder -> 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.Empty -> items
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemDraggingEnd() {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
val draggingItem = currentDraggingItem ?: return@launch
|
||||
|
||||
delay(FINISH_DRAGGING_DELAY_MILLIS)
|
||||
|
||||
updateListState {
|
||||
when (draggingItem) {
|
||||
is DraggableItem.GroupHeader -> draggableGroupsOperations.expandGroups(items)
|
||||
is DraggableItem.Token -> items.uniteItems()
|
||||
is DraggableItem.GroupPlaceholder -> items
|
||||
}
|
||||
}
|
||||
|
||||
currentDraggingItem = null
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemDragged(from: ItemPosition, to: ItemPosition) = updateListState {
|
||||
items.mutate {
|
||||
it.add(to.index, it.removeAt(from.index))
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateListState(block: OrganizeTokensListState.() -> List<DraggableItem>) {
|
||||
val updatedState = currentListState.updateItems { block(currentListState) }
|
||||
|
||||
listStateFlowInternal.tryEmit(updatedState)
|
||||
}
|
||||
|
||||
private fun findItemsToMove(
|
||||
items: List<DraggableItem>,
|
||||
moveOverItemKey: Any?,
|
||||
movedItemKey: Any?,
|
||||
): Pair<DraggableItem?, DraggableItem?> {
|
||||
var moveOverItem: DraggableItem? = null
|
||||
var movedItem: DraggableItem? = null
|
||||
|
||||
for (item in items) {
|
||||
if (item.id == moveOverItemKey) {
|
||||
moveOverItem = item
|
||||
}
|
||||
if (item.id == movedItemKey) {
|
||||
movedItem = item
|
||||
}
|
||||
if (moveOverItem != null && movedItem != null) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return Pair(moveOverItem, movedItem)
|
||||
}
|
||||
|
||||
private fun checkCanMoveHeaderOver(
|
||||
moveOverItemPosition: ItemPosition,
|
||||
moveOverItem: DraggableItem,
|
||||
lastItemIndex: Int,
|
||||
): Boolean {
|
||||
// Group item can be moved only to group divider or to ages of the items list
|
||||
return when {
|
||||
moveOverItemPosition.index == 0 -> true
|
||||
moveOverItemPosition.index == lastItemIndex -> true
|
||||
moveOverItem is DraggableItem.GroupPlaceholder -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkCanMoveTokenOver(item: DraggableItem.Token, moveOverItem: DraggableItem): Boolean {
|
||||
// Token item can be moved only in its group
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@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
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
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.getGroupPlaceholder
|
||||
import com.tangem.feature.wallet.presentation.organizetokens.utils.common.uniteItems
|
||||
|
||||
internal class DraggableGroupsOperations {
|
||||
|
||||
private var groupIdToTokens: Map<String, List<DraggableItem.Token>>? = null
|
||||
|
||||
fun collapseGroup(items: List<DraggableItem>, movingGroup: DraggableItem.GroupHeader): List<DraggableItem> {
|
||||
if (!groupIdToTokens.isNullOrEmpty()) return items
|
||||
|
||||
groupIdToTokens = items
|
||||
.asSequence()
|
||||
.filterIsInstance<DraggableItem.Token>()
|
||||
.groupBy { it.groupId }
|
||||
|
||||
val itemsWithoutGroupTokens = items.filterNot {
|
||||
it is DraggableItem.Token && it.groupId == movingGroup.id
|
||||
}
|
||||
|
||||
return divideGroups(itemsWithoutGroupTokens, movingGroup)
|
||||
}
|
||||
|
||||
fun expandGroups(items: List<DraggableItem>): List<DraggableItem> {
|
||||
if (groupIdToTokens.isNullOrEmpty()) return items
|
||||
|
||||
val currentGroups = items.filterIsInstance<DraggableItem.GroupHeader>()
|
||||
val lastGroupIndex = currentGroups.lastIndex
|
||||
|
||||
val expandedGroups = currentGroups
|
||||
.flatMapIndexed { index, group ->
|
||||
buildList {
|
||||
add(group)
|
||||
addAll(groupIdToTokens?.get(group.id).orEmpty())
|
||||
if (index != lastGroupIndex) {
|
||||
add(getGroupPlaceholder(index))
|
||||
}
|
||||
}
|
||||
}
|
||||
.uniteItems()
|
||||
|
||||
groupIdToTokens = null
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue