Updated on 2026-08-14

This commit is contained in:
Tangem 2024-06-27 13:12:31 +04:00
parent 88f84097f7
commit 21f95cf1b3
4 changed files with 232 additions and 40 deletions

View file

@ -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(),
)
}
}
}

View file

@ -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()
}

View file

@ -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,
)

View file

@ -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,