From 21f95cf1b35128f5f609d278ad129ede8cba54b0 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 27 Jun 2024 13:12:31 +0400 Subject: [PATCH] Updated on 2026-08-14 --- .../preview/PreviewManageTokensComponent.kt | 100 ++++++++--- .../managetokens/entity/CurrencyItemUM.kt | 5 +- .../managetokens/entity/ManageTokensUM.kt | 5 + .../managetokens/ui/ManageTokensScreen.kt | 162 ++++++++++++++++-- 4 files changed, 232 insertions(+), 40 deletions(-) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt index 9abc1cde2c..d68e6ca5d0 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt @@ -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 = 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(), + ) } } } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/CurrencyItemUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/CurrencyItemUM.kt index 3cd78d349a..0cfc505545 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/CurrencyItemUM.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/CurrencyItemUM.kt @@ -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() } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/ManageTokensUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/ManageTokensUM.kt index 3bd1757c82..c8678bdd38 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/ManageTokensUM.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/ManageTokensUM.kt @@ -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, + val search: SearchBarUM, + val hasChanges: Boolean, + val onAddCustomToken: () -> Unit, + val onSaveClick: () -> Unit, ) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt index af11b22917..462b1e2e79 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt @@ -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, 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, 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,