Updated on 2026-08-14
This commit is contained in:
parent
88f84097f7
commit
21f95cf1b3
4 changed files with 232 additions and 40 deletions
|
|
@ -7,9 +7,10 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.util.fastForEachIndexed
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import com.tangem.core.ui.components.rows.model.BlockchainRowUM
|
||||
import com.tangem.core.ui.components.rows.model.ChainRowUM
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.features.managetokens.component.ManageTokensComponent
|
||||
import com.tangem.features.managetokens.entity.CurrencyItemUM
|
||||
import com.tangem.features.managetokens.entity.CurrencyNetworkUM
|
||||
|
|
@ -21,25 +22,55 @@ import kotlinx.collections.immutable.toImmutableList
|
|||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlin.random.Random
|
||||
|
||||
internal class PreviewManageTokensComponent : ManageTokensComponent {
|
||||
|
||||
private var items = List(size = 30) { index ->
|
||||
if (index < 2) {
|
||||
getCustomItem(index)
|
||||
} else {
|
||||
getBasicItem(index)
|
||||
}
|
||||
}.toPersistentList()
|
||||
private val changedItemsIds: MutableSet<String> = mutableSetOf()
|
||||
|
||||
private var items = initItems()
|
||||
|
||||
private val previewState = MutableStateFlow(
|
||||
value = ManageTokensUM(
|
||||
popBack = {},
|
||||
items = items,
|
||||
search = SearchBarUM(
|
||||
placeholderText = resourceReference(R.string.manage_tokens_search_placeholder),
|
||||
query = "",
|
||||
onQueryChange = ::searchCurrencies,
|
||||
isActive = false,
|
||||
onActiveChange = ::toggleSearchBar,
|
||||
),
|
||||
hasChanges = false,
|
||||
onSaveClick = {},
|
||||
onAddCustomToken = {},
|
||||
),
|
||||
)
|
||||
|
||||
private fun searchCurrencies(query: String) {
|
||||
previewState.update { state ->
|
||||
items = if (query.isBlank()) {
|
||||
initItems()
|
||||
} else {
|
||||
items.filter { currency ->
|
||||
currency.model.name.contains(query, ignoreCase = true)
|
||||
}.toPersistentList()
|
||||
}
|
||||
|
||||
state.copy(
|
||||
search = state.search.copy(query = query),
|
||||
items = items,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleSearchBar(isActive: Boolean) {
|
||||
previewState.update { state ->
|
||||
state.copy(
|
||||
search = state.search.copy(isActive = isActive),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by previewState.collectAsState()
|
||||
|
|
@ -50,15 +81,23 @@ internal class PreviewManageTokensComponent : ManageTokensComponent {
|
|||
)
|
||||
}
|
||||
|
||||
private fun initItems() = List(size = 30) { index ->
|
||||
if (index < 2) {
|
||||
getCustomItem(index)
|
||||
} else {
|
||||
getBasicItem(index)
|
||||
}
|
||||
}.toPersistentList()
|
||||
|
||||
private fun getCustomItem(index: Int) = CurrencyItemUM.Custom(
|
||||
id = index.toString(),
|
||||
model = ChainRowUM(
|
||||
name = stringReference("Custom token $index"),
|
||||
type = stringReference("CT$index"),
|
||||
name = "Custom token $index",
|
||||
type = "CT$index",
|
||||
icon = CurrencyIconState.CustomTokenIcon(
|
||||
tint = Color.White,
|
||||
background = Color.Black,
|
||||
networkBadgeIconResId = R.drawable.img_eth_22,
|
||||
topBadgeIconResId = R.drawable.img_eth_22,
|
||||
isGrayscale = false,
|
||||
showCustomBadge = true,
|
||||
),
|
||||
|
|
@ -70,8 +109,8 @@ internal class PreviewManageTokensComponent : ManageTokensComponent {
|
|||
private fun getBasicItem(index: Int) = CurrencyItemUM.Basic(
|
||||
id = index.toString(),
|
||||
model = ChainRowUM(
|
||||
name = stringReference("Currency $index"),
|
||||
type = stringReference("C$index"),
|
||||
name = "Currency $index",
|
||||
type = "C$index",
|
||||
icon = CurrencyIconState.CoinIcon(
|
||||
url = null,
|
||||
fallbackResId = R.drawable.img_btc_22,
|
||||
|
|
@ -85,18 +124,16 @@ internal class PreviewManageTokensComponent : ManageTokensComponent {
|
|||
)
|
||||
|
||||
private fun getCurrencyNetworks(currencyIndex: Int) = List(size = 5) { networkIndex ->
|
||||
val selectedIndex = Random.nextInt(from = 0, until = 5)
|
||||
|
||||
CurrencyNetworkUM(
|
||||
id = networkIndex.toString(),
|
||||
model = BlockchainRowUM(
|
||||
name = stringReference("NETWORK$networkIndex"),
|
||||
type = stringReference("N$networkIndex"),
|
||||
icon = CurrencyIconState.Locked,
|
||||
isAccented = networkIndex == 0,
|
||||
usePrimaryTextColor = networkIndex == selectedIndex,
|
||||
name = "NETWORK$networkIndex",
|
||||
type = "N$networkIndex",
|
||||
iconResId = R.drawable.ic_eth_16,
|
||||
isMainNetwork = networkIndex == 0,
|
||||
isSelected = false,
|
||||
),
|
||||
isSelected = networkIndex == selectedIndex,
|
||||
isSelected = false,
|
||||
onSelectedStateChange = { toggleNetwork(currencyIndex, networkIndex, isSelected = it) },
|
||||
)
|
||||
}.toImmutableList()
|
||||
|
|
@ -131,7 +168,12 @@ internal class PreviewManageTokensComponent : ManageTokensComponent {
|
|||
if (index == networkIndex) {
|
||||
it[index] = network.copy(
|
||||
model = network.model.copy(
|
||||
usePrimaryTextColor = isSelected,
|
||||
iconResId = if (isSelected) {
|
||||
R.drawable.img_eth_22
|
||||
} else {
|
||||
R.drawable.ic_eth_16
|
||||
},
|
||||
isSelected = isSelected,
|
||||
),
|
||||
isSelected = isSelected,
|
||||
)
|
||||
|
|
@ -146,11 +188,21 @@ internal class PreviewManageTokensComponent : ManageTokensComponent {
|
|||
is CurrencyItemUM.Custom -> return
|
||||
}
|
||||
|
||||
val id = "${currencyIndex}_$networkIndex"
|
||||
if (changedItemsIds.contains(id)) {
|
||||
changedItemsIds.remove(id)
|
||||
} else {
|
||||
changedItemsIds.add(id)
|
||||
}
|
||||
|
||||
previewState.update { state ->
|
||||
items = items.mutate {
|
||||
it[currencyIndex] = updatedItem
|
||||
}
|
||||
state.copy(items = items)
|
||||
state.copy(
|
||||
items = items,
|
||||
hasChanges = changedItemsIds.isNotEmpty(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,10 +8,11 @@ import kotlinx.collections.immutable.ImmutableList
|
|||
internal sealed class CurrencyItemUM {
|
||||
|
||||
abstract val id: String
|
||||
abstract val model: ChainRowUM
|
||||
|
||||
data class Basic(
|
||||
override val id: String,
|
||||
val model: ChainRowUM,
|
||||
override val model: ChainRowUM,
|
||||
val networks: NetworksUM,
|
||||
val onExpandClick: () -> Unit,
|
||||
) : CurrencyItemUM() {
|
||||
|
|
@ -29,7 +30,7 @@ internal sealed class CurrencyItemUM {
|
|||
|
||||
data class Custom(
|
||||
override val id: String,
|
||||
val model: ChainRowUM,
|
||||
override val model: ChainRowUM,
|
||||
val onRemoveClick: () -> Unit,
|
||||
) : CurrencyItemUM()
|
||||
}
|
||||
|
|
@ -1,10 +1,15 @@
|
|||
package com.tangem.features.managetokens.entity
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Immutable
|
||||
internal data class ManageTokensUM(
|
||||
val popBack: () -> Unit,
|
||||
val items: ImmutableList<CurrencyItemUM>,
|
||||
val search: SearchBarUM,
|
||||
val hasChanges: Boolean,
|
||||
val onAddCustomToken: () -> Unit,
|
||||
val onSaveClick: () -> Unit,
|
||||
)
|
||||
|
|
@ -2,25 +2,33 @@ package com.tangem.features.managetokens.ui
|
|||
|
||||
import android.content.res.Configuration
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.util.fastForEachIndexed
|
||||
import com.tangem.core.ui.components.BottomFade
|
||||
import com.tangem.core.ui.components.PrimaryButtonIconEnd
|
||||
import com.tangem.core.ui.components.TangemSwitch
|
||||
import com.tangem.core.ui.components.appbar.models.TangemTopAppBarColors
|
||||
import com.tangem.core.ui.components.buttons.SecondarySmallButton
|
||||
import com.tangem.core.ui.components.buttons.SmallButtonConfig
|
||||
import com.tangem.core.ui.components.fields.SearchBar
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import com.tangem.core.ui.components.rows.ArrowRow
|
||||
import com.tangem.core.ui.components.rows.BlockchainRow
|
||||
import com.tangem.core.ui.components.rows.ChainRow
|
||||
|
|
@ -37,29 +45,151 @@ import kotlinx.collections.immutable.ImmutableList
|
|||
private const val CHEVRON_ROTATION_EXPANDED = 180f
|
||||
private const val CHEVRON_ROTATION_COLLAPSED = 0f
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
internal fun ManageTokensScreen(state: ManageTokensUM, modifier: Modifier = Modifier) {
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||
|
||||
BackHandler(onBack = state.popBack)
|
||||
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
containerColor = TangemTheme.colors.background.primary,
|
||||
topBar = {
|
||||
TopBar(
|
||||
scrollBehavior = scrollBehavior,
|
||||
onAddCustomToken = state.onAddCustomToken,
|
||||
onPopBack = state.popBack,
|
||||
)
|
||||
},
|
||||
content = { innerPadding ->
|
||||
Currencies(
|
||||
Content(
|
||||
modifier = Modifier
|
||||
.padding(innerPadding)
|
||||
.fillMaxSize(),
|
||||
items = state.items,
|
||||
state = state,
|
||||
)
|
||||
},
|
||||
floatingActionButtonPosition = FabPosition.Center,
|
||||
floatingActionButton = {
|
||||
SaveChangesButton(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.fillMaxWidth(),
|
||||
isVisible = state.hasChanges,
|
||||
onClick = state.onSaveClick,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Currencies(items: ImmutableList<CurrencyItemUM>, modifier: Modifier = Modifier) {
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
private fun TopBar(
|
||||
scrollBehavior: TopAppBarScrollBehavior,
|
||||
onPopBack: () -> Unit,
|
||||
onAddCustomToken: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
TopAppBar(
|
||||
modifier = modifier,
|
||||
scrollBehavior = scrollBehavior,
|
||||
colors = TangemTopAppBarColors.copy(
|
||||
containerColor = TangemTheme.colors.background.primary,
|
||||
scrolledContainerColor = TangemTheme.colors.background.primary,
|
||||
),
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
modifier = Modifier.size(TangemTheme.dimens.size32),
|
||||
onClick = onPopBack,
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(TangemTheme.dimens.size24),
|
||||
painter = painterResource(id = R.drawable.ic_back_24),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(id = R.string.main_manage_tokens),
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
},
|
||||
actions = {
|
||||
IconButton(
|
||||
modifier = Modifier.size(TangemTheme.dimens.size32),
|
||||
onClick = onAddCustomToken,
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(TangemTheme.dimens.size24),
|
||||
painter = painterResource(id = R.drawable.ic_plus_24),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SaveChangesButton(isVisible: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
AnimatedVisibility(
|
||||
modifier = modifier,
|
||||
visible = isVisible,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
label = "save_button_visibility",
|
||||
) {
|
||||
PrimaryButtonIconEnd(
|
||||
text = stringResource(id = R.string.common_save),
|
||||
iconResId = R.drawable.ic_tangem_24,
|
||||
onClick = onClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Content(state: ManageTokensUM, modifier: Modifier = Modifier) {
|
||||
Box(modifier = modifier) {
|
||||
Currencies(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
items = state.items,
|
||||
search = state.search,
|
||||
)
|
||||
|
||||
AnimatedVisibility(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.fillMaxWidth(),
|
||||
visible = state.hasChanges,
|
||||
label = "bottom_fade_visibility",
|
||||
) {
|
||||
BottomFade()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
private fun Currencies(items: ImmutableList<CurrencyItemUM>, search: SearchBarUM, modifier: Modifier = Modifier) {
|
||||
LazyColumn(
|
||||
modifier = modifier,
|
||||
) {
|
||||
stickyHeader(key = "search") {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.padding(
|
||||
vertical = TangemTheme.dimens.spacing12,
|
||||
horizontal = TangemTheme.dimens.spacing16,
|
||||
)
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
SearchBar(state = search)
|
||||
}
|
||||
}
|
||||
|
||||
items(
|
||||
items = items,
|
||||
key = CurrencyItemUM::id,
|
||||
|
|
@ -132,7 +262,7 @@ private fun BasicCurrencyItem(item: CurrencyItemUM.Basic, modifier: Modifier = M
|
|||
)
|
||||
|
||||
NetworksList(
|
||||
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing12),
|
||||
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
networks = item.networks,
|
||||
currencyId = item.id,
|
||||
)
|
||||
|
|
@ -140,20 +270,24 @@ private fun BasicCurrencyItem(item: CurrencyItemUM.Basic, modifier: Modifier = M
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.NetworksList(networks: NetworksUM, currencyId: String, modifier: Modifier = Modifier) {
|
||||
private fun NetworksList(networks: NetworksUM, currencyId: String, modifier: Modifier = Modifier) {
|
||||
AnimatedVisibility(
|
||||
modifier = modifier,
|
||||
visible = networks is NetworksUM.Expanded,
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = fadeOut() + shrinkVertically(),
|
||||
label = "networks_visibility",
|
||||
) {
|
||||
Column {
|
||||
val items = (networks as? NetworksUM.Expanded)?.networks
|
||||
// to keep items on collapse, and avoid animation cancellation
|
||||
val rememberedItems = remember(key1 = currencyId) { items }
|
||||
|
||||
(items ?: rememberedItems)?.fastForEachIndexed { index, network ->
|
||||
// To keep items on collapse and avoid animation cancellation
|
||||
val rememberedItems = remember(key1 = currencyId) { items }
|
||||
val currentItems = items ?: rememberedItems
|
||||
|
||||
currentItems?.fastForEachIndexed { index, network ->
|
||||
ArrowRow(
|
||||
isLastItem = index == items?.lastIndex,
|
||||
isLastItem = index == currentItems.lastIndex,
|
||||
content = {
|
||||
BlockchainRow(
|
||||
model = network.model,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue