Updated on 2026-08-14
This commit is contained in:
parent
24c7340558
commit
7a6ea23dd4
14 changed files with 816 additions and 59 deletions
|
|
@ -315,6 +315,7 @@
|
|||
<string name="common_hour">час</string>
|
||||
<string name="common_import">Импортировать</string>
|
||||
<string name="common_in_progress">В процессе</string>
|
||||
<string name="common_insufficient_balance">Недостаточный баланс</string>
|
||||
<string name="common_later">Позже</string>
|
||||
<string name="common_learn_more">Узнать больше</string>
|
||||
<string name="common_left">Осталось %1$s</string>
|
||||
|
|
@ -337,6 +338,7 @@
|
|||
<string name="common_nft">NFT</string>
|
||||
<string name="common_no">Нет</string>
|
||||
<string name="common_no_address">Нет адреса</string>
|
||||
<string name="common_no_results">Нет результата</string>
|
||||
<string name="common_not_added">Не добавлено</string>
|
||||
<string name="common_not_available">Не доступно</string>
|
||||
<string name="common_not_now">Не сейчас</string>
|
||||
|
|
@ -484,6 +486,13 @@
|
|||
<string name="domain_receive_assets_onboarding_description">Отправка средств в другой сети может повлечь потерю средств.</string>
|
||||
<string name="domain_receive_assets_onboarding_network_name">%s сеть</string>
|
||||
<string name="domain_receive_assets_onboarding_title">Отправляйте средства, используя только</string>
|
||||
<string name="dynamic_addresses">Динамические адреса</string>
|
||||
<string name="dynamic_addresses_enter_features_privacy_description">Используйте новый адрес для каждой транзакции, чтобы снизить отслеживаемость и повысить конфиденциальность в блокчейне.</string>
|
||||
<string name="dynamic_addresses_enter_features_privacy_title">Повышенная конфиденциальность</string>
|
||||
<string name="dynamic_addresses_enter_features_receving_title">Бесшовное получение</string>
|
||||
<string name="dynamic_addresses_enter_main_button_title">Включить динамические адреса</string>
|
||||
<string name="dynamic_addresses_error_has_custom_token_title">Динамические адреса недоступны</string>
|
||||
<string name="dynamic_addresses_error_service_unavailable_title">Сервис недоступен. Пожалуйста, попробуйте еще раз.</string>
|
||||
<string name="earn_best_opportunities">Лучшие возможности</string>
|
||||
<string name="earn_clear_filter">Очистить фильтр</string>
|
||||
<string name="earn_empty">Список временно пуст — он обновляется. Пожалуйста, зайдите чуть позже.</string>
|
||||
|
|
@ -701,6 +710,7 @@
|
|||
<string name="initial_message_scan_header">Приложите, чтобы отсканировать</string>
|
||||
<string name="initial_message_sign_header">Приложите, чтобы подписать</string>
|
||||
<string name="initial_message_tap_header">Приложите карту или кольцо</string>
|
||||
<string name="initial_wallet_sync_banner_title">Кошелёк успешно импортирован</string>
|
||||
<string name="key_invalidated_warning_description">Вы обновили данные биометрии, отсканируйте свою карту или кольцо для входа</string>
|
||||
<string name="koinos_insufficient_balance_to_send_koin_description">Ваш баланс должен быть выше суммы комиссии для осуществления перевода</string>
|
||||
<string name="koinos_insufficient_balance_to_send_koin_title">Недостаточно средств</string>
|
||||
|
|
@ -756,10 +766,14 @@
|
|||
<string name="markets_loading_no_data_title">Нет данных</string>
|
||||
<string name="markets_pulse_common_title">Пульс рынка</string>
|
||||
<string name="markets_quick_actions">Быстрые действия</string>
|
||||
<string name="markets_search_clear_all_hints">Очистить всё</string>
|
||||
<string name="markets_search_header_title">Поиск токенов</string>
|
||||
<string name="markets_search_hint_header">Недавние</string>
|
||||
<string name="markets_search_portfolio_header">В вашем портфеле</string>
|
||||
<string name="markets_search_result_title">Результат</string>
|
||||
<string name="markets_search_see_tokens_under_100k">Токены с капитализацией меньше 100к USD</string>
|
||||
<string name="markets_search_show_tokens">Показать токены</string>
|
||||
<string name="markets_search_title_placeholder">Криптовалюты, новости и не только</string>
|
||||
<string name="markets_search_token_no_result_title">Нет результата</string>
|
||||
<string name="markets_select_network">Выберите сеть</string>
|
||||
<string name="markets_select_wallet">Выберите кошелек</string>
|
||||
|
|
@ -817,6 +831,10 @@
|
|||
<string name="markets_token_details_holders">Держатели</string>
|
||||
<string name="markets_token_details_holders_description">Изменение количества держателей токенов в течение выбранного периода времени</string>
|
||||
<string name="markets_token_details_holders_full">Держатели</string>
|
||||
<string name="markets_token_details_insight_day_timeline">Д</string>
|
||||
<string name="markets_token_details_insight_month_timeline">М</string>
|
||||
<string name="markets_token_details_insight_week_timeline">Н</string>
|
||||
<string name="markets_token_details_insight_year_timeline">Г</string>
|
||||
<string name="markets_token_details_insights">Инсайты</string>
|
||||
<string name="markets_token_details_links">Ссылки</string>
|
||||
<string name="markets_token_details_liquidity">Ликвидность</string>
|
||||
|
|
@ -835,6 +853,7 @@
|
|||
<string name="markets_token_details_max_supply_description">Максимальное количество монет или токенов, которое может когда-либо существовать для выбранной криптовалюты</string>
|
||||
<string name="markets_token_details_max_supply_full">Максимальный объем</string>
|
||||
<string name="markets_token_details_metrics">Метрики</string>
|
||||
<string name="markets_token_details_metrics_no_limited">Нет ограничений</string>
|
||||
<string name="markets_token_details_official_links">Официальные ссылки</string>
|
||||
<string name="markets_token_details_price_performance">Динамика цены</string>
|
||||
<string name="markets_token_details_repository">Репозиторий</string>
|
||||
|
|
@ -1121,6 +1140,14 @@
|
|||
<string name="qr_scanner_camera_denied_settings_button">Настройки</string>
|
||||
<string name="qr_scanner_camera_denied_text">Вы не предоставили доступ к вашей камере</string>
|
||||
<string name="qr_scanner_camera_denied_title">Доступ к камере запрещен</string>
|
||||
<string name="qr_scanner_error_token_not_added_message">Запрашиваемый токен не добавлен в ваш кошелёк. Пожалуйста, добавьте его и попробуйте снова.</string>
|
||||
<string name="qr_scanner_error_token_not_added_title">Токен не добавлен</string>
|
||||
<string name="qr_scanner_error_unrecognized_message">Извините, этот QR-код не удалось распознать.</string>
|
||||
<string name="qr_scanner_error_unrecognized_title">Нераспознанный QR-код</string>
|
||||
<string name="qr_scanner_error_unsupported_network_message">Эта сеть не поддерживается ни одним из добавленных вами токенов. Добавьте поддерживаемый токен для отправки криптовалюты.</string>
|
||||
<string name="qr_scanner_error_unsupported_network_title">Поддерживаемые токены не найдены</string>
|
||||
<string name="qr_scanner_warning_unknown_parameters_message">Этот QR-код содержит параметры, которые не распознаны: %s. Некоторые данные платежа могут быть утеряны, если вы продолжите.</string>
|
||||
<string name="qr_scanner_warning_unknown_parameters_title">Неизвестные параметры</string>
|
||||
<string name="receive_bottom_sheet_no_memo_required_message">Memo не требуется</string>
|
||||
<string name="receive_bottom_sheet_warning_message">%1$s (%2$s) в сети %3$s</string>
|
||||
<string name="receive_bottom_sheet_warning_message_compact">%1$s в %2$s сети</string>
|
||||
|
|
@ -1239,6 +1266,12 @@
|
|||
<string name="send_memo">Мемо %s</string>
|
||||
<string name="send_memo_destination_tag_error">Недопустимый Memo</string>
|
||||
<string name="send_network_fee_warning_title">Покрытие сетевой комиссии</string>
|
||||
<plurals name="send_network_selection_hidden_tokens">
|
||||
<item quantity="one">Токен %d не совместим с этим адресом</item>
|
||||
<item quantity="few"></item>
|
||||
<item quantity="many">Токены %d не совместимы с этим адресом</item>
|
||||
<item quantity="other"></item>
|
||||
</plurals>
|
||||
<string name="send_nonce">Nonce</string>
|
||||
<string name="send_nonce_footer">Уникальный номер транзакции. Измените, чтобы ускорить отправку или отменить зависшую операцию.</string>
|
||||
<string name="send_nonce_hint">Введите nonce…</string>
|
||||
|
|
@ -1309,6 +1342,7 @@
|
|||
<string name="send_with_swap_recipient_get_amount">К получению %s</string>
|
||||
<string name="send_with_swap_remove_convert_alert_message">Вы уверены, что хотите отменить конвертацию? Ваши предыдущие данные будут удалены.</string>
|
||||
<string name="send_with_swap_remove_convert_alert_title">Убрать конвертацию</string>
|
||||
<string name="send_with_swap_something_went_wrong">Что-то пошло не так. Попробуйте еще раз.</string>
|
||||
<string name="send_with_swap_title">Отправка с обменом</string>
|
||||
<string name="sent_transaction_sent_title">Транзакция отправлена</string>
|
||||
<string name="settings_card_settings_footer">Подготовьтесь к сканированию кольца или карты, которую вы хотите настроить.</string>
|
||||
|
|
@ -1497,14 +1531,18 @@
|
|||
<string name="swapping_fee_estimation_error_text">Ошибка расчета комиссии. Пожалуйста, отправьте информацию в поддержку.</string>
|
||||
<string name="swapping_from_title">Вы отправляете</string>
|
||||
<string name="swapping_high_price_impact_description">Обмен этой суммы выбранных токенов может вызвать значительные колебания цены и уменьшить получаемую сумму.</string>
|
||||
<string name="swapping_high_price_impact_text">Вы можете получить значительно меньше из-за низкой ликвидности. Попробуйте меньшую сумму или другого провайдера.</string>
|
||||
<string name="swapping_high_price_impact_title">Высокое влияние на цену</string>
|
||||
<string name="swapping_insufficient_funds">Недостаточно средств</string>
|
||||
<string name="swapping_insufficient_funds_description">Недостаточно средств для завершения этой транзакции. Уменьшите сумму для получения или добавьте больше средств.</string>
|
||||
<string name="swapping_permission_header">Дать разрешение</string>
|
||||
<string name="swapping_swap_action">Обменять</string>
|
||||
<string name="swapping_swap_action_in_progress">Обмен…</string>
|
||||
<string name="swapping_to_title">Вы получите</string>
|
||||
<string name="swapping_token_list_title">Выберите токен</string>
|
||||
<string name="swapping_token_not_available">недоступен</string>
|
||||
<string name="swapping_trade_too_large_text">Недостаточно ликвидности для этой сделки.\nУменьшите сумму или выберите другого провайдера.</string>
|
||||
<string name="swapping_trade_too_large_title">Сумма сделки слишком велика</string>
|
||||
<string name="tangem_pay_beta_notification_subtitle">Будем рады вашей обратной связи</string>
|
||||
<string name="tangem_pay_beta_notification_title">Tangem Pay в режиме beta</string>
|
||||
<string name="tangem_pay_card_frozen">Карта заморожена</string>
|
||||
|
|
|
|||
|
|
@ -765,11 +765,13 @@
|
|||
<string name="markets_insights_info_description_message">This section’s data is sourced from the following networks: %s</string>
|
||||
<string name="markets_loading_error_title">Unable to load the data…</string>
|
||||
<string name="markets_loading_no_data_title">No data</string>
|
||||
<string name="markets_portfolio_block_add_token_title">**Add to your portfolio** to start buying, exchanging or receiving this asset</string>
|
||||
<string name="markets_portfolio_block_subtitle">In your portfolio</string>
|
||||
<string name="markets_pulse_common_title">Market Pulse</string>
|
||||
<string name="markets_quick_actions">Quick actions</string>
|
||||
<string name="markets_search_clear_all_hints">Clear all</string>
|
||||
<string name="markets_search_header_title">Search tokens</string>
|
||||
<string name="markets_search_hint_header">Recents</string>
|
||||
<string name="markets_search_hint_header">Recent\'s</string>
|
||||
<string name="markets_search_portfolio_header">In your portfolio</string>
|
||||
<string name="markets_search_result_title">Result</string>
|
||||
<string name="markets_search_see_tokens_under_100k">See tokens under 100k USD market cap</string>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
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 org.intellij.markdown.MarkdownElementTypes
|
||||
|
|
@ -26,18 +27,23 @@ fun rememberMarkdownParser() = remember {
|
|||
* @param node current processed node
|
||||
*/
|
||||
@Composable
|
||||
fun AnnotatedString.Builder.appendMarkdown(markdownText: String, node: ASTNode): AnnotatedString.Builder {
|
||||
fun AnnotatedString.Builder.appendMarkdown(
|
||||
markdownText: String,
|
||||
node: ASTNode,
|
||||
boldColor: Color = Color.Unspecified,
|
||||
): AnnotatedString.Builder {
|
||||
when (node.type) {
|
||||
MarkdownElementTypes.MARKDOWN_FILE, MarkdownElementTypes.PARAGRAPH -> {
|
||||
node.children.forEach { childNode ->
|
||||
appendMarkdown(
|
||||
markdownText = markdownText,
|
||||
node = childNode,
|
||||
boldColor = boldColor,
|
||||
)
|
||||
}
|
||||
}
|
||||
MarkdownElementTypes.STRONG -> {
|
||||
withStyle(SpanStyle(fontWeight = FontWeight.Medium)) {
|
||||
withStyle(SpanStyle(fontWeight = FontWeight.Medium, color = boldColor)) {
|
||||
node.children
|
||||
.drop(2)
|
||||
.dropLast(2)
|
||||
|
|
@ -45,6 +51,7 @@ fun AnnotatedString.Builder.appendMarkdown(markdownText: String, node: ASTNode):
|
|||
appendMarkdown(
|
||||
markdownText = markdownText,
|
||||
node = childNode,
|
||||
boldColor = boldColor,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -56,6 +63,19 @@ fun AnnotatedString.Builder.appendMarkdown(markdownText: String, node: ASTNode):
|
|||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses [rawString] as markdown and returns an [AnnotatedString] where bold (`**...**`) sections
|
||||
* are styled with [boldColor].
|
||||
*/
|
||||
@Composable
|
||||
fun formatAnnotatedWithBoldColor(rawString: String, boldColor: Color): AnnotatedString {
|
||||
val markdownParser = rememberMarkdownParser()
|
||||
val parsedTree = markdownParser.parse(MarkdownElementTypes.MARKDOWN_FILE, rawString, true)
|
||||
return buildAnnotatedString {
|
||||
appendMarkdown(markdownText = rawString, node = parsedTree, boldColor = boldColor)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a single space character to the [AnnotatedString.Builder].
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.features.feed.components.portfolio
|
||||
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
|
||||
interface PortfolioComponent : ComposableContentComponent {
|
||||
|
||||
data class Params(
|
||||
val id: CryptoCurrency.ID,
|
||||
)
|
||||
|
||||
interface Factory : ComponentFactory<Params, PortfolioComponent>
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
|
|||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.navigation.Route
|
||||
import com.tangem.core.ui.DesignFeatureToggles
|
||||
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
|
||||
import com.tangem.features.feed.components.earn.DefaultEarnComponent
|
||||
import com.tangem.features.feed.components.feed.DefaultFeedComponent
|
||||
|
|
@ -12,6 +13,7 @@ import com.tangem.features.feed.components.feed.DefaultFeedComponent.FeedParams
|
|||
import com.tangem.features.feed.components.market.details.DefaultMarketsTokenDetailsComponent
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioPreselectedDataComponent
|
||||
import com.tangem.features.feed.components.market.details.portfolio.api.MarketsPortfolioComponent
|
||||
import com.tangem.features.feed.components.market.details.portfolioblock.PortfolioBlockComponent
|
||||
import com.tangem.features.feed.components.market.list.DefaultMarketsTokenListComponent
|
||||
import com.tangem.features.feed.components.news.details.DefaultNewsDetailsComponent
|
||||
import com.tangem.features.feed.components.news.list.DefaultNewsListComponent
|
||||
|
|
@ -22,12 +24,15 @@ import com.tangem.features.promobanners.api.PromoBannersBlockComponent
|
|||
import kotlinx.serialization.Serializable
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class FeedEntryChildFactory @Inject constructor(
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val portfolioComponentFactory: MarketsPortfolioComponent.Factory,
|
||||
private val portfolioBlockComponentFactory: PortfolioBlockComponent.Factory,
|
||||
private val addToPortfolioPreselectedDataComponent: AddToPortfolioPreselectedDataComponent.Factory,
|
||||
private val promoBannersBlockComponentFactory: PromoBannersBlockComponent.Factory,
|
||||
private val newPromoBannersFeatureToggles: NewPromoBannersFeatureToggles,
|
||||
private val designFeatureToggles: DesignFeatureToggles,
|
||||
) {
|
||||
|
||||
@Serializable
|
||||
|
|
@ -77,6 +82,8 @@ internal class FeedEntryChildFactory @Inject constructor(
|
|||
params = child.params,
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
portfolioComponentFactory = portfolioComponentFactory,
|
||||
portfolioBlockComponentFactory = portfolioBlockComponentFactory,
|
||||
designFeatureToggles = designFeatureToggles,
|
||||
)
|
||||
}
|
||||
is Child.TokenList -> {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
|
|||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.context.child
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.DesignFeatureToggles
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
|
||||
import com.tangem.core.ui.components.haze.hazeEffectTangem
|
||||
|
|
@ -35,6 +36,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency
|
|||
import com.tangem.domain.markets.TokenMarketParams
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.features.feed.components.market.details.portfolio.api.MarketsPortfolioComponent
|
||||
import com.tangem.features.feed.components.market.details.portfolioblock.PortfolioBlockComponent
|
||||
import com.tangem.features.feed.model.market.details.MarketsTokenDetailsModel
|
||||
import com.tangem.features.feed.model.market.details.analytics.MarketDetailsAnalyticsEvent
|
||||
import com.tangem.features.feed.model.market.details.state.TokenNetworksState
|
||||
|
|
@ -49,7 +51,9 @@ internal class DefaultMarketsTokenDetailsComponent(
|
|||
appComponentContext: AppComponentContext,
|
||||
val params: Params,
|
||||
analyticsEventHandler: AnalyticsEventHandler,
|
||||
designFeatureToggles: DesignFeatureToggles,
|
||||
portfolioComponentFactory: MarketsPortfolioComponent.Factory,
|
||||
portfolioBlockComponentFactory: PortfolioBlockComponent.Factory,
|
||||
) : ComposableModularBottomSheetContentComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
// applying l2 compatibility
|
||||
|
|
@ -61,25 +65,41 @@ internal class DefaultMarketsTokenDetailsComponent(
|
|||
private val analyticsParams = params.analyticsParams
|
||||
private val model: MarketsTokenDetailsModel = getOrCreateModel(updatedParams)
|
||||
|
||||
private val portfolioComponent: MarketsPortfolioComponent? = if (updatedParams.shouldShowPortfolio) {
|
||||
portfolioComponentFactory.create(
|
||||
context = child("my_portfolio"),
|
||||
params = MarketsPortfolioComponent.Params(
|
||||
updatedParams.token,
|
||||
analyticsParams = analyticsParams?.source?.let { MarketsPortfolioComponent.AnalyticsParams(it) },
|
||||
),
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
private val portfolioComponent: MarketsPortfolioComponent? =
|
||||
if (updatedParams.shouldShowPortfolio && !designFeatureToggles.isRedesignEnabled) {
|
||||
portfolioComponentFactory.create(
|
||||
context = child("my_portfolio"),
|
||||
params = MarketsPortfolioComponent.Params(
|
||||
updatedParams.token,
|
||||
analyticsParams = analyticsParams?.source?.let { MarketsPortfolioComponent.AnalyticsParams(it) },
|
||||
),
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
private val portfolioBlockComponent: PortfolioBlockComponent? =
|
||||
if (updatedParams.shouldShowPortfolio && designFeatureToggles.isRedesignEnabled) {
|
||||
portfolioBlockComponentFactory.create(
|
||||
context = child("portfolio_block"),
|
||||
params = PortfolioBlockComponent.Params(updatedParams.token),
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
init {
|
||||
|
||||
componentScope.launch(dispatchers.default) {
|
||||
model.networksState.collectLatest { state ->
|
||||
when (state) {
|
||||
is TokenNetworksState.NetworksAvailable -> portfolioComponent?.setTokenNetworks(state.networks)
|
||||
TokenNetworksState.NoNetworksAvailable -> portfolioComponent?.setNoNetworksAvailable()
|
||||
is TokenNetworksState.NetworksAvailable -> {
|
||||
portfolioBlockComponent?.setTokenNetworks(state.networks)
|
||||
portfolioComponent?.setTokenNetworks(state.networks)
|
||||
}
|
||||
TokenNetworksState.NoNetworksAvailable -> {
|
||||
portfolioBlockComponent?.setNoNetworksAvailable()
|
||||
portfolioComponent?.setNoNetworksAvailable()
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
|
@ -192,6 +212,11 @@ internal class DefaultMarketsTokenDetailsComponent(
|
|||
component.Content(blockModifier)
|
||||
}
|
||||
},
|
||||
portfolioFloatingBlock = portfolioBlockComponent?.let { component ->
|
||||
{ blockModifier ->
|
||||
component.Content(blockModifier)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import com.tangem.domain.markets.TokenMarketInfo
|
|||
import com.tangem.domain.markets.TokenMarketParams
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Deprecated("Will be removed in favor of PortfolioComponent with redesigned UI")
|
||||
@Stable
|
||||
interface MarketsPortfolioComponent : ComposableContentComponent {
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,115 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolioblock
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.arkivanov.decompose.ComponentContext
|
||||
import com.arkivanov.decompose.extensions.compose.subscribeAsState
|
||||
import com.arkivanov.decompose.router.slot.childSlot
|
||||
import com.arkivanov.decompose.router.slot.dismiss
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.context.childByContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
|
||||
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.domain.markets.TokenMarketParams
|
||||
import com.tangem.features.feed.components.market.details.portfolioblock.model.PortfolioBlockModel
|
||||
import com.tangem.features.feed.components.market.details.portfolioblock.model.PortfolioBlockRoute
|
||||
import com.tangem.features.feed.components.market.details.portfolioblock.ui.PortfolioBlock
|
||||
import com.tangem.features.feed.components.portfolio.PortfolioComponent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Stable
|
||||
internal class PortfolioBlockComponent @AssistedInject constructor(
|
||||
@Assisted context: AppComponentContext,
|
||||
@Assisted private val params: Params,
|
||||
) : ComposableContentComponent, AppComponentContext by context {
|
||||
|
||||
private val portfolioComponentFactory: PortfolioComponent.Factory = object : PortfolioComponent.Factory {
|
||||
override fun create(context: AppComponentContext, params: PortfolioComponent.Params): PortfolioComponent {
|
||||
TODO("STUB. Will be implemented")
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Params(
|
||||
val token: TokenMarketParams,
|
||||
)
|
||||
|
||||
private val model: PortfolioBlockModel = getOrCreateModel(params)
|
||||
|
||||
private val bottomSheetSlot = childSlot(
|
||||
source = model.bottomSheetNavigation,
|
||||
serializer = PortfolioBlockRoute.serializer(),
|
||||
handleBackButton = true,
|
||||
childFactory = ::createBottomSheetChild,
|
||||
)
|
||||
|
||||
fun setTokenNetworks(networks: List<TokenMarketInfo.Network>) {
|
||||
model.setTokenNetworks(networks)
|
||||
}
|
||||
|
||||
fun setNoNetworksAvailable() {
|
||||
model.setNoNetworksAvailable()
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.state.collectAsStateWithLifecycle()
|
||||
val bottomSheet by bottomSheetSlot.subscribeAsState()
|
||||
|
||||
PortfolioBlock(modifier = modifier, state = state)
|
||||
bottomSheet.child?.instance?.BottomSheet()
|
||||
}
|
||||
|
||||
@Suppress("UnusedParameter")
|
||||
private fun createBottomSheetChild(
|
||||
config: PortfolioBlockRoute,
|
||||
componentContext: ComponentContext,
|
||||
): ComposableBottomSheetComponent {
|
||||
val currencyId = model.cryptoCurrencyIdState.value ?: return ComposableBottomSheetComponent.EMPTY
|
||||
val portfolioComponent = portfolioComponentFactory.create(
|
||||
context = childByContext(componentContext),
|
||||
params = PortfolioComponent.Params(id = currencyId),
|
||||
)
|
||||
return PortfolioBottomSheetWrapper(
|
||||
portfolioComponent = portfolioComponent,
|
||||
onDismiss = { model.bottomSheetNavigation.dismiss() },
|
||||
)
|
||||
}
|
||||
|
||||
private class PortfolioBottomSheetWrapper(
|
||||
private val portfolioComponent: PortfolioComponent,
|
||||
private val onDismiss: () -> Unit,
|
||||
) : ComposableBottomSheetComponent {
|
||||
|
||||
override fun dismiss() = onDismiss()
|
||||
|
||||
@Composable
|
||||
override fun BottomSheet() {
|
||||
TangemBottomSheet<TangemBottomSheetConfigContent.Empty>(
|
||||
config = TangemBottomSheetConfig(
|
||||
isShown = true,
|
||||
onDismissRequest = ::dismiss,
|
||||
content = TangemBottomSheetConfigContent.Empty,
|
||||
),
|
||||
) {
|
||||
portfolioComponent.Content(modifier = Modifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(context: AppComponentContext, params: Params): PortfolioBlockComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolioblock.di
|
||||
|
||||
import com.tangem.core.decompose.di.ModelComponent
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.features.feed.components.market.details.portfolioblock.model.PortfolioBlockModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.multibindings.ClassKey
|
||||
import dagger.multibindings.IntoMap
|
||||
|
||||
@Module
|
||||
@InstallIn(ModelComponent::class)
|
||||
internal interface PortfolioBlockModelModule {
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(PortfolioBlockModel::class)
|
||||
fun providePortfolioBlockModel(model: PortfolioBlockModel): Model
|
||||
}
|
||||
|
|
@ -0,0 +1,188 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolioblock.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import com.arkivanov.decompose.router.slot.SlotNavigation
|
||||
import com.arkivanov.decompose.router.slot.activate
|
||||
import com.tangem.blockchainsdk.compatibility.getTokenIdIfL2Network
|
||||
import com.tangem.common.getTotalFiatAmount
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.format.bigdecimal.fiat
|
||||
import com.tangem.core.ui.format.bigdecimal.formatStyled
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.domain.account.models.AccountStatusList
|
||||
import com.tangem.domain.account.status.supplier.MultiAccountStatusListSupplier
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.domain.models.account.filterCryptoPortfolio
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.isMultiCurrency
|
||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.features.feed.components.market.details.portfolioblock.PortfolioBlockComponent
|
||||
import com.tangem.features.feed.components.market.details.portfolioblock.ui.state.PortfolioBlockUM
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.*
|
||||
import java.math.BigDecimal
|
||||
import javax.inject.Inject
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Stable
|
||||
@ModelScoped
|
||||
internal class PortfolioBlockModel @Inject constructor(
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
paramsContainer: ParamsContainer,
|
||||
private val allAccountSupplier: MultiAccountStatusListSupplier,
|
||||
private val getUserWalletUseCase: GetUserWalletUseCase,
|
||||
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
|
||||
) : Model() {
|
||||
|
||||
val state: StateFlow<PortfolioBlockUM>
|
||||
field = MutableStateFlow<PortfolioBlockUM>(PortfolioBlockUM.Loading)
|
||||
|
||||
val bottomSheetNavigation: SlotNavigation<PortfolioBlockRoute> = SlotNavigation()
|
||||
|
||||
val cryptoCurrencyIdState: StateFlow<CryptoCurrency.ID?>
|
||||
field = MutableStateFlow(null)
|
||||
|
||||
private val params = paramsContainer.require<PortfolioBlockComponent.Params>()
|
||||
private val currencyRawId: CryptoCurrency.RawID = params.token.id
|
||||
|
||||
private val tokenIcon: CurrencyIconState = CurrencyIconState.CoinIcon(
|
||||
url = params.token.imageUrl,
|
||||
fallbackResId = R.drawable.ic_custom_token_44,
|
||||
isGrayscale = false,
|
||||
shouldShowCustomBadge = false,
|
||||
)
|
||||
|
||||
private val availableNetworks = MutableSharedFlow<List<TokenMarketInfo.Network>>(
|
||||
replay = 1,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST,
|
||||
)
|
||||
|
||||
init {
|
||||
combineData()
|
||||
.onEach { state.value = it }
|
||||
.flowOn(dispatchers.default)
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
fun setTokenNetworks(networks: List<TokenMarketInfo.Network>) {
|
||||
availableNetworks.tryEmit(networks)
|
||||
}
|
||||
|
||||
fun setNoNetworksAvailable() {
|
||||
availableNetworks.tryEmit(emptyList())
|
||||
}
|
||||
|
||||
private fun combineData(): Flow<PortfolioBlockUM> {
|
||||
return availableNetworks.transformLatest { networks ->
|
||||
if (networks.isEmpty()) {
|
||||
emit(PortfolioBlockUM.Hidden)
|
||||
} else {
|
||||
emitAll(portfolioFlow().distinctUntilChanged())
|
||||
}
|
||||
}.distinctUntilChanged()
|
||||
}
|
||||
|
||||
private fun portfolioFlow(): Flow<PortfolioBlockUM> {
|
||||
val portfolioDataFlow = allAccountSupplier().flatMapLatest { accountStatusLists ->
|
||||
val walletFlows = accountStatusLists.map { accountStatusList ->
|
||||
getUserWalletUseCase.invokeFlow(accountStatusList.userWalletId)
|
||||
.mapNotNull { it.getOrNull() }
|
||||
.map { wallet ->
|
||||
WalletPortfolio(
|
||||
userWallet = wallet,
|
||||
currencies = accountStatusList.filterByRawId(),
|
||||
)
|
||||
}
|
||||
}
|
||||
if (walletFlows.isEmpty()) {
|
||||
flowOf(emptyList())
|
||||
} else {
|
||||
combine(walletFlows) { it.toList() }
|
||||
}
|
||||
}.distinctUntilChanged()
|
||||
|
||||
val settingsFlow = combine(
|
||||
getSelectedAppCurrencyUseCase.invokeOrDefault(),
|
||||
getBalanceHidingSettingsUseCase.isBalanceHidden(),
|
||||
) { appCurrency, isBalanceHidden ->
|
||||
SettingsBox(appCurrency, isBalanceHidden)
|
||||
}.distinctUntilChanged()
|
||||
|
||||
return combine(portfolioDataFlow, settingsFlow) { portfolios, settings ->
|
||||
buildState(portfolios, settings)
|
||||
}.distinctUntilChanged()
|
||||
}
|
||||
|
||||
private fun AccountStatusList.filterByRawId(): List<CryptoCurrencyStatus> {
|
||||
return accountStatuses.filterCryptoPortfolio().flatMap { accountStatus ->
|
||||
accountStatus.tokenList.flattenCurrencies().filter { status ->
|
||||
val statusRawId = status.currency.id.rawCurrencyId ?: return@filter false
|
||||
getTokenIdIfL2Network(statusRawId.value) == currencyRawId.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildState(portfolios: List<WalletPortfolio>, settings: SettingsBox): PortfolioBlockUM {
|
||||
val allCurrencies = portfolios.flatMap { it.currencies }
|
||||
val hasMultiCurrencyWallet = portfolios.any { it.userWallet.isMultiCurrency }
|
||||
|
||||
if (allCurrencies.isEmpty()) {
|
||||
return if (hasMultiCurrencyWallet) {
|
||||
PortfolioBlockUM.AddToken(
|
||||
tokenIcon = tokenIcon,
|
||||
onClick = { bottomSheetNavigation.activate(PortfolioBlockRoute) },
|
||||
)
|
||||
} else {
|
||||
PortfolioBlockUM.Hidden
|
||||
}
|
||||
}
|
||||
|
||||
cryptoCurrencyIdState.update {
|
||||
it ?: allCurrencies.first().currency.id
|
||||
}
|
||||
|
||||
val totalFiat = allCurrencies.mapNotNull { it.getTotalFiatAmount() }
|
||||
.fold(BigDecimal.ZERO, BigDecimal::add)
|
||||
|
||||
val formattedBalance = totalFiat.formatStyled {
|
||||
fiat(
|
||||
fiatCurrencyCode = settings.appCurrency.code,
|
||||
fiatCurrencySymbol = settings.appCurrency.symbol,
|
||||
spanStyleReference = { SpanStyle(color = TangemTheme.colors2.text.neutral.secondary) },
|
||||
)
|
||||
}
|
||||
|
||||
return PortfolioBlockUM.Content(
|
||||
totalBalance = formattedBalance,
|
||||
tokensInPortfolioCount = allCurrencies.size,
|
||||
tokenIcon = tokenIcon,
|
||||
tokenName = allCurrencies.first().currency.name,
|
||||
tokenSymbol = allCurrencies.first().currency.symbol,
|
||||
isBalanceHidden = settings.isBalanceHidden,
|
||||
onClick = { bottomSheetNavigation.activate(PortfolioBlockRoute) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private data class WalletPortfolio(
|
||||
val userWallet: UserWallet,
|
||||
val currencies: List<CryptoCurrencyStatus>,
|
||||
)
|
||||
|
||||
private data class SettingsBox(
|
||||
val appCurrency: AppCurrency,
|
||||
val isBalanceHidden: Boolean,
|
||||
)
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolioblock.model
|
||||
|
||||
import com.tangem.core.decompose.navigation.Route
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
internal data object PortfolioBlockRoute : Route
|
||||
|
|
@ -0,0 +1,266 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolioblock.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.EnterExitState
|
||||
import androidx.compose.animation.core.CubicBezierEasing
|
||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||
import androidx.compose.animation.core.animateFloat
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.BottomSheetDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.layout.layoutId
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.BottomFade
|
||||
import com.tangem.core.ui.components.SpacerW
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.ds.button.*
|
||||
import com.tangem.core.ui.ds.row.TangemRowContainer
|
||||
import com.tangem.core.ui.ds.row.TangemRowLayoutId
|
||||
import com.tangem.core.ui.extensions.formatAnnotatedWithBoldColor
|
||||
import com.tangem.core.ui.extensions.orMaskWithStars
|
||||
import com.tangem.core.ui.extensions.resolveAnnotatedReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.LocalWindowSize
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||
import com.tangem.features.feed.components.market.details.portfolioblock.ui.state.PortfolioBlockUM
|
||||
import com.tangem.features.feed.impl.R
|
||||
|
||||
@Composable
|
||||
internal fun PortfolioBlock(state: PortfolioBlockUM, modifier: Modifier = Modifier) {
|
||||
val isVisible = state is PortfolioBlockUM.AddToken || state is PortfolioBlockUM.Content
|
||||
val screenHeightPx = with(LocalDensity.current) { LocalWindowSize.current.height.toPx() }
|
||||
|
||||
Box(modifier = modifier) {
|
||||
AnimatedVisibility(
|
||||
visible = isVisible,
|
||||
enter = fadeIn(animationSpec = tween(durationMillis = 300)),
|
||||
) {
|
||||
BottomFade(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.align(Alignment.BottomCenter),
|
||||
)
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = isVisible,
|
||||
enter = fadeIn(animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing)),
|
||||
exit = fadeOut(animationSpec = tween(durationMillis = 300)),
|
||||
) {
|
||||
val slideOffset by transition.animateFloat(
|
||||
transitionSpec = {
|
||||
tween(
|
||||
durationMillis = 600,
|
||||
easing = CubicBezierEasing(a = 0.83f, b = 0f, c = 0.17f, d = 1f),
|
||||
)
|
||||
},
|
||||
label = "slideOffset",
|
||||
) { enterExitState ->
|
||||
when (enterExitState) {
|
||||
EnterExitState.PreEnter -> screenHeightPx
|
||||
EnterExitState.Visible -> 0f
|
||||
EnterExitState.PostExit -> 0f
|
||||
}
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.graphicsLayer { translationY = slideOffset }) {
|
||||
when (state) {
|
||||
is PortfolioBlockUM.AddToken -> AddTokenBlock(state)
|
||||
is PortfolioBlockUM.Content -> ContentBlock(state)
|
||||
is PortfolioBlockUM.Hidden,
|
||||
is PortfolioBlockUM.Loading,
|
||||
-> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ContentBlock(state: PortfolioBlockUM.Content, modifier: Modifier = Modifier) {
|
||||
FloatingCard(
|
||||
modifier = modifier,
|
||||
onClick = state.onClick,
|
||||
) {
|
||||
TangemRowContainer(
|
||||
contentPadding = PaddingValues(0.dp),
|
||||
) {
|
||||
CurrencyIcon(
|
||||
modifier = Modifier
|
||||
.padding(end = TangemTheme.dimens2.x3)
|
||||
.layoutId(TangemRowLayoutId.HEAD),
|
||||
state = state.tokenIcon,
|
||||
)
|
||||
|
||||
Text(
|
||||
modifier = Modifier.layoutId(TangemRowLayoutId.START_TOP),
|
||||
text = state.tokenName,
|
||||
style = TangemTheme.typography2.bodyMedium16,
|
||||
color = TangemTheme.colors2.text.neutral.primary,
|
||||
maxLines = 1,
|
||||
)
|
||||
|
||||
Text(
|
||||
modifier = Modifier.layoutId(TangemRowLayoutId.START_BOTTOM),
|
||||
text = stringResourceSafe(R.string.markets_portfolio_block_subtitle),
|
||||
style = TangemTheme.typography2.captionMedium12,
|
||||
color = TangemTheme.colors2.text.neutral.secondary,
|
||||
maxLines = 1,
|
||||
)
|
||||
|
||||
Text(
|
||||
modifier = Modifier.layoutId(TangemRowLayoutId.END_TOP),
|
||||
text = state.totalBalance.orMaskWithStars(state.isBalanceHidden).resolveAnnotatedReference(),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = TangemTheme.colors2.text.neutral.primary,
|
||||
style = TangemTheme.typography2.bodyMedium16,
|
||||
)
|
||||
|
||||
Text(
|
||||
modifier = Modifier.layoutId(TangemRowLayoutId.END_BOTTOM),
|
||||
text = state.tokenSymbol,
|
||||
style = TangemTheme.typography2.captionMedium12,
|
||||
color = TangemTheme.colors2.text.neutral.secondary,
|
||||
maxLines = 1,
|
||||
)
|
||||
|
||||
TangemButton(
|
||||
modifier = Modifier
|
||||
.padding(start = TangemTheme.dimens2.x3)
|
||||
.layoutId(TangemRowLayoutId.TAIL),
|
||||
buttonUM = TangemButtonUM(
|
||||
type = TangemButtonType.Secondary,
|
||||
iconRes = R.drawable.ic_chevron_24,
|
||||
shape = TangemButtonShape.Rounded,
|
||||
size = TangemButtonSize.X9,
|
||||
onClick = state.onClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AddTokenBlock(state: PortfolioBlockUM.AddToken, modifier: Modifier = Modifier) {
|
||||
FloatingCard(
|
||||
modifier = modifier,
|
||||
onClick = state.onClick,
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
CurrencyIcon(state.tokenIcon)
|
||||
|
||||
SpacerW(TangemTheme.dimens2.x3)
|
||||
|
||||
Text(
|
||||
text = formatAnnotatedWithBoldColor(
|
||||
rawString = stringResourceSafe(R.string.markets_portfolio_block_add_token_title),
|
||||
boldColor = TangemTheme.colors2.text.neutral.primary,
|
||||
),
|
||||
style = TangemTheme.typography2.captionMedium12,
|
||||
color = TangemTheme.colors2.text.neutral.secondary,
|
||||
modifier = Modifier.weight(1f),
|
||||
maxLines = 2,
|
||||
)
|
||||
|
||||
SpacerW(TangemTheme.dimens2.x2)
|
||||
|
||||
TangemButton(
|
||||
buttonUM = TangemButtonUM(
|
||||
type = TangemButtonType.Secondary,
|
||||
text = resourceReference(R.string.common_add),
|
||||
shape = TangemButtonShape.Rounded,
|
||||
size = TangemButtonSize.X9,
|
||||
onClick = state.onClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun FloatingCard(modifier: Modifier = Modifier, onClick: () -> Unit = {}, content: @Composable () -> Unit) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.navigationBarsPadding()
|
||||
.fillMaxWidth()
|
||||
.widthIn(max = BottomSheetDefaults.SheetMaxWidth)
|
||||
.padding(
|
||||
start = TangemTheme.dimens2.x2,
|
||||
end = TangemTheme.dimens2.x2,
|
||||
bottom = TangemTheme.dimens2.x2,
|
||||
)
|
||||
.clip(RoundedCornerShape(size = TangemTheme.dimens2.x5))
|
||||
.background(
|
||||
color = TangemTheme.colors2.surface.level3,
|
||||
shape = RoundedCornerShape(size = TangemTheme.dimens2.x5),
|
||||
)
|
||||
.clickable(onClick = onClick)
|
||||
.padding(horizontal = TangemTheme.dimens2.x4, vertical = TangemTheme.dimens2.x3),
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun ContentPreview() {
|
||||
TangemThemePreviewRedesign {
|
||||
PortfolioBlock(
|
||||
state = PortfolioBlockUM.Content(
|
||||
totalBalance = stringReference("$1,234.56"),
|
||||
tokensInPortfolioCount = 3,
|
||||
tokenIcon = previewCoinIcon,
|
||||
tokenName = "Bitcoin",
|
||||
tokenSymbol = "BTC",
|
||||
isBalanceHidden = false,
|
||||
onClick = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun AddTokenPreview() {
|
||||
TangemThemePreviewRedesign {
|
||||
PortfolioBlock(
|
||||
state = PortfolioBlockUM.AddToken(
|
||||
tokenIcon = previewCoinIcon,
|
||||
onClick = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val previewCoinIcon
|
||||
get() = CurrencyIconState.CoinIcon(
|
||||
url = null,
|
||||
fallbackResId = com.tangem.core.ui.R.drawable.img_polygon_22,
|
||||
isGrayscale = false,
|
||||
shouldShowCustomBadge = false,
|
||||
)
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolioblock.ui.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
@Immutable
|
||||
internal sealed class PortfolioBlockUM {
|
||||
|
||||
data object Loading : PortfolioBlockUM()
|
||||
data object Hidden : PortfolioBlockUM()
|
||||
|
||||
data class AddToken(
|
||||
val tokenIcon: CurrencyIconState,
|
||||
val onClick: () -> Unit,
|
||||
) : PortfolioBlockUM()
|
||||
|
||||
data class Content(
|
||||
val totalBalance: TextReference,
|
||||
val tokensInPortfolioCount: Int,
|
||||
val tokenIcon: CurrencyIconState,
|
||||
val tokenName: String,
|
||||
val tokenSymbol: String,
|
||||
val isBalanceHidden: Boolean,
|
||||
val onClick: () -> Unit,
|
||||
) : PortfolioBlockUM()
|
||||
}
|
||||
|
|
@ -12,16 +12,22 @@ import androidx.compose.foundation.text.TextAutoSize
|
|||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.SpacerH16
|
||||
import com.tangem.core.ui.components.SpacerH32
|
||||
import com.tangem.core.ui.components.SpacerH4
|
||||
|
|
@ -64,6 +70,7 @@ internal fun MarketsTokenDetailsContent(
|
|||
backgroundColor: Color,
|
||||
modifier: Modifier = Modifier,
|
||||
portfolioBlock: @Composable ((Modifier) -> Unit)?,
|
||||
portfolioFloatingBlock: @Composable ((Modifier) -> Unit)?,
|
||||
) {
|
||||
Content(
|
||||
contentPadding = contentPadding,
|
||||
|
|
@ -71,6 +78,7 @@ internal fun MarketsTokenDetailsContent(
|
|||
backgroundColor = backgroundColor,
|
||||
state = state,
|
||||
portfolioBlock = portfolioBlock,
|
||||
portfolioFloatingBlock = portfolioFloatingBlock,
|
||||
)
|
||||
|
||||
when (state.bottomSheetConfig.content) {
|
||||
|
|
@ -80,7 +88,7 @@ internal fun MarketsTokenDetailsContent(
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Suppress("LongParameterList", "LongMethod")
|
||||
@Composable
|
||||
private fun Content(
|
||||
contentPadding: PaddingValues,
|
||||
|
|
@ -88,6 +96,7 @@ private fun Content(
|
|||
backgroundColor: Color,
|
||||
modifier: Modifier = Modifier,
|
||||
portfolioBlock: @Composable ((Modifier) -> Unit)?,
|
||||
portfolioFloatingBlock: @Composable ((Modifier) -> Unit)?,
|
||||
) {
|
||||
val isRedesignEnabled = LocalRedesignEnabled.current
|
||||
val density = LocalDensity.current
|
||||
|
|
@ -97,54 +106,71 @@ private fun Content(
|
|||
lazyListState = lazyListState,
|
||||
onShouldShowPriceSubtitleChange = state.onShouldShowPriceSubtitleChange,
|
||||
)
|
||||
var bottomSpacing by remember { mutableStateOf(0.dp) }
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
Box(
|
||||
modifier
|
||||
.drawBehind { drawRect(backgroundColor) }
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
SpacerH4()
|
||||
Column {
|
||||
SpacerH4()
|
||||
|
||||
LazyColumn(
|
||||
state = lazyListState,
|
||||
contentPadding = PaddingValues(bottom = bottomBarHeight, top = contentPadding.calculateTopPadding()),
|
||||
) {
|
||||
item("header") {
|
||||
Header(
|
||||
state = state,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
item { SpacerH16() }
|
||||
item("intervalSelector") {
|
||||
IntervalSelector(
|
||||
trendInterval = state.selectedInterval,
|
||||
onIntervalClick = state.onSelectedIntervalChange,
|
||||
isEnabled = state.body !is MarketsTokenDetailsUM.Body.Nothing,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
item { SpacerH32() }
|
||||
item("chart") {
|
||||
MarketTokenDetailsChart(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
backgroundColor = backgroundColor,
|
||||
state = state.chartState,
|
||||
)
|
||||
}
|
||||
item { SpacerH16() }
|
||||
LazyColumn(
|
||||
state = lazyListState,
|
||||
contentPadding = PaddingValues(bottom = bottomBarHeight, top = contentPadding.calculateTopPadding()),
|
||||
) {
|
||||
item("header") {
|
||||
Header(
|
||||
state = state,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
item { SpacerH16() }
|
||||
item("intervalSelector") {
|
||||
IntervalSelector(
|
||||
trendInterval = state.selectedInterval,
|
||||
onIntervalClick = state.onSelectedIntervalChange,
|
||||
isEnabled = state.body !is MarketsTokenDetailsUM.Body.Nothing,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
item { SpacerH32() }
|
||||
item("chart") {
|
||||
MarketTokenDetailsChart(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
backgroundColor = backgroundColor,
|
||||
state = state.chartState,
|
||||
)
|
||||
}
|
||||
item { SpacerH16() }
|
||||
|
||||
tokenMarketDetailsBody(
|
||||
state = state.body,
|
||||
portfolioBlock = portfolioBlock,
|
||||
relatedNews = state.relatedNews,
|
||||
isRedesignEnabled = isRedesignEnabled,
|
||||
)
|
||||
tokenMarketDetailsBody(
|
||||
state = state.body,
|
||||
portfolioBlock = portfolioBlock,
|
||||
relatedNews = state.relatedNews,
|
||||
isRedesignEnabled = isRedesignEnabled,
|
||||
)
|
||||
|
||||
item { SpacerH(bottomSpacing) }
|
||||
}
|
||||
}
|
||||
|
||||
portfolioFloatingBlock?.invoke(
|
||||
Modifier
|
||||
.onSizeChanged { size ->
|
||||
bottomSpacing = if (size.height > 0) {
|
||||
with(density) { size.height.toDp() + 16.dp }
|
||||
} else {
|
||||
0.dp
|
||||
}
|
||||
}
|
||||
.align(Alignment.BottomCenter),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -404,6 +430,7 @@ private fun MarketsTokenDetailsContent_Preview(
|
|||
state = params,
|
||||
backgroundColor = TangemTheme.colors.background.tertiary,
|
||||
portfolioBlock = {},
|
||||
portfolioFloatingBlock = null,
|
||||
contentPadding = PaddingValues(),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue