diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/TangemSwitch.kt b/core/ui/src/main/java/com/tangem/core/ui/components/TangemSwitch.kt index 8e0081a633..7b3c924e95 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/TangemSwitch.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/TangemSwitch.kt @@ -15,6 +15,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.tangem.core.ui.res.TangemTheme @@ -23,6 +24,8 @@ import com.tangem.core.ui.res.TangemTheme @Composable fun TangemSwitch( onCheckedChange: (Boolean) -> Unit, + modifier: Modifier = Modifier, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, checkedColor: Color = TangemTheme.colors.control.checked, uncheckedColor: Color = TangemTheme.colors.icon.informative, size: Dp = 48.dp, @@ -39,23 +42,20 @@ fun TangemSwitch( (if (isChecked) checkedColor else uncheckedColor) .copy(alpha = if (enabled) 1f else .4f) } - val interactionSource = remember { MutableInteractionSource() } Box( - modifier = Modifier + modifier = modifier .clickable( - interactionSource = interactionSource, - indication = null, - enabled = enabled, - ) { - onCheckedChange(!checked) - } - .indication( interactionSource = interactionSource, indication = rememberRipple( bounded = false, color = Color.Transparent, ), + enabled = enabled, + role = Role.Switch, + onClick = { + onCheckedChange(!checked) + }, ), ) { BoxWithConstraints( diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt index cf2abf3f5f..3de373a0e3 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt @@ -1,13 +1,17 @@ package com.tangem.core.ui.components.fields import android.content.res.Configuration +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.size +import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.* import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusManager import androidx.compose.ui.focus.onFocusChanged @@ -18,6 +22,7 @@ import androidx.compose.ui.platform.SoftwareKeyboardController import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -33,8 +38,9 @@ import com.tangem.core.ui.res.TangemThemePreview fun SearchBar(state: SearchBarUM, modifier: Modifier = Modifier, colors: TextFieldColors = TangemSearchBarColors) { val keyboardController = LocalSoftwareKeyboardController.current val focusManager = LocalFocusManager.current + val interactionSource = remember { MutableInteractionSource() } - TextField( + BasicTextField( modifier = modifier .fillMaxWidth() .heightIn(min = TangemTheme.dimens.size48) @@ -47,6 +53,7 @@ fun SearchBar(state: SearchBarUM, modifier: Modifier = Modifier, colors: TextFie }, value = state.query, onValueChange = state.onQueryChange, + interactionSource = interactionSource, keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Text, imeAction = ImeAction.Search, @@ -59,7 +66,43 @@ fun SearchBar(state: SearchBarUM, modifier: Modifier = Modifier, colors: TextFie ), singleLine = true, maxLines = 1, - textStyle = TangemTheme.typography.body2, + textStyle = TangemTheme.typography.body2.copy( + color = TangemTheme.colors.text.primary1, + ), + decorationBox = @Composable { innerTextField -> + DecorationBox( + state = state, + innerTextField = innerTextField, + interactionSource = interactionSource, + colors = colors, + focusManager = focusManager, + keyboardController = keyboardController, + ) + }, + ) +} + +@Suppress("LongParameterList") +@Composable +@OptIn(ExperimentalMaterial3Api::class) +private fun DecorationBox( + state: SearchBarUM, + innerTextField: @Composable () -> Unit, + interactionSource: MutableInteractionSource, + colors: TextFieldColors, + focusManager: FocusManager, + keyboardController: SoftwareKeyboardController?, +) { + TextFieldDefaults.DecorationBox( + value = state.query, + innerTextField = innerTextField, + enabled = true, + singleLine = true, + visualTransformation = VisualTransformation.None, + interactionSource = interactionSource, + shape = TangemTheme.shapes.roundedCornersXLarge, + colors = colors, + contentPadding = PaddingValues(all = TangemTheme.dimens.spacing12), leadingIcon = { Icon( modifier = Modifier.size(TangemTheme.dimens.size20), @@ -84,8 +127,6 @@ fun SearchBar(state: SearchBarUM, modifier: Modifier = Modifier, colors: TextFie overflow = TextOverflow.Ellipsis, ) }, - shape = TangemTheme.shapes.roundedCornersXLarge, - colors = colors, ) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/BlockchainRow.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/BlockchainRow.kt index d0341755a7..4f8edf75c4 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/rows/BlockchainRow.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/BlockchainRow.kt @@ -29,7 +29,7 @@ fun BlockchainRow(model: BlockchainRowUM, action: @Composable BoxScope.() -> Uni modifier = modifier .heightIn(min = TangemTheme.dimens.size52) .padding( - vertical = TangemTheme.dimens.spacing16, + vertical = TangemTheme.dimens.spacing8, horizontal = TangemTheme.dimens.spacing8, ), icon = { diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/ChainRow.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/ChainRow.kt index 175ace75f5..c57b6ebc1a 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/rows/ChainRow.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/ChainRow.kt @@ -2,16 +2,17 @@ package com.tangem.core.ui.components.rows import android.content.res.Configuration import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview 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.TangemSwitch import com.tangem.core.ui.components.currency.icon.CurrencyIcon import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.rows.model.ChainRowUM @@ -27,9 +28,10 @@ fun ChainRow(model: ChainRowUM, modifier: Modifier = Modifier, action: @Composab RowContentContainer( modifier = modifier .heightIn(min = TangemTheme.dimens.size68) + .padding(vertical = TangemTheme.dimens.spacing8) .padding( - vertical = TangemTheme.dimens.spacing12, - horizontal = TangemTheme.dimens.spacing14, + start = TangemTheme.dimens.spacing8, + end = TangemTheme.dimens.spacing12, ), horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), icon = { @@ -65,17 +67,7 @@ private fun Preview_ChainRow(@PreviewParameter(ChainRowParameterProvider::class) modifier = Modifier.background(TangemTheme.colors.background.primary), model = state, action = { - IconButton( - modifier = Modifier.size(TangemTheme.dimens.size32), - onClick = { /* [REDACTED_TODO_COMMENT]*/ }, - ) { - Icon( - modifier = Modifier.size(TangemTheme.dimens.size24), - tint = TangemTheme.colors.icon.informative, - painter = painterResource(id = R.drawable.ic_chevron_24), - contentDescription = null, - ) - } + TangemSwitch(onCheckedChange = {}, checked = false) }, ) } diff --git a/features/details/impl/build.gradle.kts b/features/details/impl/build.gradle.kts index c6694323ba..82ca657a96 100644 --- a/features/details/impl/build.gradle.kts +++ b/features/details/impl/build.gradle.kts @@ -48,7 +48,6 @@ dependencies { implementation(deps.tangem.blockchain) /* AndroidX */ - implementation(deps.androidx.fragment.ktx) implementation(deps.androidx.activity.compose) implementation(deps.lifecycle.compose) diff --git a/features/manage-tokens/api/build.gradle.kts b/features/manage-tokens/api/build.gradle.kts index cbfb3f7b1f..0cf94d3840 100644 --- a/features/manage-tokens/api/build.gradle.kts +++ b/features/manage-tokens/api/build.gradle.kts @@ -9,5 +9,10 @@ android { } dependencies { - implementation(deps.compose.foundation) + /* Project - Domain */ + implementation(projects.domain.wallets.models) + + /* Project - Core */ + implementation(projects.core.ui) + implementation(projects.core.decompose) } \ No newline at end of file diff --git a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensComponent.kt b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensComponent.kt new file mode 100644 index 0000000000..54cb358614 --- /dev/null +++ b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensComponent.kt @@ -0,0 +1,14 @@ +package com.tangem.features.managetokens.component + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.wallets.models.UserWalletId + +interface ManageTokensComponent : ComposableContentComponent { + + data class Params( + val userWalletId: UserWalletId, + ) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/manage-tokens/impl/build.gradle.kts b/features/manage-tokens/impl/build.gradle.kts index 88d462b52c..8e3cc2bddb 100644 --- a/features/manage-tokens/impl/build.gradle.kts +++ b/features/manage-tokens/impl/build.gradle.kts @@ -1,6 +1,7 @@ plugins { alias(deps.plugins.android.library) alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.serialization) alias(deps.plugins.kotlin.kapt) alias(deps.plugins.hilt.android) id("configuration") @@ -11,61 +12,30 @@ android { } dependencies { - /** AndroidX */ - implementation(deps.androidx.activity.compose) - implementation(deps.material) + /* Project - API */ + implementation(projects.features.manageTokens.api) - /** Compose */ - implementation(deps.compose.accompanist.systemUiController) - implementation(deps.compose.coil) - implementation(deps.compose.constraintLayout) - implementation(deps.compose.foundation) - implementation(deps.compose.material) - implementation(deps.compose.material3) - implementation(deps.compose.navigation) - implementation(deps.compose.navigation.hilt) - implementation(deps.compose.paging) - implementation(deps.compose.reorderable) - implementation(deps.compose.shimmer) + /* Project - Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) + implementation(projects.common.routing) + + /* AndroidX */ + implementation(deps.androidx.activity.compose) + implementation(deps.lifecycle.compose) + + /* Compose */ implementation(deps.compose.ui) implementation(deps.compose.ui.tooling) + implementation(deps.compose.foundation) + implementation(deps.compose.material3) + implementation(deps.compose.shimmer) - /** Other libraries */ - implementation(deps.arrow.core) - implementation(deps.jodatime) - implementation(deps.kotlin.immutable.collections) - implementation(deps.tangem.card.core) - implementation(deps.timber) - - /** DI */ + /* DI */ implementation(deps.hilt.android) kapt(deps.hilt.kapt) - /** Core modules */ - implementation(projects.core.analytics) - implementation(projects.core.analytics.models) - implementation(projects.core.featuretoggles) - implementation(projects.core.navigation) - implementation(projects.core.ui) - implementation(projects.core.utils) - - /** Project - Data */ - implementation(projects.data.tokens) - - /** Domain modules */ - implementation(projects.domain.card) - implementation(projects.domain.demo) - implementation(projects.domain.legacy) - implementation(projects.libs.blockchainSdk) - implementation(projects.domain.models) - implementation(projects.domain.settings) - implementation(projects.domain.tokens) - implementation(projects.domain.tokens.models) - implementation(projects.domain.wallets) - implementation(projects.domain.wallets.models) - implementation(projects.domain.appCurrency) - implementation(projects.domain.appCurrency.models) - - /** Feature Apis */ - implementation(projects.features.manageTokens.api) + /* Other */ + implementation(deps.kotlin.immutable.collections) + implementation(deps.timber) } \ No newline at end of file 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 new file mode 100644 index 0000000000..8fabf7cfd4 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt @@ -0,0 +1,212 @@ +package com.tangem.features.managetokens.component.preview + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +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.resourceReference +import com.tangem.features.managetokens.component.ManageTokensComponent +import com.tangem.features.managetokens.entity.CurrencyItemUM +import com.tangem.features.managetokens.entity.CurrencyNetworkUM +import com.tangem.features.managetokens.entity.ManageTokensUM +import com.tangem.features.managetokens.impl.R +import com.tangem.features.managetokens.ui.ManageTokensScreen +import kotlinx.collections.immutable.mutate +import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update + +internal class PreviewManageTokensComponent : ManageTokensComponent { + + 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() + + ManageTokensScreen( + modifier = modifier, + state = state, + ) + } + + 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 = "Custom token $index", + type = "CT$index", + icon = CurrencyIconState.CustomTokenIcon( + tint = Color.White, + background = Color.Black, + topBadgeIconResId = R.drawable.img_eth_22, + isGrayscale = false, + showCustomBadge = true, + ), + showCustom = true, + ), + onRemoveClick = {}, + ) + + private fun getBasicItem(index: Int) = CurrencyItemUM.Basic( + id = index.toString(), + model = ChainRowUM( + name = "Currency $index", + type = "C$index", + icon = CurrencyIconState.CoinIcon( + url = null, + fallbackResId = R.drawable.img_btc_22, + isGrayscale = false, + showCustomBadge = false, + ), + showCustom = false, + ), + networks = if (index == 2) { + CurrencyItemUM.Basic.NetworksUM.Expanded(getCurrencyNetworks(index)) + } else { + CurrencyItemUM.Basic.NetworksUM.Collapsed + }, + onExpandClick = { toggleCurrency(index) }, + ) + + private fun getCurrencyNetworks(currencyIndex: Int) = List(size = 3) { networkIndex -> + CurrencyNetworkUM( + id = networkIndex.toString(), + model = BlockchainRowUM( + name = "NETWORK$networkIndex", + type = "N$networkIndex", + iconResId = R.drawable.ic_eth_16, + isMainNetwork = networkIndex == 0, + isSelected = false, + ), + isSelected = false, + onSelectedStateChange = { toggleNetwork(currencyIndex, networkIndex, isSelected = it) }, + ) + }.toImmutableList() + + private fun toggleCurrency(index: Int) { + val updatedItem = when (val item = items[index]) { + is CurrencyItemUM.Basic -> item.copy( + networks = if (item.networks is CurrencyItemUM.Basic.NetworksUM.Collapsed) { + CurrencyItemUM.Basic.NetworksUM.Expanded(getCurrencyNetworks(index)) + } else { + CurrencyItemUM.Basic.NetworksUM.Collapsed + }, + ) + is CurrencyItemUM.Custom -> return + } + + previewState.update { state -> + items = items.mutate { + it[index] = updatedItem + } + state.copy(items = items) + } + } + + private fun toggleNetwork(currencyIndex: Int, networkIndex: Int, isSelected: Boolean) { + val updatedItem = when (val item = items[currencyIndex]) { + is CurrencyItemUM.Basic -> { + val updatedNetworks = (item.networks as? CurrencyItemUM.Basic.NetworksUM.Expanded) + ?.copy( + networks = item.networks.networks.toPersistentList().mutate { + it.fastForEachIndexed { index, network -> + if (index == networkIndex) { + it[index] = network.copy( + model = network.model.copy( + iconResId = if (isSelected) { + R.drawable.img_eth_22 + } else { + R.drawable.ic_eth_16 + }, + isSelected = isSelected, + ), + isSelected = isSelected, + ) + } + } + }, + ) + ?: return + + item.copy(networks = updatedNetworks) + } + 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, + 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 new file mode 100644 index 0000000000..0cfc505545 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/CurrencyItemUM.kt @@ -0,0 +1,36 @@ +package com.tangem.features.managetokens.entity + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.rows.model.ChainRowUM +import kotlinx.collections.immutable.ImmutableList + +@Immutable +internal sealed class CurrencyItemUM { + + abstract val id: String + abstract val model: ChainRowUM + + data class Basic( + override val id: String, + override val model: ChainRowUM, + val networks: NetworksUM, + val onExpandClick: () -> Unit, + ) : CurrencyItemUM() { + + @Immutable + sealed class NetworksUM { + + data object Collapsed : NetworksUM() + + data class Expanded( + val networks: ImmutableList, + ) : NetworksUM() + } + } + + data class Custom( + override val id: String, + 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/CurrencyNetworkUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/CurrencyNetworkUM.kt new file mode 100644 index 0000000000..8ed8dc5f9d --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/CurrencyNetworkUM.kt @@ -0,0 +1,12 @@ +package com.tangem.features.managetokens.entity + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.rows.model.BlockchainRowUM + +@Immutable +internal data class CurrencyNetworkUM( + val id: String, + val model: BlockchainRowUM, + val isSelected: Boolean, + val onSelectedStateChange: (Boolean) -> Unit, +) \ 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 new file mode 100644 index 0000000000..c8678bdd38 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/ManageTokensUM.kt @@ -0,0 +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 new file mode 100644 index 0000000000..63a14af8d4 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt @@ -0,0 +1,272 @@ +package com.tangem.features.managetokens.ui + +import android.content.res.Configuration +import androidx.activity.compose.BackHandler +import androidx.compose.animation.* +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.FabPosition +import androidx.compose.material3.Icon +import androidx.compose.material3.Scaffold +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.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.TangemTopAppBar +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +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 +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.managetokens.component.preview.PreviewManageTokensComponent +import com.tangem.features.managetokens.entity.CurrencyItemUM +import com.tangem.features.managetokens.entity.CurrencyItemUM.Basic.NetworksUM +import com.tangem.features.managetokens.entity.ManageTokensUM +import com.tangem.features.managetokens.impl.R +import kotlinx.collections.immutable.ImmutableList + +private const val CHEVRON_ROTATION_EXPANDED = 180f +private const val CHEVRON_ROTATION_COLLAPSED = 0f + +@Composable +internal fun ManageTokensScreen(state: ManageTokensUM, modifier: Modifier = Modifier) { + BackHandler(onBack = state.popBack) + + Scaffold( + modifier = modifier, + containerColor = TangemTheme.colors.background.primary, + topBar = { + TangemTopAppBar( + modifier = Modifier.statusBarsPadding(), + title = stringResource(id = R.string.main_manage_tokens), + startButton = TopAppBarButtonUM.Back(state.popBack), + endButton = TopAppBarButtonUM( + iconRes = R.drawable.ic_plus_24, + onIconClicked = state.onAddCustomToken, + ), + ) + }, + content = { innerPadding -> + Content( + modifier = Modifier + .padding(innerPadding) + .fillMaxSize(), + state = state, + ) + }, + floatingActionButtonPosition = FabPosition.Center, + floatingActionButton = { + SaveChangesButton( + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing16) + .fillMaxWidth(), + isVisible = state.hasChanges, + onClick = state.onSaveClick, + ) + }, + ) +} + +@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( + top = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing12, + ) + .padding(horizontal = TangemTheme.dimens.spacing16) + .fillMaxWidth(), + ) { + SearchBar(state = search) + } + } + + items( + items = items, + key = CurrencyItemUM::id, + ) { item -> + when (item) { + is CurrencyItemUM.Basic -> { + BasicCurrencyItem( + modifier = Modifier.fillMaxWidth(), + item = item, + ) + } + is CurrencyItemUM.Custom -> { + CustomCurrencyItem( + modifier = Modifier.fillMaxWidth(), + item = item, + ) + } + } + } + } +} + +@Composable +private fun CustomCurrencyItem(item: CurrencyItemUM.Custom, modifier: Modifier = Modifier) { + ChainRow( + modifier = modifier, + model = item.model, + action = { + SecondarySmallButton( + config = SmallButtonConfig( + text = resourceReference(R.string.manage_tokens_remove), + onClick = item.onRemoveClick, + ), + ) + }, + ) +} + +@Composable +private fun BasicCurrencyItem(item: CurrencyItemUM.Basic, modifier: Modifier = Modifier) { + val isExpanded = item.networks is NetworksUM.Expanded + + Column(modifier = modifier) { + ChainRow( + modifier = Modifier.clickable(onClick = item.onExpandClick), + model = item.model, + action = { + val rotation by animateFloatAsState( + targetValue = if (isExpanded) { + CHEVRON_ROTATION_EXPANDED + } else { + CHEVRON_ROTATION_COLLAPSED + }, + label = "chevron_rotation", + ) + + Icon( + modifier = Modifier + .rotate(rotation) + .size(TangemTheme.dimens.size24), + painter = painterResource(id = R.drawable.ic_chevron_24), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + ) + }, + ) + + NetworksList( + modifier = Modifier.padding( + start = TangemTheme.dimens.spacing10, + end = TangemTheme.dimens.spacing8, + ), + networks = item.networks, + currencyId = item.id, + ) + } +} + +@Composable +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 } + val currentItems = items ?: rememberedItems + + currentItems?.fastForEachIndexed { index, network -> + ArrowRow( + isLastItem = index == currentItems.lastIndex, + content = { + BlockchainRow( + model = network.model, + action = { + TangemSwitch( + checked = network.isSelected, + onCheckedChange = network.onSelectedStateChange, + ) + }, + ) + }, + ) + } + } + } +} + +// region Preview +@Preview(showBackground = true, widthDp = 360, heightDp = 800) +@Preview(showBackground = true, widthDp = 360, heightDp = 800, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_ManageTokens() { + TangemThemePreview { + PreviewManageTokensComponent().Content(Modifier.fillMaxWidth()) + } +} +// endregion Preview \ No newline at end of file diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt index 1612e170ed..49a6869c47 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt @@ -24,7 +24,6 @@ internal fun BaseExtension.configureCompose(project: Project) { contains(":features:onboarding") || // TODO: divide on api/impl after migrating all onboarding to module contains(Regex(pattern = ":presentation\$")) || contains(Regex(pattern = ":app\$")) || // TODO: [REDACTED_JIRA] - contains(Regex(pattern = ":features:manage-tokens:api\$")) || // provides Composable function contains(Regex(pattern = ":features:markets:api\$")) || // provides Composable function contains(Regex(pattern = ":impl\$")) }