From 552e8cc7526ce547aa72068acf3fec3f40a006f1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 5 Apr 2023 15:06:54 +0800 Subject: [PATCH] Updated on 2026-08-14 --- app/build.gradle.kts | 1 + .../tokens/presentation/ui/AddTokensScreen.kt | 223 +++++++++++++ .../presentation/ui/AddTokensToolbar.kt | 207 +++++++++++++ .../presentation/ui/BriefNetworksList.kt | 82 +++++ .../presentation/ui/DetailedNetworksList.kt | 168 ++++++++++ .../presentation/ui/NetworkItemArrow.kt | 60 ++++ .../tokens/presentation/ui/TokenItem.kt | 292 ++++++++++++++++++ .../compose/currencyItem/CurrencyItemArrow.kt | 89 ------ .../ui/compose/currencyItem/NetworkItem.kt | 7 +- app/src/main/res/drawable/ic_back_24.xml | 9 + .../com/tangem/core/ui/res/TangemDimens.kt | 4 + plugins/configuration/detekt.yml | 2 +- 12 files changed, 1051 insertions(+), 93 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/features/tokens/presentation/ui/AddTokensScreen.kt create mode 100644 app/src/main/java/com/tangem/tap/features/tokens/presentation/ui/AddTokensToolbar.kt create mode 100644 app/src/main/java/com/tangem/tap/features/tokens/presentation/ui/BriefNetworksList.kt create mode 100644 app/src/main/java/com/tangem/tap/features/tokens/presentation/ui/DetailedNetworksList.kt create mode 100644 app/src/main/java/com/tangem/tap/features/tokens/presentation/ui/NetworkItemArrow.kt create mode 100644 app/src/main/java/com/tangem/tap/features/tokens/presentation/ui/TokenItem.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/tokens/ui/compose/currencyItem/CurrencyItemArrow.kt create mode 100644 app/src/main/res/drawable/ic_back_24.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3e26bcc1f8..85f77c58ce 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -45,6 +45,7 @@ dependencies { implementation(deps.lifecycle.viewModel.ktx) /** Compose libraries */ + implementation(deps.compose.constraintLayout) implementation(deps.compose.material) implementation(deps.compose.animation) implementation(deps.compose.foundation) diff --git a/app/src/main/java/com/tangem/tap/features/tokens/presentation/ui/AddTokensScreen.kt b/app/src/main/java/com/tangem/tap/features/tokens/presentation/ui/AddTokensScreen.kt new file mode 100644 index 0000000000..e2098944a0 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/presentation/ui/AddTokensScreen.kt @@ -0,0 +1,223 @@ +package com.tangem.tap.features.tokens.presentation.ui + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.res.TangemColorPalette +import com.tangem.core.ui.res.TangemTheme +import com.tangem.tap.common.compose.extensions.OnBottomReached +import com.tangem.tap.features.tokens.presentation.states.AddTokensNetworkItemState +import com.tangem.tap.features.tokens.presentation.states.AddTokensStateHolder +import com.tangem.tap.features.tokens.presentation.states.AddTokensToolbarState +import com.tangem.tap.features.tokens.presentation.states.TokenItemModel +import com.tangem.tap.features.tokens.presentation.states.TokensListVisibility +import com.tangem.tap.features.tokens.redux.TokensAction +import com.tangem.tap.store +import com.tangem.wallet.R + +/** +[REDACTED_AUTHOR] + */ +@Composable +internal fun AddTokensScreen(stateHolder: AddTokensStateHolder) { + BackHandler(onBack = stateHolder.toolbarState.onBackButtonClick) + + Scaffold( + topBar = { AddTokensToolbar(state = stateHolder.toolbarState) }, + floatingActionButton = { + if (stateHolder is AddTokensStateHolder.ManageAccess) { + SaveChangesButton(onClick = stateHolder.onSaveButtonClick) + } + }, + ) { + when (stateHolder) { + is AddTokensStateHolder.Loading -> LoadingContent() + is TokensListVisibility -> TokensListContent(stateHolder = stateHolder, scaffoldPadding = it) + } + } +} + +@Composable +private fun LoadingContent() { + Box( + modifier = Modifier + .fillMaxSize() + .background(color = TangemColorPalette.White), + contentAlignment = Alignment.Center, + ) { + CircularProgressIndicator(color = TangemColorPalette.Meadow) + } +} + +@Composable +private fun TokensListContent(stateHolder: TokensListVisibility, scaffoldPadding: PaddingValues) { + val state = rememberLazyListState() + state.OnBottomReached(loadMoreThreshold = 40) { + store.dispatch(TokensAction.LoadMore(scanResponse = store.state.globalState.scanResponse)) + } + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(scaffoldPadding), + state, + ) { + items( + items = stateHolder.tokens, + key = TokenItemModel::name, + itemContent = { TokenItem(model = it) }, + ) + } +} + +@Composable +private fun SaveChangesButton(onClick: () -> Unit) { + PrimaryButton( + modifier = Modifier + .imePadding() + .padding(horizontal = TangemTheme.dimens.spacing16) + .fillMaxWidth(), + text = stringResource(id = R.string.common_save_changes), + onClick = onClick, + ) +} + +@Preview +@Composable +private fun Preview_AddTokensScreen_Loading() { + TangemTheme { + AddTokensScreen( + stateHolder = AddTokensStateHolder.Loading( + toolbarState = AddTokensToolbarState.Title.EditAccess( + titleResId = R.string.main_manage_tokens, + onBackButtonClick = {}, + onSearchButtonClick = {}, + onAddCustomTokenClick = {}, + ), + ), + ) + } +} + +@Preview +@Composable +private fun Preview_AddTokensScreen_ManageAccess() { + TangemTheme { + AddTokensScreen( + stateHolder = AddTokensStateHolder.ManageAccess( + toolbarState = AddTokensToolbarState.Title.EditAccess( + titleResId = R.string.main_manage_tokens, + onBackButtonClick = {}, + onSearchButtonClick = {}, + onAddCustomTokenClick = {}, + ), + tokens = listOf( + TokenItemModel( + name = "ETHEREUM (ETH)", + iconUrl = "", + networks = listOf( + AddTokensNetworkItemState.EditAccess( + name = "ETHEREUM", + protocolName = "MAIN", + iconResId = R.drawable.img_btc_22, + isMainNetwork = true, + isAdded = true, + networkId = "", + onToggleClick = {}, + ), + ), + ), + TokenItemModel( + name = "Tether (USDT)", + iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/tether.png", + networks = listOf( + AddTokensNetworkItemState.EditAccess( + name = "ETHEREUM", + protocolName = "MAIN", + iconResId = R.drawable.img_eth_22, + isMainNetwork = false, + isAdded = true, + networkId = "", + onToggleClick = {}, + ), + AddTokensNetworkItemState.EditAccess( + name = "BNB SMART CHAIN", + protocolName = "BEP20", + iconResId = R.drawable.ic_bsc_no_color, + isMainNetwork = false, + isAdded = false, + networkId = "", + onToggleClick = {}, + ), + ), + ), + ), + onSaveButtonClick = {}, + ), + ) + } +} + +@Preview +@Composable +private fun Preview_AddTokensScreen_ReadAccess() { + TangemTheme { + AddTokensScreen( + stateHolder = AddTokensStateHolder.ReadAccess( + toolbarState = AddTokensToolbarState.Title.EditAccess( + titleResId = R.string.search_tokens_title, + onBackButtonClick = {}, + onSearchButtonClick = {}, + onAddCustomTokenClick = {}, + ), + tokens = listOf( + TokenItemModel( + name = "ETHEREUM (ETH)", + iconUrl = "", + networks = listOf( + AddTokensNetworkItemState.ReadAccess( + name = "ETHEREUM", + protocolName = "MAIN", + iconResId = R.drawable.img_btc_22, + isMainNetwork = true, + ), + ), + ), + TokenItemModel( + name = "Tether (USDT)", + iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/tether.png", + networks = listOf( + AddTokensNetworkItemState.ReadAccess( + name = "ETHEREUM", + protocolName = "MAIN", + iconResId = R.drawable.img_eth_22, + isMainNetwork = false, + ), + AddTokensNetworkItemState.ReadAccess( + name = "BNB SMART CHAIN", + protocolName = "BEP20", + iconResId = R.drawable.ic_bsc_no_color, + isMainNetwork = false, + ), + ), + ), + ), + ), + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/presentation/ui/AddTokensToolbar.kt b/app/src/main/java/com/tangem/tap/features/tokens/presentation/ui/AddTokensToolbar.kt new file mode 100644 index 0000000000..b8c160fa6e --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/presentation/ui/AddTokensToolbar.kt @@ -0,0 +1,207 @@ +package com.tangem.tap.features.tokens.presentation.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.Text +import androidx.compose.material.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.sp +import com.tangem.core.ui.res.TangemTheme +import com.tangem.tap.features.tokens.presentation.states.AddTokensToolbarState +import com.tangem.tap.features.tokens.presentation.states.AddTokensToolbarState.SearchInputField +import com.tangem.tap.features.tokens.presentation.states.AddTokensToolbarState.Title +import com.tangem.wallet.R + +/** +[REDACTED_AUTHOR] + */ +@Composable +internal fun AddTokensToolbar(state: AddTokensToolbarState) { + TopAppBar(backgroundColor = TangemTheme.colors.background.secondary) { + IconButton(onClick = state.onBackButtonClick) { + Icon( + painter = painterResource(id = R.drawable.ic_back_24), + contentDescription = "Go back", + tint = TangemTheme.colors.icon.secondary, + ) + } + + val contentModifier = Modifier + .weight(1f) + .padding(horizontal = TangemTheme.dimens.spacing16) + .padding(bottom = TangemTheme.dimens.spacing4) + + when (state) { + is Title -> TitleContent(state = state, modifier = contentModifier) + is SearchInputField -> InputContent(state = state, modifier = contentModifier) + } + } +} + +@Composable +private fun TitleContent(state: Title, modifier: Modifier = Modifier) { + Text( + text = stringResource(id = state.titleResId), + modifier = modifier, + color = TangemTheme.colors.text.primary1, + maxLines = 1, + style = TangemTheme.typography.h3, + ) + + IconButton(onClick = state.onSearchButtonClick) { + Icon( + painter = painterResource(id = R.drawable.ic_search_24), + contentDescription = "Search", + tint = TangemTheme.colors.icon.secondary, + ) + } + + if (state is Title.EditAccess) { + IconButton(onClick = state.onAddCustomTokenClick) { + Icon( + painter = painterResource(id = R.drawable.ic_plus_24), + contentDescription = "Add custom token", + tint = TangemTheme.colors.icon.secondary, + ) + } + } +} + +@Composable +fun InputContent(state: SearchInputField, modifier: Modifier = Modifier) { + BasicTextField( + value = state.value, + onValueChange = state.onValueChange, + modifier = modifier, + textStyle = TangemTheme.typography.subtitle1.copy(fontWeight = FontWeight.Normal), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text, imeAction = ImeAction.Search), + keyboardActions = KeyboardActions(onSearch = { state.onSearchButtonClick() }), + singleLine = true, + maxLines = 1, + cursorBrush = SolidColor(value = TangemTheme.colors.stroke.secondary), + ) { innerTextField -> + Column(modifier = Modifier.padding(top = TangemTheme.dimens.spacing6)) { + Row(verticalAlignment = Alignment.CenterVertically) { + Box( + modifier = Modifier + .weight(1f) + .padding(start = TangemTheme.dimens.spacing6), + ) { + Hint(value = state.value) + innerTextField() + } + + IconButton(onClick = state.onCleanButtonClick) { + Icon( + painter = painterResource(id = R.drawable.ic_cross_rounded_24), + contentDescription = "Clean entered value", + tint = TangemTheme.colors.icon.secondary, + ) + } + } + + Box( + modifier = Modifier + .fillMaxWidth() + .height(height = TangemTheme.dimens.size1) + .background(color = TangemTheme.colors.stroke.primary), + ) + } + } +} + +@Composable +private fun Hint(value: String) { + if (value.isBlank()) { + Row( + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing4), + horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing4), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + painter = painterResource(id = R.drawable.ic_search_24), + contentDescription = null, + tint = TangemTheme.colors.icon.secondary, + ) + Text( + text = stringResource(id = R.string.common_search), + modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing2), + color = TangemTheme.colors.text.tertiary, + maxLines = 1, + // FIXME("Incorrect typography. Replace with typography from design system") + style = TangemTheme.typography.subtitle1.copy(fontSize = 18.sp, fontWeight = FontWeight.Normal), + ) + } + } +} + +@Preview +@Composable +private fun Preview_AddTokensToolbar_EditAccess() { + TangemTheme { + AddTokensToolbar( + state = Title.EditAccess( + titleResId = R.string.main_manage_tokens, + onBackButtonClick = {}, + onSearchButtonClick = {}, + onAddCustomTokenClick = {}, + ), + ) + } +} + +@Preview +@Composable +private fun Preview_AddTokensToolbar_ReadAccess() { + TangemTheme { + AddTokensToolbar( + state = Title.ReadAccess( + titleResId = R.string.search_tokens_title, + onBackButtonClick = {}, + onSearchButtonClick = {}, + ), + ) + } +} + +@Preview +@Composable +private fun Preview_AddTokensToolbar_SearchInputField() { + var value by remember { mutableStateOf("") } + + TangemTheme { + AddTokensToolbar( + state = SearchInputField( + onBackButtonClick = {}, + onSearchButtonClick = {}, + value = value, + onValueChange = { value = it }, + onCleanButtonClick = { value = "" }, + ), + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/presentation/ui/BriefNetworksList.kt b/app/src/main/java/com/tangem/tap/features/tokens/presentation/ui/BriefNetworksList.kt new file mode 100644 index 0000000000..868395a094 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/presentation/ui/BriefNetworksList.kt @@ -0,0 +1,82 @@ +package com.tangem.tap.features.tokens.presentation.ui + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.key +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import com.tangem.core.ui.res.TangemColorPalette +import com.tangem.core.ui.res.TangemTheme +import com.tangem.tap.features.tokens.presentation.states.AddTokensNetworkItemState + +/** +[REDACTED_AUTHOR] + */ +@Composable +internal fun BriefNetworksList( + isCollapsed: Boolean, + networks: List, + modifier: Modifier = Modifier, +) { + AnimatedVisibility( + visible = isCollapsed, + modifier = modifier, + enter = fadeIn() + expandVertically(), + exit = shrinkVertically() + fadeOut(), + ) { + if (isCollapsed) { + Row(horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4)) { + networks.forEach { + key(it.name) { + BriefNetworkItem(model = it) + } + } + } + } + } +} + +/** +[REDACTED_AUTHOR] + */ +@Composable +internal fun BriefNetworkItem(model: AddTokensNetworkItemState) { + Box(modifier = Modifier.size(size = TangemTheme.dimens.size20)) { + Image( + painter = painterResource(id = model.iconResId), + contentDescription = null, + modifier = Modifier.size(size = TangemTheme.dimens.size20), + ) + + if (model.isMainNetwork) { + Box( + modifier = Modifier + .align(Alignment.TopEnd) + .size(TangemTheme.dimens.size7) + .clip(CircleShape) + .background(TangemColorPalette.White), + contentAlignment = Alignment.Center, + ) { + Box( + modifier = Modifier + .size(TangemTheme.dimens.size5) + .clip(CircleShape) + .background(TangemColorPalette.Meadow), + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/presentation/ui/DetailedNetworksList.kt b/app/src/main/java/com/tangem/tap/features/tokens/presentation/ui/DetailedNetworksList.kt new file mode 100644 index 0000000000..1ad714408f --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/presentation/ui/DetailedNetworksList.kt @@ -0,0 +1,168 @@ +package com.tangem.tap.features.tokens.presentation.ui + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material.Switch +import androidx.compose.material.SwitchDefaults +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.key +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.sp +import com.tangem.core.ui.res.TangemColorPalette +import com.tangem.core.ui.res.TangemTheme +import com.tangem.tap.features.tokens.presentation.states.AddTokensNetworkItemState +import com.tangem.wallet.R + +/** +[REDACTED_AUTHOR] + */ +@Composable +internal fun DetailedNetworksList( + isExpanded: Boolean, + networks: List, + modifier: Modifier = Modifier, +) { + AnimatedVisibility( + visible = isExpanded, + modifier = modifier, + enter = fadeIn() + expandVertically(), + exit = shrinkVertically() + fadeOut(), + ) { + if (isExpanded) { + Column { + networks.forEachIndexed { index, network -> + key(network.name) { + DetailedNetworkItem(model = network, isLastItem = networks.lastIndex == index) + } + } + } + } + } +} + +@Composable +private fun DetailedNetworkItem(model: AddTokensNetworkItemState, isLastItem: Boolean) { + val itemHeight = TangemTheme.dimens.size50 + Row( + modifier = Modifier + .fillMaxWidth() + .heightIn(min = itemHeight) + .padding(start = TangemTheme.dimens.spacing38), + verticalAlignment = Alignment.CenterVertically, + ) { + NetworkItemArrow(itemHeight = itemHeight, isLastItem = isLastItem) + Spacer(modifier = Modifier.width(TangemTheme.dimens.spacing14)) + BriefNetworkItem(model = model) + Spacer(modifier = Modifier.width(TangemTheme.dimens.spacing6)) + NetworkTitle(model = model) + + if (model is AddTokensNetworkItemState.EditAccess) { + Switch( + checked = model.isAdded, + onCheckedChange = { model.onToggleClick(model.networkId) }, + modifier = Modifier.padding(start = TangemTheme.dimens.spacing16, end = TangemTheme.dimens.spacing8), + colors = SwitchDefaults.colors( + checkedThumbColor = TangemColorPalette.Meadow, + ), + ) + } + } +} + +@Composable +private fun RowScope.NetworkTitle(model: AddTokensNetworkItemState) { + Text( + modifier = Modifier.weight(1f), + text = buildAnnotatedString { + append(text = model.name) + append(text = " ") + withStyle( + style = SpanStyle( + fontWeight = FontWeight.Normal, + color = if (model.isMainNetwork) TangemColorPalette.Meadow else TangemColorPalette.Dark2, + ), + ) { + append(text = model.protocolName) + } + }, + fontWeight = FontWeight.SemiBold, + fontSize = 13.sp, + color = if (model is AddTokensNetworkItemState.EditAccess && model.isAdded) { + TangemColorPalette.Black + } else { + TangemColorPalette.Dark2 + }, + ) +} + +@Preview +@Composable +private fun Preview_DetailedNetworksList_EditAccess() { + TangemTheme { + DetailedNetworksList( + networks = listOf( + AddTokensNetworkItemState.EditAccess( + name = "ETHEREUM", + protocolName = "MAIN", + iconResId = R.drawable.ic_eth_no_color, + isMainNetwork = true, + isAdded = true, + networkId = "", + onToggleClick = {}, + ), + AddTokensNetworkItemState.EditAccess( + name = "BNB SMART CHAIN", + protocolName = "BEP20", + iconResId = R.drawable.ic_bsc_no_color, + isMainNetwork = false, + isAdded = false, + networkId = "", + onToggleClick = {}, + ), + ), + isExpanded = false, + ) + } +} + +@Preview +@Composable +private fun Preview_DetailedNetworksList_ReadAccess() { + TangemTheme { + DetailedNetworksList( + networks = listOf( + AddTokensNetworkItemState.ReadAccess( + name = "ETHEREUM", + protocolName = "MAIN", + iconResId = R.drawable.ic_eth_no_color, + isMainNetwork = true, + ), + AddTokensNetworkItemState.ReadAccess( + name = "BNB SMART CHAIN", + protocolName = "BEP20", + iconResId = R.drawable.ic_bsc_no_color, + isMainNetwork = false, + ), + ), + isExpanded = false, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/presentation/ui/NetworkItemArrow.kt b/app/src/main/java/com/tangem/tap/features/tokens/presentation/ui/NetworkItemArrow.kt new file mode 100644 index 0000000000..5aadb66fa1 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/presentation/ui/NetworkItemArrow.kt @@ -0,0 +1,60 @@ +package com.tangem.tap.features.tokens.presentation.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.Icon +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.unit.Dp +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.res.TangemTheme +import com.tangem.wallet.R + +private const val SPECIAL_HEIGHT_3_5 = 3.5 + +/** +[REDACTED_AUTHOR] + */ +@Composable +internal fun NetworkItemArrow(itemHeight: Dp, isLastItem: Boolean) { + Box(modifier = Modifier.height(height = itemHeight)) { + if (!isLastItem) { + Box( + modifier = Modifier + .padding(start = TangemTheme.dimens.spacing0_5) + .background(color = TangemTheme.colors.stroke.primary) + .size(width = TangemTheme.dimens.size1, height = itemHeight), + ) + } + + Icon( + painter = painterResource(id = R.drawable.ic_link), + contentDescription = null, + modifier = Modifier.height(height = itemHeight / 2 + SPECIAL_HEIGHT_3_5.dp), + tint = TangemTheme.colors.stroke.primary, + ) + } +} + +@Preview +@Composable +private fun Preview_NetworkItemArrow_Column() { + TangemTheme { + Column( + modifier = Modifier + .fillMaxWidth() + .background(color = TangemTheme.colors.background.primary) + .padding(start = TangemTheme.dimens.size36), + ) { + NetworkItemArrow(itemHeight = TangemTheme.dimens.size62, isLastItem = false) + NetworkItemArrow(itemHeight = TangemTheme.dimens.size62, isLastItem = true) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/presentation/ui/TokenItem.kt b/app/src/main/java/com/tangem/tap/features/tokens/presentation/ui/TokenItem.kt new file mode 100644 index 0000000000..10b261d68b --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/presentation/ui/TokenItem.kt @@ -0,0 +1,292 @@ +package com.tangem.tap.features.tokens.presentation.ui + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.animation.with +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.constraintlayout.compose.ConstraintLayout +import androidx.constraintlayout.compose.Dimension +import coil.compose.SubcomposeAsyncImage +import coil.request.ImageRequest +import com.tangem.core.ui.res.TangemTheme +import com.tangem.tap.common.compose.extensions.toPx +import com.tangem.tap.features.tokens.presentation.states.AddTokensNetworkItemState +import com.tangem.tap.features.tokens.presentation.states.TokenItemModel +import com.tangem.wallet.R + +/** +[REDACTED_AUTHOR] + */ +@Composable +internal fun TokenItem(model: TokenItemModel) { + var isExpanded by remember { mutableStateOf(false) } + + ConstraintLayout( + modifier = Modifier + .fillMaxWidth() + .padding(top = TangemTheme.dimens.spacing16), + ) { + val (icon, title, availableNetworksText) = createRefs() + val (briefNetworksList, detailedNetworksList, changeNetworksViewButton) = createRefs() + + val spacing16 = TangemTheme.dimens.spacing16 + val spacing6 = TangemTheme.dimens.spacing6 + + Icon( + name = model.name, + iconUrl = model.iconUrl, + modifier = Modifier.constrainAs(icon) { + top.linkTo(parent.top) + start.linkTo(anchor = parent.start, margin = spacing16) + }, + ) + + Title( + title = model.name, + modifier = Modifier.constrainAs(title) { + top.linkTo(parent.top) + start.linkTo(icon.end, margin = spacing16) + end.linkTo(anchor = changeNetworksViewButton.start, margin = spacing16) + width = Dimension.fillToConstraints + }, + ) + + Subtitle( + isExpanded = isExpanded, + modifier = Modifier.constrainAs(availableNetworksText) { + top.linkTo(title.bottom, margin = spacing6) + start.linkTo(icon.end, margin = spacing16) + end.linkTo(anchor = changeNetworksViewButton.start, margin = spacing16) + width = Dimension.fillToConstraints + }, + ) + + BriefNetworksList( + isCollapsed = !isExpanded, + networks = model.networks, + modifier = Modifier.constrainAs(briefNetworksList) { + top.linkTo(title.bottom, margin = spacing6) + start.linkTo(icon.end, margin = spacing16) + end.linkTo(anchor = changeNetworksViewButton.start, margin = spacing16) + width = Dimension.fillToConstraints + }, + ) + + DetailedNetworksList( + isExpanded = isExpanded, + networks = model.networks, + modifier = Modifier.constrainAs(detailedNetworksList) { + top.linkTo(anchor = availableNetworksText.bottom, margin = spacing16) + centerHorizontallyTo(parent) + width = Dimension.fillToConstraints + }, + ) + + ChangeNetworksViewButton( + isExpanded = isExpanded, + onClick = { isExpanded = !isExpanded }, + modifier = Modifier.constrainAs(changeNetworksViewButton) { + top.linkTo(parent.top) + end.linkTo(anchor = parent.end, margin = spacing16) + if (!isExpanded) bottom.linkTo(parent.bottom) + }, + ) + } +} + +@Composable +private fun Icon(name: String, iconUrl: String, modifier: Modifier = Modifier) { + val iconModifier = modifier.size(size = TangemTheme.dimens.size46) + + SubcomposeAsyncImage( + modifier = iconModifier, + model = ImageRequest.Builder(context = LocalContext.current) + .size(size = TangemTheme.dimens.size46.toPx().toInt()) + .data(data = iconUrl) + .crossfade(enable = true) + .build(), + contentDescription = null, + loading = { PlaceholderIcon(name = name, modifier = iconModifier) }, + error = { PlaceholderIcon(name = name, modifier = iconModifier) }, + ) +} + +@Composable +private fun PlaceholderIcon(name: String, modifier: Modifier = Modifier) { + Box( + contentAlignment = Alignment.Center, + modifier = modifier.background(TangemTheme.colors.icon.primary1, shape = CircleShape), + ) { + Text( + text = name.firstOrNull()?.titlecase() ?: "", + color = TangemTheme.colors.icon.primary2, + maxLines = 1, + // FIXME("Incorrect typography. Replace with typography from design system") + style = TangemTheme.typography.h3.copy(fontWeight = FontWeight.Bold), + ) + } +} + +@Composable +private fun Title(title: String, modifier: Modifier = Modifier) { + Text( + text = title, + modifier = modifier, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Start, + maxLines = 1, + style = TangemTheme.typography.body1, + ) +} + +@Composable +private fun Subtitle(isExpanded: Boolean, modifier: Modifier = Modifier) { + AnimatedVisibility( + visible = isExpanded, + modifier = modifier, + enter = fadeIn() + expandVertically(), + exit = shrinkVertically() + fadeOut(), + ) { + Text( + text = stringResource(id = R.string.currency_subtitle_expanded), + color = TangemTheme.colors.text.secondary, + maxLines = 1, + style = TangemTheme.typography.body2, + ) + } +} + +@OptIn(ExperimentalAnimationApi::class) +@Composable +private fun ChangeNetworksViewButton( + isExpanded: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + IconButton(onClick = onClick, modifier = modifier.size(size = TangemTheme.dimens.size46)) { + AnimatedContent( + targetState = isExpanded, + transitionSpec = { fadeIn() + scaleIn() with scaleOut() + fadeOut() }, + ) { isExpanded -> + Icon( + painter = painterResource( + id = if (isExpanded) R.drawable.ic_arrow_extended else R.drawable.ic_arrow_collapsed, + ), + contentDescription = null, + tint = TangemTheme.colors.icon.secondary, + ) + } + } +} + +@Preview +@Composable +private fun Preview_TokenItem_EditAccess() { + TangemTheme { + TokenItem( + TokenItemModel( + name = "Tether (USDT)", + iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/tether.png", + networks = listOf( + AddTokensNetworkItemState.EditAccess( + name = "Ethereum", + protocolName = "MAIN", + iconResId = R.drawable.ic_eth_no_color, + isMainNetwork = true, + isAdded = true, + networkId = "", + onToggleClick = {}, + ), + AddTokensNetworkItemState.EditAccess( + name = "BNB SMART CHAIN", + protocolName = "BEP20", + iconResId = R.drawable.ic_bsc_no_color, + isMainNetwork = false, + isAdded = false, + networkId = "", + onToggleClick = {}, + ), + ), + ), + ) + } +} + +@Preview +@Composable +private fun Preview_TokenItem_ReadAccess() { + TangemTheme { + TokenItem( + TokenItemModel( + name = "Tether (USDT)", + iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/tether.png", + networks = listOf( + AddTokensNetworkItemState.ReadAccess( + name = "Ethereum", + protocolName = "MAIN", + iconResId = R.drawable.ic_eth_no_color, + isMainNetwork = true, + ), + AddTokensNetworkItemState.ReadAccess( + name = "BNB SMART CHAIN", + protocolName = "BEP20", + iconResId = R.drawable.ic_bsc_no_color, + isMainNetwork = false, + ), + ), + ), + ) + } +} + +@Preview +@Composable +private fun Preview_TokenItem_EditAccess_LargeNetworkList() { + TangemTheme { + TokenItem( + TokenItemModel( + name = "Tether (USDT)", + iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/tether.png", + networks = List( + size = 15, + init = { + AddTokensNetworkItemState.ReadAccess( + name = if (it % 2 != 0) "Ethereum" else "BNB SMART CHAIN", + protocolName = if (it % 2 != 0) "MAIN" else "BEP20", + iconResId = if (it % 2 != 0) R.drawable.ic_eth_no_color else R.drawable.ic_bsc_no_color, + isMainNetwork = it % 2 != 0, + ) + }, + ), + ), + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/currencyItem/CurrencyItemArrow.kt b/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/currencyItem/CurrencyItemArrow.kt deleted file mode 100644 index 3c0c5655cc..0000000000 --- a/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/currencyItem/CurrencyItemArrow.kt +++ /dev/null @@ -1,89 +0,0 @@ -package com.tangem.tap.features.tokens.ui.compose.currencyItem - -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import com.tangem.wallet.R - -/** -[REDACTED_AUTHOR] - */ -@Composable -fun CurrencyItemArrow( - rowHeight: Dp, - isLastArrow: Boolean, -) { - Box(Modifier.height(rowHeight)) { - if (!isLastArrow) { - MiddleArrowView(rowHeight) - } else { - LastArrowView(rowHeight) - } - } -} - -@Suppress("MagicNumber") -@Composable -private fun MiddleArrowView(rowHeight: Dp) { - Box { - Box( - modifier = Modifier - .padding(start = 0.5.dp), - ) { - Box( - modifier = Modifier - .background(color = Color(0xFFDEDEDE)) - .width(0.8.dp) - .fillMaxHeight(), - ) - } - - LastArrowView(rowHeight) - } -} - -@Composable -private fun LastArrowView(rowHeight: Dp) { - Image( - modifier = Modifier - .height(rowHeight / 2 + 3.5.dp), - painter = painterResource(id = R.drawable.ic_link), - contentDescription = null, - ) -} - -@Preview -@Composable -private fun ArrowViewPreview() { - Box(Modifier.background(color = Color.White)) { - Column( - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight() - .padding(start = 37.dp), - ) { - CurrencyItemArrow( - rowHeight = 62.dp, - isLastArrow = false, - ) - CurrencyItemArrow( - rowHeight = 62.dp, - isLastArrow = true, - ) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/currencyItem/NetworkItem.kt b/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/currencyItem/NetworkItem.kt index cf744085ba..bff5cc2247 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/currencyItem/NetworkItem.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/currencyItem/NetworkItem.kt @@ -37,6 +37,7 @@ import com.tangem.tap.common.extensions.getGreyedOutIconRes import com.tangem.tap.common.extensions.getNetworkName import com.tangem.tap.domain.tokens.Contract import com.tangem.tap.domain.tokens.Currency +import com.tangem.tap.features.tokens.presentation.ui.NetworkItemArrow import com.tangem.tap.features.tokens.redux.ContractAddress import com.tangem.tap.features.tokens.redux.TokenWithBlockchain import com.tangem.tap.features.tokens.ui.compose.CurrencyPlaceholderIcon @@ -76,9 +77,9 @@ fun NetworkItem( .width(78.dp) .padding(start = 38.dp), ) { - CurrencyItemArrow( - rowHeight = rowHeight, - isLastArrow = index == size - 1, + NetworkItemArrow( + itemHeight = rowHeight, + isLastItem = index == size - 1, ) } Box( diff --git a/app/src/main/res/drawable/ic_back_24.xml b/app/src/main/res/drawable/ic_back_24.xml new file mode 100644 index 0000000000..fd06de4ca4 --- /dev/null +++ b/app/src/main/res/drawable/ic_back_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt index a681a0239b..26189619df 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt @@ -32,8 +32,11 @@ data class TangemDimens internal constructor( // region Size val size0: Dp = 0.dp, val size0_5: Dp = 0.5.dp, + val size1: Dp = 1.dp, val size2: Dp = 2.dp, val size4: Dp = 4.dp, + val size5: Dp = 5.dp, + val size7: Dp = 7.dp, val size8: Dp = 8.dp, val size11: Dp = 11.dp, val size12: Dp = 12.dp, @@ -80,6 +83,7 @@ data class TangemDimens internal constructor( val spacing28: Dp = 28.dp, val spacing32: Dp = 32.dp, val spacing34: Dp = 34.dp, + val spacing38: Dp = 38.dp, val spacing44: Dp = 44.dp, val spacing50: Dp = 50.dp, val spacing52: Dp = 52.dp, diff --git a/plugins/configuration/detekt.yml b/plugins/configuration/detekt.yml index f26f48c71c..434aed89be 100644 --- a/plugins/configuration/detekt.yml +++ b/plugins/configuration/detekt.yml @@ -84,7 +84,7 @@ complexity: threshold: 300 LongMethod: active: true - threshold: 60 + threshold: 70 LongParameterList: active: true functionThreshold: 6