Updated on 2026-08-14
This commit is contained in:
parent
0a6b47b668
commit
71038ca2d8
39 changed files with 1073 additions and 89 deletions
|
|
@ -142,6 +142,8 @@ dependencies {
|
|||
implementation(projects.features.pushNotifications.impl)
|
||||
implementation(projects.features.walletSettings.api)
|
||||
implementation(projects.features.walletSettings.impl)
|
||||
implementation(projects.features.markets.api)
|
||||
implementation(projects.features.markets.impl)
|
||||
|
||||
/** AndroidX libraries */
|
||||
implementation(deps.androidx.core.ktx)
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ private fun GiveTxPermissionBottomSheetContent(content: GiveTxPermissionBottomSh
|
|||
)
|
||||
Text(
|
||||
text = stringResource(
|
||||
id = R.string.give_permission_subtitle,
|
||||
id = R.string.give_permission_swap_subtitle,
|
||||
data.currency,
|
||||
),
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
|
|
|
|||
|
|
@ -40,15 +40,7 @@ inline fun <reified M : Model, reified P : Any> AppComponentContext.getOrCreateM
|
|||
val hiltComponent = hiltComponentBuilder
|
||||
.router(router)
|
||||
.uiMessageSender(messageSender)
|
||||
.let { builder ->
|
||||
if (params != null) {
|
||||
val container = MutableParamsContainer(params)
|
||||
|
||||
builder.paramsContainer(container)
|
||||
} else {
|
||||
builder
|
||||
}
|
||||
}
|
||||
.paramsContainer(MutableParamsContainer(params ?: Unit))
|
||||
.build()
|
||||
|
||||
EntryPoints.get(hiltComponent, ModelsEntryPoint::class.java)
|
||||
|
|
|
|||
|
|
@ -42,5 +42,9 @@
|
|||
{
|
||||
"name": "PUSH_NOTIFICATIONS_ENABLED",
|
||||
"version": "5.13.0"
|
||||
},
|
||||
{
|
||||
"name": "MARKETS_ENABLED",
|
||||
"version": "undefined"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -268,7 +268,8 @@
|
|||
<string name="give_permission_fee_footer">Комиссия сети за одобрение токена будет взиматься за подтверждение того, что именно вы разрешаете использовать ваш токен для обмена.</string>
|
||||
<string name="give_permission_policy_type_footer">Укажите лимит доступа к выбранному токену</string>
|
||||
<string name="give_permission_rows_amount">Количество %s</string>
|
||||
<string name="give_permission_subtitle">Чтобы продолжить, вам нужно разрешить смарт-контракту 1inch использовать ваш %s</string>
|
||||
<string name="give_permission_staking_footer">Функция подтверждения необходима для предоставления другому адресу разрешения на использование определенного количества ваших токенов.По замыслу смарт-контракты не могут получить доступ к вашим токенам, если вы не одобрите доступ со своей стороны. «Разблокируя» свои токены, вы даете смарт-контракту StakeKit разрешение использовать ваши активы. Майнеры сети получают компенсацию за газ (оплачиваемый вами) за запись этого действия в блокчейне. Как только разрешение будет предоставлено, вы сможете осуществить стейкинг токена.</string>
|
||||
<string name="give_permission_swap_subtitle" formatted="false">Чтобы продолжить, вам нужно разрешить смарт-контракту %1s использовать ваш %2s</string>
|
||||
<string name="give_permission_title">Дать разрешение</string>
|
||||
<string name="give_permission_unlimited">Безлимитно</string>
|
||||
<string name="home_button_order">Купить</string>
|
||||
|
|
|
|||
|
|
@ -271,7 +271,9 @@
|
|||
<string name="give_permission_fee_footer">The network will charge a token approval fee to verify that you are authorizing the use of your token for the swap.</string>
|
||||
<string name="give_permission_policy_type_footer">Specify the approve limit for the selected token</string>
|
||||
<string name="give_permission_rows_amount">Amount %s</string>
|
||||
<string name="give_permission_subtitle">To continue, grant 1inch smart contracts permission to use your %s</string>
|
||||
<string name="give_permission_staking_footer">The Approve function is needed to grant permission to another address to use a specific amount of your tokens. By design, smart contracts can\'t access your tokens unless you approve. By \"unlocking\" your tokens, you authorize the StakeKit smart contract to use them. The network\'s miners receive a gas fee (paid by you) to record this action on the blockchain. You can stake your token after giving approval.</string>
|
||||
<string name="give_permission_staking_subtitle">To continue you need to allow StakeKit smart contract to use your %s</string>
|
||||
<string name="give_permission_swap_subtitle" formatted="false">To continue, grant %1s smart contracts permission to use your %2s</string>
|
||||
<string name="give_permission_title">Give Permission</string>
|
||||
<string name="give_permission_unlimited">Unlimited</string>
|
||||
<string name="home_button_order">Order card</string>
|
||||
|
|
@ -327,15 +329,62 @@
|
|||
<string name="markets_add_to_my_portfolio_description">To start buying, exchanging or receiving this asset, add this token to at least 1 network</string>
|
||||
<string name="markets_add_to_my_portfolio_unavailable_description">This asset is not available</string>
|
||||
<string name="markets_add_to_portfolio_button">Add to portfolio</string>
|
||||
<string name="markets_available_networks">Available networks</string>
|
||||
<string name="markets_common_my_portfolio">My portfolio</string>
|
||||
<string name="markets_common_title">Market</string>
|
||||
<string name="markets_select_network">Select network</string>
|
||||
<string name="markets_select_wallet">Select wallet</string>
|
||||
<string name="markets_selector_interval_1m_title">1m</string>
|
||||
<string name="markets_selector_interval_1y_title">1y</string>
|
||||
<string name="markets_selector_interval_24h_title">24h</string>
|
||||
<string name="markets_selector_interval_3m_title">3m</string>
|
||||
<string name="markets_selector_interval_6m_title">6m</string>
|
||||
<string name="markets_selector_interval_7d_title">7d</string>
|
||||
<string name="markets_selector_interval_all_title">All</string>
|
||||
<string name="markets_sort_by_experienced_buyers_title">Experienced buyers</string>
|
||||
<string name="markets_sort_by_rating_title">Rating</string>
|
||||
<string name="markets_sort_by_title">Sort By</string>
|
||||
<string name="markets_sort_by_top_gainers_title">Top Gainers</string>
|
||||
<string name="markets_sort_by_top_losers_title">Top losers</string>
|
||||
<string name="markets_sort_by_trending_title">Trending</string>
|
||||
<plurals name="markets_token_details_based_on_ratings">
|
||||
<item quantity="one">Based on %d rating</item>
|
||||
<item quantity="other">Based on %d ratings</item>
|
||||
</plurals>
|
||||
<string name="markets_token_details_blockchain_site">Blockchain site</string>
|
||||
<string name="markets_token_details_buy_pressure">Buy pressure</string>
|
||||
<string name="markets_token_details_buy_pressure_description">The difference between buyers volume and sellers volume</string>
|
||||
<string name="markets_token_details_circulating_supply">Circulating supply</string>
|
||||
<string name="markets_token_details_circulating_supply_description">The total number of coins that are available for trading and are circulating in the market</string>
|
||||
<string name="markets_token_details_experienced_buyers">Experienced buyers</string>
|
||||
<string name="markets_token_details_experienced_buyers_description">Net buyers with the additional requirement of having at least 100 outgoing transactions</string>
|
||||
<string name="markets_token_details_fully_diluted_valuation">Fully diluted valuation</string>
|
||||
<string name="markets_token_details_fully_diluted_valuation_description">The total theoretical value of a cryptocurrency if all coins that could exist are in circulation, including those not currently circulating</string>
|
||||
<string name="markets_token_details_genesis_date">Genesis date</string>
|
||||
<string name="markets_token_details_high">High</string>
|
||||
<string name="markets_token_details_holders">Holders</string>
|
||||
<string name="markets_token_details_holders_description">The change in the number of token holders within a specific timeframe</string>
|
||||
<string name="markets_token_details_insights">Insights</string>
|
||||
<string name="markets_token_details_links">Links</string>
|
||||
<string name="markets_token_details_liquidity">Liquidity</string>
|
||||
<string name="markets_token_details_liquidity_description">The change in how much liquidity is available for the token during the specified timeframe</string>
|
||||
<string name="markets_token_details_liquidity_index">Liquidity index</string>
|
||||
<string name="markets_token_details_low">Low</string>
|
||||
<string name="markets_token_details_market_capitalization">Market capitalization</string>
|
||||
<string name="markets_token_details_market_capitalization_description">The total market value of a cryptocurrency, calculated by multiplying the current price of the coin by the total number of coins in circulation</string>
|
||||
<string name="markets_token_details_market_rating">Market rating</string>
|
||||
<string name="markets_token_details_market_rating_description">Position in crypto rating between all coins based on market capitalization</string>
|
||||
<string name="markets_token_details_max_supply">Max supply</string>
|
||||
<string name="markets_token_details_metrics">Metrics</string>
|
||||
<string name="markets_token_details_official_links">Official links</string>
|
||||
<string name="markets_token_details_price_performance">Price performance</string>
|
||||
<string name="markets_token_details_repository">Repository</string>
|
||||
<string name="markets_token_details_security_score">Security score</string>
|
||||
<string name="markets_token_details_social">Social</string>
|
||||
<string name="markets_token_details_total_supply">Total supply</string>
|
||||
<string name="markets_token_details_total_supply_description">The maximum number of coins or tokens that can ever exist for a particular cryptocurrency</string>
|
||||
<string name="markets_token_details_trading_volume">Trading volume (24h)</string>
|
||||
<string name="markets_token_details_trading_volume_24h_description">The total amount of a cryptocurrency that has been traded within the last 24 hours, indicating the level of activity and liquidity in the market</string>
|
||||
<string name="onboarding_access_code_feature_1_description">You have to set up a single access code to protect all your cards</string>
|
||||
<string name="onboarding_access_code_feature_1_title">Protect</string>
|
||||
<string name="onboarding_access_code_feature_2_description">You can set up an individual access code on each card later</string>
|
||||
|
|
@ -675,7 +724,7 @@
|
|||
<string name="twins_scan_twin_with_number">Tap the twin card with number %s and do not remove until the end of the operation</string>
|
||||
<string name="unlock_wallet_description_full">Use %s or scan a card to have an access to your wallet</string>
|
||||
<string name="user_push_notification_agreement_argument_one">Stay up to date with the latest features and news</string>
|
||||
<string name="user_push_notification_agreement_argument_two">Be able to get important market notifications</string>
|
||||
<string name="user_push_notification_agreement_argument_two">Be the first to know about new promotions</string>
|
||||
<string name="user_push_notification_agreement_header">Would you like to use\nPush-notifications?</string>
|
||||
<string name="user_wallet_list_add_button">Add new wallet</string>
|
||||
<string name="user_wallet_list_delete_prompt">Are you sure you want to delete this wallet?</string>
|
||||
|
|
|
|||
|
|
@ -17,10 +17,7 @@ import androidx.compose.ui.graphics.BlendMode
|
|||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import com.tangem.core.ui.res.LocalIsInDarkTheme
|
||||
import com.tangem.core.ui.res.TangemColorPalette
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.*
|
||||
import com.valentinilk.shimmer.*
|
||||
|
||||
/**
|
||||
|
|
@ -31,7 +28,7 @@ fun RectangleShimmer(modifier: Modifier = Modifier, radius: Dp = TangemTheme.dim
|
|||
Box(
|
||||
modifier = modifier
|
||||
.clip(RoundedCornerShape(size = radius))
|
||||
.shimmer(TangemShimmer),
|
||||
.shimmer(LocalTangemShimmer.current),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -44,11 +41,11 @@ fun CircleShimmer(modifier: Modifier = Modifier) {
|
|||
Box(
|
||||
modifier = modifier
|
||||
.clip(CircleShape)
|
||||
.shimmer(TangemShimmer),
|
||||
.shimmer(LocalTangemShimmer.current),
|
||||
)
|
||||
}
|
||||
|
||||
private val TangemShimmer: Shimmer
|
||||
internal val TangemShimmer: Shimmer
|
||||
@Composable
|
||||
get() = rememberShimmer(
|
||||
shimmerBounds = ShimmerBounds.View,
|
||||
|
|
|
|||
|
|
@ -4,9 +4,7 @@ import android.content.res.Configuration
|
|||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.Spring
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
|
|
@ -18,6 +16,7 @@ import androidx.compose.ui.graphics.Color
|
|||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
|
@ -57,12 +56,14 @@ inline fun <reified T> SegmentedButtons(
|
|||
val index = if (initialSelectedItem == null) 0 else config.indexOf(initialSelectedItem)
|
||||
mutableIntStateOf(index)
|
||||
}
|
||||
val shape = RoundedCornerShape(TangemTheme.dimens.radius26)
|
||||
|
||||
Row(
|
||||
modifier = modifier
|
||||
.clip(RoundedCornerShape(TangemTheme.dimens.radius26))
|
||||
.clip(shape)
|
||||
.background(dividerColor)
|
||||
.padding(TangemTheme.dimens.spacing1),
|
||||
.border(BorderStroke(1.dp, dividerColor), shape = shape)
|
||||
.height(IntrinsicSize.Max),
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing1),
|
||||
) {
|
||||
repeat(config.size) { index ->
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package com.tangem.core.ui.components.fields
|
|||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
|
|
@ -37,7 +37,7 @@ fun SearchBar(state: SearchBarUM, modifier: Modifier = Modifier, colors: TextFie
|
|||
TextField(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(TangemTheme.dimens.size48)
|
||||
.heightIn(min = TangemTheme.dimens.size48)
|
||||
.onFocusChanged {
|
||||
if (it.isFocused) {
|
||||
state.onActiveChange(true)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,5 @@ import androidx.compose.ui.Modifier
|
|||
fun interface ComposableContentComponent {
|
||||
|
||||
@Composable
|
||||
@Suppress("TopLevelComposableFunctions") // TODO: Remove this check
|
||||
fun Content(modifier: Modifier)
|
||||
}
|
||||
|
|
@ -9,6 +9,5 @@ interface ComposableDialogComponent {
|
|||
val doOnDismiss: () -> Unit
|
||||
|
||||
@Composable
|
||||
@Suppress("TopLevelComposableFunctions") // TODO: Remove this check
|
||||
fun Dialog()
|
||||
}
|
||||
|
|
@ -7,9 +7,11 @@ import androidx.compose.material3.SnackbarHostState
|
|||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
import com.tangem.core.ui.components.TangemShimmer
|
||||
import com.tangem.core.ui.haptic.HapticManager
|
||||
import com.tangem.core.ui.haptic.MockHapticManager
|
||||
import com.tangem.core.ui.windowsize.WindowSize
|
||||
import com.valentinilk.shimmer.Shimmer
|
||||
|
||||
@Composable
|
||||
fun TangemTheme(
|
||||
|
|
@ -48,6 +50,9 @@ fun TangemTheme(
|
|||
LocalHapticManager provides hapticManager,
|
||||
LocalSnackbarHostState provides snackbarHostState,
|
||||
LocalWindowSize provides windowSize,
|
||||
) {
|
||||
CompositionLocalProvider(
|
||||
LocalTangemShimmer provides TangemShimmer,
|
||||
) {
|
||||
ProvideTextStyle(
|
||||
value = TangemTheme.typography.body1,
|
||||
|
|
@ -55,6 +60,7 @@ fun TangemTheme(
|
|||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object TangemTheme {
|
||||
|
|
@ -228,3 +234,7 @@ val LocalSnackbarHostState = staticCompositionLocalOf<SnackbarHostState> {
|
|||
val LocalWindowSize = staticCompositionLocalOf<WindowSize> {
|
||||
error("No WindowSize provided")
|
||||
}
|
||||
|
||||
val LocalTangemShimmer = staticCompositionLocalOf<Shimmer> {
|
||||
error("No TangemShimmer provided")
|
||||
}
|
||||
|
|
@ -33,7 +33,6 @@ internal class PreviewDetailsComponent : DetailsComponent {
|
|||
)
|
||||
|
||||
@Composable
|
||||
@Suppress("TopLevelComposableFunctions") // TODO: Remove this check
|
||||
override fun Content(modifier: Modifier) {
|
||||
DetailsScreen(
|
||||
modifier = modifier,
|
||||
|
|
|
|||
|
|
@ -45,7 +45,6 @@ internal class PreviewUserWalletListComponent : UserWalletListComponent {
|
|||
)
|
||||
|
||||
@Composable
|
||||
@Suppress("TopLevelComposableFunctions") // TODO: Remove this check
|
||||
override fun Content(modifier: Modifier) {
|
||||
UserWalletListBlock(state = previewState, modifier = modifier)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
package com.tangem.features.markets
|
||||
|
||||
interface MarketsFeatureToggles {
|
||||
val isFeatureEnabled: Boolean
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
package com.tangem.features.markets
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
|
||||
@Stable
|
||||
interface MarketsListComponent {
|
||||
|
||||
// TODO
|
||||
|
||||
// @Composable
|
||||
// fun BottomSheetContent(state: State<BottomSheetState>, onHeaderSizeChange: (Dp) -> Unit, modifier: Modifier)
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.features.markets
|
||||
package com.tangem.features.markets.component
|
||||
|
||||
enum class BottomSheetState {
|
||||
EXPANDED,
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package com.tangem.features.markets.component
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
|
||||
@Stable
|
||||
interface MarketsListComponent {
|
||||
|
||||
@Composable
|
||||
fun BottomSheetContent(
|
||||
bottomSheetState: State<BottomSheetState>,
|
||||
onHeaderSizeChange: (Dp) -> Unit,
|
||||
modifier: Modifier,
|
||||
)
|
||||
|
||||
interface Factory {
|
||||
fun create(context: AppComponentContext): MarketsListComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
alias(deps.plugins.kotlin.kapt)
|
||||
alias(deps.plugins.kotlin.serialization)
|
||||
alias(deps.plugins.hilt.android)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
|
|
@ -9,16 +12,31 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
/* Project - API */
|
||||
api(projects.features.markets.api)
|
||||
|
||||
/* Compose */
|
||||
implementation(deps.compose.coil)
|
||||
implementation(deps.compose.foundation)
|
||||
implementation(deps.compose.material3)
|
||||
implementation(deps.compose.ui)
|
||||
implementation(deps.compose.ui.tooling)
|
||||
implementation(deps.compose.ui.utils)
|
||||
implementation(deps.lifecycle.compose)
|
||||
|
||||
/* DI */
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
|
||||
/* Other */
|
||||
implementation(deps.kotlin.immutable.collections)
|
||||
implementation(deps.timber)
|
||||
|
||||
/* Core */
|
||||
implementation(projects.core.decompose)
|
||||
implementation(projects.core.ui)
|
||||
implementation(projects.core.featuretoggles)
|
||||
|
||||
implementation(projects.common.ui)
|
||||
implementation(projects.common.uiCharts)
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.features.markets
|
||||
|
||||
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
|
||||
|
||||
internal class DefaultMarketsFeatureToggles(
|
||||
private val featureTogglesManager: FeatureTogglesManager,
|
||||
) : MarketsFeatureToggles {
|
||||
|
||||
override val isFeatureEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled("MARKETS_ENABLED")
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package com.tangem.features.markets.component.impl
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.features.markets.component.BottomSheetState
|
||||
import com.tangem.features.markets.component.MarketsListComponent
|
||||
import com.tangem.features.markets.model.MarketsListModel
|
||||
import com.tangem.features.markets.ui.MarketsList
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
internal class DefaultMarketsListComponent @AssistedInject constructor(
|
||||
@Assisted context: AppComponentContext,
|
||||
) : MarketsListComponent, AppComponentContext by context {
|
||||
|
||||
private val model: MarketsListModel = getOrCreateModel()
|
||||
|
||||
@Composable
|
||||
override fun BottomSheetContent(
|
||||
bottomSheetState: State<BottomSheetState>,
|
||||
onHeaderSizeChange: (Dp) -> Unit,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
val state by model.state.collectAsStateWithLifecycle()
|
||||
|
||||
MarketsList(
|
||||
onHeaderSizeChange = onHeaderSizeChange,
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : MarketsListComponent.Factory {
|
||||
override fun create(context: AppComponentContext): DefaultMarketsListComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.features.markets.di
|
||||
|
||||
import com.tangem.features.markets.component.MarketsListComponent
|
||||
import com.tangem.features.markets.component.impl.DefaultMarketsListComponent
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface ComponentModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindMarketsListComponent(factory: DefaultMarketsListComponent.Factory): MarketsListComponent.Factory
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.features.markets.di
|
||||
|
||||
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
|
||||
import com.tangem.features.markets.DefaultMarketsFeatureToggles
|
||||
import com.tangem.features.markets.MarketsFeatureToggles
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object FeatureModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideFeatureToggles(featureTogglesManager: FeatureTogglesManager): MarketsFeatureToggles =
|
||||
DefaultMarketsFeatureToggles(featureTogglesManager = featureTogglesManager)
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.features.markets.di
|
||||
|
||||
import com.tangem.core.decompose.di.DecomposeComponent
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.features.markets.model.MarketsListModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.multibindings.ClassKey
|
||||
import dagger.multibindings.IntoMap
|
||||
|
||||
@Module
|
||||
@InstallIn(DecomposeComponent::class)
|
||||
internal interface ModelModule {
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(MarketsListModel::class)
|
||||
fun provideMarketsListModel(model: MarketsListModel): Model
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package com.tangem.features.markets.model
|
||||
|
||||
import com.tangem.core.decompose.di.ComponentScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.features.markets.impl.R
|
||||
import com.tangem.features.markets.ui.entity.ListUM
|
||||
import com.tangem.features.markets.ui.entity.MarketsListUM
|
||||
import com.tangem.features.markets.ui.entity.SortByTypeUM
|
||||
import com.tangem.features.markets.ui.preview.MarketChartListItemPreviewDataProvider
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@ComponentScoped
|
||||
internal class MarketsListModel @Inject constructor(
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
) : Model() {
|
||||
|
||||
val state = MutableStateFlow(
|
||||
MarketsListUM(
|
||||
list = ListUM.Loading,
|
||||
searchBar = SearchBarUM(
|
||||
placeholderText = resourceReference(R.string.manage_tokens_search_placeholder),
|
||||
query = "",
|
||||
onQueryChange = {},
|
||||
isActive = false,
|
||||
onActiveChange = { },
|
||||
),
|
||||
selectedSortBy = SortByTypeUM.Rating,
|
||||
selectedInterval = MarketsListUM.TrendInterval.H24,
|
||||
onIntervalClick = {},
|
||||
onSortByButtonClick = {},
|
||||
),
|
||||
)
|
||||
|
||||
init {
|
||||
modelScope.launch {
|
||||
delay(timeMillis = 5000)
|
||||
state.update {
|
||||
it.copy(
|
||||
list = ListUM.Content(
|
||||
items = MarketChartListItemPreviewDataProvider().values
|
||||
.flatMap { item -> List(size = 10) { item } }
|
||||
.mapIndexed { index, item ->
|
||||
item.copy(id = index.toString())
|
||||
}
|
||||
.toImmutableList(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO
|
||||
}
|
||||
|
|
@ -0,0 +1,217 @@
|
|||
package com.tangem.features.markets.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.SpacerH12
|
||||
import com.tangem.core.ui.components.buttons.SecondarySmallButton
|
||||
import com.tangem.core.ui.components.buttons.SmallButtonConfig
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
|
||||
import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons
|
||||
import com.tangem.core.ui.components.fields.SearchBar
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
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.markets.impl.R
|
||||
import com.tangem.features.markets.ui.components.MarketsListItem
|
||||
import com.tangem.features.markets.ui.components.MarketsListItemPlaceholder
|
||||
import com.tangem.features.markets.ui.entity.ListUM
|
||||
import com.tangem.features.markets.ui.entity.MarketsListUM
|
||||
import com.tangem.features.markets.ui.entity.SortByTypeUM
|
||||
import com.tangem.features.markets.ui.preview.MarketChartListItemPreviewDataProvider
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@Composable
|
||||
internal fun MarketsList(state: MarketsListUM, onHeaderSizeChange: (Dp) -> Unit, modifier: Modifier = Modifier) {
|
||||
Content(
|
||||
modifier = modifier,
|
||||
state = state,
|
||||
onHeaderSizeChange = onHeaderSizeChange,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Content(state: MarketsListUM, onHeaderSizeChange: (Dp) -> Unit, modifier: Modifier = Modifier) {
|
||||
val density = LocalDensity.current
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.imePadding()
|
||||
.background(color = TangemTheme.colors.background.primary),
|
||||
) {
|
||||
SearchBar(
|
||||
state = state.searchBar,
|
||||
modifier = Modifier
|
||||
.background(color = TangemTheme.colors.background.primary)
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing16,
|
||||
end = TangemTheme.dimens.spacing16,
|
||||
bottom = TangemTheme.dimens.spacing4,
|
||||
)
|
||||
.onGloballyPositioned {
|
||||
with(density) { onHeaderSizeChange(it.size.height.toDp()) }
|
||||
},
|
||||
)
|
||||
Spacer(Modifier.height(TangemTheme.dimens.spacing20))
|
||||
Column(Modifier.padding(horizontal = TangemTheme.dimens.size16)) {
|
||||
Title()
|
||||
SpacerH12()
|
||||
Options(
|
||||
sortByTypeUM = state.selectedSortBy,
|
||||
trendInterval = state.selectedInterval,
|
||||
onIntervalClick = state.onIntervalClick,
|
||||
onSortByClick = state.onSortByButtonClick,
|
||||
)
|
||||
}
|
||||
SpacerH12()
|
||||
Items(state = state.list)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Title(modifier: Modifier = Modifier) {
|
||||
Text(
|
||||
modifier = modifier,
|
||||
text = stringResource(id = R.string.markets_common_title),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Options(
|
||||
sortByTypeUM: SortByTypeUM,
|
||||
trendInterval: MarketsListUM.TrendInterval,
|
||||
onSortByClick: () -> Unit,
|
||||
onIntervalClick: (MarketsListUM.TrendInterval) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.height(IntrinsicSize.Max)
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
SecondarySmallButton(
|
||||
config = SmallButtonConfig(
|
||||
text = sortByTypeUM.text,
|
||||
onClick = onSortByClick,
|
||||
icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_chevron_24),
|
||||
),
|
||||
)
|
||||
SegmentedButtons(
|
||||
config = persistentListOf(
|
||||
MarketsListUM.TrendInterval.H24,
|
||||
MarketsListUM.TrendInterval.D7,
|
||||
MarketsListUM.TrendInterval.M1,
|
||||
),
|
||||
initialSelectedItem = trendInterval,
|
||||
onClick = onIntervalClick,
|
||||
modifier = Modifier
|
||||
.width(160.dp)
|
||||
.fillMaxHeight(),
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.align(Alignment.Center)
|
||||
.padding(
|
||||
vertical = TangemTheme.dimens.spacing4,
|
||||
),
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
text = it.text.resolveReference(),
|
||||
style = TangemTheme.typography.caption1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Items(state: ListUM, modifier: Modifier = Modifier) {
|
||||
val lazyListState = rememberLazyListState()
|
||||
val scrollEnabled = state !is ListUM.Loading
|
||||
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
|
||||
|
||||
LazyColumn(
|
||||
modifier = modifier,
|
||||
state = lazyListState,
|
||||
contentPadding = PaddingValues(bottom = bottomBarHeight),
|
||||
userScrollEnabled = scrollEnabled,
|
||||
) {
|
||||
when (state) {
|
||||
ListUM.Loading -> {
|
||||
items(count = 50, key = { it }) {
|
||||
MarketsListItemPlaceholder()
|
||||
}
|
||||
}
|
||||
ListUM.SearchNothingFound -> {
|
||||
// TODO
|
||||
}
|
||||
is ListUM.Content -> {
|
||||
items(
|
||||
items = state.items,
|
||||
key = { it.id },
|
||||
) { item ->
|
||||
MarketsListItem(
|
||||
model = item,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//region: Preview
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview() {
|
||||
TangemThemePreview {
|
||||
MarketsList(
|
||||
state = MarketsListUM(
|
||||
list = ListUM.Content(
|
||||
items = MarketChartListItemPreviewDataProvider().values
|
||||
.flatMap { item -> List(size = 10) { item } }
|
||||
.mapIndexed { index, item ->
|
||||
item.copy(id = index.toString())
|
||||
}
|
||||
.toImmutableList(),
|
||||
),
|
||||
searchBar = SearchBarUM(
|
||||
placeholderText = resourceReference(R.string.manage_tokens_search_placeholder),
|
||||
query = "",
|
||||
onQueryChange = {},
|
||||
isActive = false,
|
||||
onActiveChange = { },
|
||||
),
|
||||
selectedSortBy = SortByTypeUM.Rating,
|
||||
selectedInterval = MarketsListUM.TrendInterval.H24,
|
||||
onIntervalClick = {},
|
||||
onSortByButtonClick = {},
|
||||
),
|
||||
onHeaderSizeChange = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
//endregion: Preview
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.features.markets.ui
|
||||
package com.tangem.features.markets.ui.components
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.Animatable
|
||||
|
|
@ -45,7 +45,7 @@ import com.tangem.core.ui.res.TangemTheme
|
|||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.windowsize.WindowSizeType
|
||||
import com.tangem.features.markets.impl.R
|
||||
import com.tangem.features.markets.ui.models.MarketsListItemModel
|
||||
import com.tangem.features.markets.ui.entity.MarketsListItemUM
|
||||
import com.tangem.features.markets.ui.preview.MarketChartListItemPreviewDataProvider
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
|
|
@ -63,7 +63,7 @@ const val SWIPE_VELOCITY_THRESHOLD = 20f
|
|||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun MarketsListItem(
|
||||
model: MarketsListItemModel,
|
||||
model: MarketsListItemUM,
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: () -> Unit = {},
|
||||
onSwipeToAction: () -> Unit = {},
|
||||
|
|
@ -184,7 +184,7 @@ fun MarketsListItem(
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun MarketsListItemContent(model: MarketsListItemModel, modifier: Modifier = Modifier) {
|
||||
private fun MarketsListItemContent(model: MarketsListItemUM, modifier: Modifier = Modifier) {
|
||||
val windowSize = LocalWindowSize.current
|
||||
|
||||
Row(
|
||||
|
|
@ -303,7 +303,7 @@ private fun RowScope.TokenRatingPlace(ratingPosition: String?) {
|
|||
color = TangemTheme.colors.field.primary,
|
||||
shape = TangemTheme.shapes.roundedCornersSmall2,
|
||||
)
|
||||
.padding(horizontal = TangemTheme.dimens.size5), // TODO check
|
||||
.padding(horizontal = TangemTheme.dimens.spacing5),
|
||||
) {
|
||||
Text(
|
||||
text = ratingPosition ?: "-",
|
||||
|
|
@ -379,8 +379,9 @@ private fun Chart(chartType: MarketChartLook.Type, chartRawData: MarketChartRawD
|
|||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(TangemTheme.dimens.size15) // TODO check
|
||||
.height(TangemTheme.dimens.size12)
|
||||
.align(Alignment.Center),
|
||||
radius = TangemTheme.dimens.radius3,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -391,7 +392,7 @@ private fun Chart(chartType: MarketChartLook.Type, chartRawData: MarketChartRawD
|
|||
@Preview(showBackground = true, widthDp = 360, name = "normal night", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Preview(showBackground = true, widthDp = 320, name = "small width")
|
||||
@Composable
|
||||
private fun Preview(@PreviewParameter(MarketChartListItemPreviewDataProvider::class) state: MarketsListItemModel) {
|
||||
private fun Preview(@PreviewParameter(MarketChartListItemPreviewDataProvider::class) state: MarketsListItemUM) {
|
||||
TangemThemePreview {
|
||||
var state1 by remember { mutableStateOf(state) }
|
||||
var state2 by remember { mutableStateOf(state) }
|
||||
|
|
@ -435,13 +436,13 @@ private fun Preview(@PreviewParameter(MarketChartListItemPreviewDataProvider::cl
|
|||
}
|
||||
}
|
||||
state1 = state1.copy(
|
||||
price = MarketsListItemModel.Price(
|
||||
price = MarketsListItemUM.Price(
|
||||
text = "0.${prices[0].first}023 $",
|
||||
changeType = prices[0].second,
|
||||
),
|
||||
)
|
||||
state2 = state2.copy(
|
||||
price = MarketsListItemModel.Price(
|
||||
price = MarketsListItemUM.Price(
|
||||
text = "0.${prices[1].first}023 $",
|
||||
changeType = prices[1].second,
|
||||
),
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
package com.tangem.features.markets.ui.components
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.tangem.core.ui.components.*
|
||||
import com.tangem.core.ui.res.LocalWindowSize
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.windowsize.WindowSizeType
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
fun MarketsListItemPlaceholder() {
|
||||
val density = LocalDensity.current
|
||||
val windowSize = LocalWindowSize.current
|
||||
val sp12 = with(density) { 12.sp.toDp() }
|
||||
|
||||
Row(
|
||||
modifier = Modifier.padding(
|
||||
horizontal = TangemTheme.dimens.spacing16,
|
||||
vertical = TangemTheme.dimens.spacing15,
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
CircleShimmer(Modifier.size(TangemTheme.dimens.size36))
|
||||
|
||||
SpacerW12()
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = TangemTheme.dimens.spacing4),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.width(TangemTheme.dimens.size70)
|
||||
.height(sp12),
|
||||
radius = TangemTheme.dimens.radius3,
|
||||
)
|
||||
SpacerW8()
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.width(TangemTheme.dimens.size70)
|
||||
.height(sp12),
|
||||
radius = TangemTheme.dimens.radius3,
|
||||
)
|
||||
}
|
||||
|
||||
SpacerH(height = TangemTheme.dimens.spacing2)
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = TangemTheme.dimens.spacing2),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.Bottom,
|
||||
) {
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.width(TangemTheme.dimens.size52)
|
||||
.height(sp12),
|
||||
radius = TangemTheme.dimens.radius3,
|
||||
)
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.width(TangemTheme.dimens.size52)
|
||||
.height(sp12),
|
||||
radius = TangemTheme.dimens.radius3,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (windowSize.widthAtLeast(WindowSizeType.Small)) {
|
||||
Spacer(Modifier.width(TangemTheme.dimens.spacing10))
|
||||
|
||||
Box {
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.width(TangemTheme.dimens.size56)
|
||||
.height(TangemTheme.dimens.size12),
|
||||
radius = TangemTheme.dimens.radius3,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360, name = "normal")
|
||||
@Preview(showBackground = true, widthDp = 360, name = "normal night", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Preview(showBackground = true, widthDp = 320, name = "small width")
|
||||
@Composable
|
||||
private fun Preview() {
|
||||
TangemThemePreview {
|
||||
Column(Modifier.background(TangemTheme.colors.background.tertiary)) {
|
||||
repeat(20) {
|
||||
MarketsListItemPlaceholder()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package com.tangem.features.markets.ui.entity
|
||||
|
||||
sealed class ListItem
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.features.markets.ui.models
|
||||
package com.tangem.features.markets.ui.entity
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.common.ui.charts.state.MarketChartLook
|
||||
|
|
@ -6,7 +6,7 @@ import com.tangem.common.ui.charts.state.MarketChartRawData
|
|||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
|
||||
@Immutable
|
||||
data class MarketsListItemModel(
|
||||
data class MarketsListItemUM(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val currencySymbol: String,
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
package com.tangem.features.markets.ui.entity
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.features.markets.impl.R
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
internal data class MarketsListUM(
|
||||
val list: ListUM,
|
||||
val searchBar: SearchBarUM,
|
||||
val selectedSortBy: SortByTypeUM,
|
||||
val selectedInterval: TrendInterval,
|
||||
val onIntervalClick: (TrendInterval) -> Unit,
|
||||
val onSortByButtonClick: () -> Unit,
|
||||
) {
|
||||
enum class TrendInterval(val text: TextReference) {
|
||||
H24(resourceReference(R.string.markets_selector_interval_24h_title)),
|
||||
D7(resourceReference(R.string.markets_selector_interval_7d_title)),
|
||||
M1(resourceReference(R.string.markets_selector_interval_1m_title)),
|
||||
}
|
||||
}
|
||||
|
||||
enum class SortByTypeUM(val text: TextReference) {
|
||||
Rating(resourceReference(R.string.markets_sort_by_rating_title)),
|
||||
Trending(resourceReference(R.string.markets_sort_by_trending_title)),
|
||||
ExperiencedBuyers(resourceReference(R.string.markets_sort_by_experienced_buyers_title)),
|
||||
TopGainers(resourceReference(R.string.markets_sort_by_top_gainers_title)),
|
||||
TopLosers(resourceReference(R.string.markets_sort_by_top_losers_title)),
|
||||
}
|
||||
|
||||
@Immutable
|
||||
sealed class ListUM {
|
||||
|
||||
data class Content(
|
||||
val items: ImmutableList<MarketsListItemUM>,
|
||||
) : ListUM()
|
||||
|
||||
data object Loading : ListUM()
|
||||
data object SearchNothingFound : ListUM()
|
||||
}
|
||||
|
|
@ -4,86 +4,86 @@ package com.tangem.features.markets.ui.preview
|
|||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import com.tangem.common.ui.charts.state.MarketChartRawData
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.features.markets.ui.models.MarketsListItemModel
|
||||
import com.tangem.features.markets.ui.entity.MarketsListItemUM
|
||||
|
||||
internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParameterProvider<MarketsListItemModel>(
|
||||
internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParameterProvider<MarketsListItemUM>(
|
||||
collection = listOf(
|
||||
MarketsListItemModel(
|
||||
MarketsListItemUM(
|
||||
id = "1",
|
||||
name = "Bitcoin",
|
||||
currencySymbol = "BTC",
|
||||
iconUrl = "",
|
||||
ratingPosition = "10",
|
||||
marketCap = "$6.233 B",
|
||||
price = MarketsListItemModel.Price(text = "31 285.72$"),
|
||||
price = MarketsListItemUM.Price(text = "31 285.72$"),
|
||||
trendPercentText = "12.43%",
|
||||
trendType = PriceChangeType.UP,
|
||||
chardData = MarketChartRawData(
|
||||
y = listOf(0.4f, 0.2f, 0.4f, 0.1f, 0.4f, 2f, 5f, 0.1f, 2f, 2f, 3f),
|
||||
),
|
||||
),
|
||||
MarketsListItemModel(
|
||||
MarketsListItemUM(
|
||||
id = "1",
|
||||
name = "Bitcoin",
|
||||
currencySymbol = "BTC",
|
||||
iconUrl = null,
|
||||
ratingPosition = "10",
|
||||
marketCap = "$6.233 B",
|
||||
price = MarketsListItemModel.Price(text = "31 285.72$"),
|
||||
price = MarketsListItemUM.Price(text = "31 285.72$"),
|
||||
trendPercentText = "12.43%",
|
||||
trendType = PriceChangeType.NEUTRAL,
|
||||
chardData = null,
|
||||
),
|
||||
MarketsListItemModel(
|
||||
MarketsListItemUM(
|
||||
id = "1",
|
||||
name = "Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin",
|
||||
currencySymbol = "BTC",
|
||||
iconUrl = null,
|
||||
ratingPosition = "10",
|
||||
marketCap = "$6.23348172384781234 B",
|
||||
price = MarketsListItemModel.Price(text = "31 285.72$"),
|
||||
price = MarketsListItemUM.Price(text = "31 285.72$"),
|
||||
trendPercentText = "12.43%",
|
||||
trendType = PriceChangeType.DOWN,
|
||||
chardData = MarketChartRawData(
|
||||
y = listOf(0.4f, 0.2f, 0.4f, 0.1f, 0.4f, 2f, 5f, 0.1f, 2f, 2f, 3f),
|
||||
),
|
||||
),
|
||||
MarketsListItemModel(
|
||||
MarketsListItemUM(
|
||||
id = "1",
|
||||
name = "Bitcoin",
|
||||
currencySymbol = "BTC",
|
||||
iconUrl = null,
|
||||
ratingPosition = "10",
|
||||
marketCap = null,
|
||||
price = MarketsListItemModel.Price(text = "31 285.72$"),
|
||||
price = MarketsListItemUM.Price(text = "31 285.72$"),
|
||||
trendPercentText = "12.43%",
|
||||
trendType = PriceChangeType.UP,
|
||||
chardData = MarketChartRawData(
|
||||
y = listOf(0.4f, 0.2f, 0.4f, 0.1f, 0.4f, 2f, 5f, 0.1f, 2f, 2f, 3f),
|
||||
),
|
||||
),
|
||||
MarketsListItemModel(
|
||||
MarketsListItemUM(
|
||||
id = "1",
|
||||
name = "Bitcoin",
|
||||
currencySymbol = "BTC",
|
||||
iconUrl = null,
|
||||
ratingPosition = null,
|
||||
marketCap = "$6.233 B",
|
||||
price = MarketsListItemModel.Price(text = "31 285.72$"),
|
||||
price = MarketsListItemUM.Price(text = "31 285.72$"),
|
||||
trendPercentText = "12.43%",
|
||||
trendType = PriceChangeType.UP,
|
||||
chardData = MarketChartRawData(
|
||||
y = listOf(0.4f, 0.2f, 0.4f, 0.1f, 0.4f, 2f, 5f, 0.1f, 2f, 2f, 3f),
|
||||
),
|
||||
),
|
||||
MarketsListItemModel(
|
||||
MarketsListItemUM(
|
||||
id = "1",
|
||||
name = "Bitcoin",
|
||||
currencySymbol = "BTC",
|
||||
iconUrl = null,
|
||||
ratingPosition = null,
|
||||
marketCap = null,
|
||||
price = MarketsListItemModel.Price(text = "31 285.72$"),
|
||||
price = MarketsListItemUM.Price(text = "31 285.72$"),
|
||||
trendPercentText = "12.43%",
|
||||
trendType = PriceChangeType.UP,
|
||||
chardData = MarketChartRawData(
|
||||
|
|
|
|||
|
|
@ -1407,7 +1407,7 @@ internal class StateBuilder(
|
|||
return NotificationConfig(
|
||||
title = resourceReference(R.string.express_provider_permission_needed),
|
||||
subtitle = resourceReference(
|
||||
id = R.string.give_permission_subtitle,
|
||||
id = R.string.give_permission_swap_subtitle,
|
||||
formatArgs = wrappedList(fromTokenSymbol),
|
||||
),
|
||||
iconResId = R.drawable.ic_locked_24,
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ dependencies {
|
|||
implementation(projects.common.routing)
|
||||
implementation(projects.core.deepLinks)
|
||||
implementation(projects.core.deepLinks.global)
|
||||
implementation(projects.core.decompose)
|
||||
|
||||
implementation(projects.libs.crypto)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,18 @@
|
|||
package com.tangem.feature.wallet.presentation
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.arkivanov.decompose.defaultComponentContext
|
||||
import com.tangem.core.decompose.context.DefaultAppComponentContext
|
||||
import com.tangem.core.decompose.di.DecomposeComponent
|
||||
import com.tangem.core.ui.UiDependencies
|
||||
import com.tangem.core.ui.screen.ComposeFragment
|
||||
import com.tangem.feature.wallet.presentation.router.InnerWalletRouter
|
||||
import com.tangem.features.markets.MarketsFeatureToggles
|
||||
import com.tangem.features.markets.component.MarketsListComponent
|
||||
import com.tangem.features.wallet.navigation.WalletRouter
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -24,15 +31,45 @@ internal class WalletFragment : ComposeFragment() {
|
|||
@Inject
|
||||
internal lateinit var walletRouter: WalletRouter
|
||||
|
||||
@Inject
|
||||
internal lateinit var marketsListComponentFactory: MarketsListComponent.Factory
|
||||
|
||||
@Inject
|
||||
internal lateinit var coroutineDispatcherProvider: CoroutineDispatcherProvider
|
||||
|
||||
@Inject
|
||||
internal lateinit var componentBuilder: DecomposeComponent.Builder
|
||||
|
||||
@Inject
|
||||
internal lateinit var marketsFeatureToggles: MarketsFeatureToggles
|
||||
|
||||
private var marketsListComponent: MarketsListComponent? = null
|
||||
|
||||
private val _walletRouter: InnerWalletRouter
|
||||
get() = requireNotNull(walletRouter as? InnerWalletRouter) {
|
||||
"_walletRouter should be instance of InnerWalletRouter"
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
if (marketsFeatureToggles.isFeatureEnabled) {
|
||||
val appContext = DefaultAppComponentContext(
|
||||
componentContext = defaultComponentContext(requireActivity().onBackPressedDispatcher),
|
||||
messageHandler = uiDependencies.eventMessageHandler,
|
||||
dispatchers = coroutineDispatcherProvider,
|
||||
hiltComponentBuilder = componentBuilder,
|
||||
)
|
||||
|
||||
marketsListComponent = marketsListComponentFactory.create(appContext)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun ScreenContent(modifier: Modifier) {
|
||||
_walletRouter.Initialize(
|
||||
onFinish = requireActivity()::finish,
|
||||
marketsListComponent = marketsListComponent,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensScree
|
|||
import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensViewModel
|
||||
import com.tangem.feature.wallet.presentation.wallet.ui.WalletScreen
|
||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletViewModel
|
||||
import com.tangem.features.markets.component.MarketsListComponent
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
/** Default implementation of wallet feature router */
|
||||
|
|
@ -40,7 +41,7 @@ internal class DefaultWalletRouter(
|
|||
override fun getEntryFragment(): Fragment = WalletFragment.create()
|
||||
|
||||
@Composable
|
||||
override fun Initialize(onFinish: () -> Unit) {
|
||||
override fun Initialize(onFinish: () -> Unit, marketsListComponent: MarketsListComponent?) {
|
||||
this.onFinish = onFinish
|
||||
|
||||
NavHost(
|
||||
|
|
@ -55,6 +56,7 @@ internal class DefaultWalletRouter(
|
|||
|
||||
WalletScreen(
|
||||
state = viewModel.uiState.collectAsStateWithLifecycle().value,
|
||||
marketsListComponent = marketsListComponent,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.features.markets.component.MarketsListComponent
|
||||
import com.tangem.features.wallet.navigation.WalletRouter
|
||||
|
||||
/**
|
||||
|
|
@ -22,9 +23,8 @@ internal interface InnerWalletRouter : WalletRouter {
|
|||
*
|
||||
* @param onFinish finish activity callback
|
||||
*/
|
||||
@Suppress("TopLevelComposableFunctions")
|
||||
@Composable
|
||||
fun Initialize(onFinish: () -> Unit)
|
||||
fun Initialize(onFinish: () -> Unit, marketsListComponent: MarketsListComponent?)
|
||||
|
||||
/** Pop back stack */
|
||||
fun popBackStack()
|
||||
|
|
|
|||
|
|
@ -2,6 +2,11 @@ package com.tangem.feature.wallet.presentation.wallet.ui
|
|||
|
||||
import android.content.res.Configuration
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.core.TweenSpec
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
|
|
@ -10,27 +15,36 @@ import androidx.compose.foundation.lazy.rememberLazyListState
|
|||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.pullrefresh.pullRefresh
|
||||
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
||||
import androidx.compose.material3.FabPosition
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.luminance
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.clearAndSetSemantics
|
||||
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 androidx.compose.ui.unit.dp
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
import com.tangem.core.ui.components.BottomFade
|
||||
import com.tangem.core.ui.components.Keyboard
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.components.atoms.Hand
|
||||
import com.tangem.core.ui.components.atoms.handComposableComponentHeight
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig
|
||||
import com.tangem.core.ui.components.keyboardAsState
|
||||
import com.tangem.core.ui.components.snackbar.CopiedTextSnackbar
|
||||
import com.tangem.core.ui.components.snackbar.TangemSnackbar
|
||||
import com.tangem.core.ui.components.transactions.state.TxHistoryState
|
||||
|
|
@ -55,10 +69,13 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.VisaTxDe
|
|||
import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.balancesAndLimitsBlock
|
||||
import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.depositButton
|
||||
import com.tangem.feature.wallet.presentation.wallet.ui.utils.changeWalletAnimator
|
||||
import com.tangem.features.markets.component.BottomSheetState
|
||||
import com.tangem.features.markets.component.MarketsListComponent
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
internal fun WalletScreen(state: WalletScreenState) {
|
||||
internal fun WalletScreen(state: WalletScreenState, marketsListComponent: MarketsListComponent?) {
|
||||
BackHandler(onBack = state.onBackClick)
|
||||
|
||||
// It means that screen is still initializing
|
||||
|
|
@ -81,6 +98,8 @@ internal fun WalletScreen(state: WalletScreenState) {
|
|||
snackbarHostState = snackbarHostState,
|
||||
isAutoScroll = isAutoScroll,
|
||||
onAutoScrollReset = { isAutoScroll.value = false },
|
||||
marketsListComponent = marketsListComponent,
|
||||
alertConfig = alertConfig,
|
||||
)
|
||||
|
||||
WalletEventEffect(
|
||||
|
|
@ -100,6 +119,8 @@ private fun WalletContent(
|
|||
walletsListState: LazyListState,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
isAutoScroll: State<Boolean>,
|
||||
marketsListComponent: MarketsListComponent?,
|
||||
alertConfig: WalletAlertState?,
|
||||
onAutoScrollReset: () -> Unit,
|
||||
) {
|
||||
var selectedWalletIndex by remember(state.selectedWalletIndex) { mutableIntStateOf(state.selectedWalletIndex) }
|
||||
|
|
@ -195,6 +216,32 @@ private fun WalletContent(
|
|||
)
|
||||
}
|
||||
|
||||
if (marketsListComponent != null) {
|
||||
val bottomSheetState = remember {
|
||||
mutableStateOf(BottomSheetState.COLLAPSED)
|
||||
}
|
||||
var headerSize by remember {
|
||||
mutableStateOf(0.dp)
|
||||
}
|
||||
|
||||
BaseScaffoldWithMarkets(
|
||||
state = state,
|
||||
selectedWallet = selectedWallet,
|
||||
snackbarHostState = snackbarHostState,
|
||||
bottomSheetHeaderHeightProvider = { headerSize },
|
||||
alertConfig = alertConfig,
|
||||
onBottomSheetStateChange = { bottomSheetState.value = it },
|
||||
bottomSheetContent = {
|
||||
marketsListComponent.BottomSheetContent(
|
||||
bottomSheetState = bottomSheetState,
|
||||
onHeaderSizeChange = { headerSize = it },
|
||||
modifier = Modifier,
|
||||
)
|
||||
},
|
||||
) {
|
||||
scaffoldContent()
|
||||
}
|
||||
} else {
|
||||
BaseScaffold(
|
||||
state = state,
|
||||
selectedWallet = selectedWallet,
|
||||
|
|
@ -202,6 +249,7 @@ private fun WalletContent(
|
|||
) {
|
||||
scaffoldContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
|
|
@ -265,6 +313,268 @@ private fun BaseScaffold(
|
|||
)
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList", "LongMethod")
|
||||
@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun BaseScaffoldWithMarkets(
|
||||
state: WalletScreenState,
|
||||
selectedWallet: WalletState,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
bottomSheetHeaderHeightProvider: () -> Dp,
|
||||
bottomSheetContent: @Composable () -> Unit,
|
||||
alertConfig: WalletAlertState?,
|
||||
onBottomSheetStateChange: (BottomSheetState) -> Unit,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
// show the bottom sheet if there is at least one multicurrency wallet
|
||||
val showManageTokensBottomSheet = remember(state.wallets) {
|
||||
state.wallets.any { it is WalletState.MultiCurrency }
|
||||
}
|
||||
val bottomSheetState = rememberSheetStateEnhanced(
|
||||
initialValue = if (showManageTokensBottomSheet) SheetValue.PartiallyExpanded else SheetValue.Hidden,
|
||||
confirmValueChange = { sheetValue ->
|
||||
when {
|
||||
sheetValue == SheetValue.Hidden && showManageTokensBottomSheet -> false
|
||||
sheetValue != SheetValue.Hidden && !showManageTokensBottomSheet -> false
|
||||
else -> true
|
||||
}
|
||||
},
|
||||
skipHiddenState = showManageTokensBottomSheet,
|
||||
)
|
||||
|
||||
val keyboardShown = keyboardAsState()
|
||||
|
||||
BottomSheetStateEffects(
|
||||
bottomSheetState = bottomSheetState,
|
||||
showManageTokensBottomSheet = showManageTokensBottomSheet,
|
||||
alertConfig = alertConfig,
|
||||
keyboardShown = keyboardShown,
|
||||
onBottomSheetStateChange = onBottomSheetStateChange,
|
||||
)
|
||||
|
||||
val scaffoldState = rememberBottomSheetScaffoldState(
|
||||
bottomSheetState = bottomSheetState,
|
||||
snackbarHostState = snackbarHostState,
|
||||
)
|
||||
|
||||
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
|
||||
val statusBarHeight = with(LocalDensity.current) { WindowInsets.statusBars.getTop(this).toDp() }
|
||||
val peekHeight = bottomSheetHeaderHeightProvider() + handComposableComponentHeight + bottomBarHeight
|
||||
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
BottomSheetScaffold(
|
||||
snackbarHost = {
|
||||
WalletSnackbarHost(
|
||||
snackbarHostState = it,
|
||||
event = state.event,
|
||||
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing16),
|
||||
)
|
||||
},
|
||||
containerColor = TangemTheme.colors.background.secondary,
|
||||
sheetContainerColor = TangemTheme.colors.background.primary,
|
||||
scaffoldState = scaffoldState,
|
||||
sheetPeekHeight = peekHeight,
|
||||
sheetDragHandle = {
|
||||
Hand(modifier = Modifier.background(color = TangemTheme.colors.background.primary))
|
||||
},
|
||||
sheetTonalElevation = 8.dp,
|
||||
sheetShadowElevation = 8.dp,
|
||||
sheetContent = {
|
||||
BoxWithConstraints {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.sizeIn(maxHeight = maxHeight - statusBarHeight)
|
||||
.align(Alignment.BottomCenter),
|
||||
) {
|
||||
bottomSheetContent()
|
||||
}
|
||||
}
|
||||
|
||||
// hide bottom sheet when back pressed
|
||||
BackHandler(
|
||||
keyboardShown.value is Keyboard.Closed &&
|
||||
bottomSheetState.currentValue == SheetValue.Expanded,
|
||||
) {
|
||||
coroutineScope.launch { bottomSheetState.partialExpand() }
|
||||
}
|
||||
},
|
||||
content = { _ ->
|
||||
val pullRefreshState = rememberPullRefreshState(
|
||||
refreshing = selectedWallet.pullToRefreshConfig.isRefreshing,
|
||||
onRefresh = {
|
||||
selectedWallet.pullToRefreshConfig.onRefresh(WalletPullToRefreshConfig.ShowRefreshState(true))
|
||||
},
|
||||
)
|
||||
|
||||
Column {
|
||||
WalletTopBar(config = state.topBarConfig)
|
||||
Box(
|
||||
modifier = Modifier.pullRefresh(pullRefreshState),
|
||||
) {
|
||||
content()
|
||||
|
||||
WalletPullToRefreshIndicator(
|
||||
isRefreshing = selectedWallet.pullToRefreshConfig.isRefreshing,
|
||||
state = pullRefreshState,
|
||||
modifier = Modifier.align(Alignment.TopCenter),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
BottomSheetScrim(
|
||||
color = BottomSheetDefaults.ScrimColor,
|
||||
visible = bottomSheetState.targetValue == SheetValue.Expanded,
|
||||
onDismissRequest = { coroutineScope.launch { bottomSheetState.partialExpand() } },
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BottomSheetScrim(color: Color, visible: Boolean, onDismissRequest: () -> Unit) {
|
||||
val alpha by animateFloatAsState(
|
||||
targetValue = if (visible) 1f else 0f,
|
||||
animationSpec = TweenSpec(),
|
||||
label = "scrim",
|
||||
)
|
||||
val dismissSheet = if (visible) {
|
||||
Modifier
|
||||
.pointerInput(onDismissRequest) {
|
||||
detectTapGestures {
|
||||
onDismissRequest()
|
||||
}
|
||||
}
|
||||
.clearAndSetSemantics {}
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
Canvas(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.then(dismissSheet),
|
||||
) {
|
||||
drawRect(color = color, alpha = alpha)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Suppress("CyclomaticComplexMethod", "MagicNumber", "LongMethod")
|
||||
@Composable
|
||||
private fun BottomSheetStateEffects(
|
||||
bottomSheetState: SheetState,
|
||||
showManageTokensBottomSheet: Boolean,
|
||||
alertConfig: WalletAlertState?,
|
||||
keyboardShown: State<Keyboard>,
|
||||
onBottomSheetStateChange: (BottomSheetState) -> Unit,
|
||||
) {
|
||||
// Bottom sheet during initialization internally expand partially after its content was remeasured,
|
||||
// therefore initialValue = SheetValue.Hidden in rememberStandardBottomSheetState doesn't work as expected
|
||||
// so we have to manually restrict expansion in this case
|
||||
LaunchedEffect(bottomSheetState.targetValue, bottomSheetState.currentValue) {
|
||||
if (!showManageTokensBottomSheet &&
|
||||
(bottomSheetState.targetValue != SheetValue.Hidden || bottomSheetState.currentValue != SheetValue.Hidden)
|
||||
) {
|
||||
bottomSheetState.hide()
|
||||
}
|
||||
}
|
||||
// react to changes in wallet list
|
||||
LaunchedEffect(showManageTokensBottomSheet) {
|
||||
when {
|
||||
showManageTokensBottomSheet && bottomSheetState.currentValue != SheetValue.PartiallyExpanded -> {
|
||||
bottomSheetState.partialExpand()
|
||||
}
|
||||
!showManageTokensBottomSheet && bottomSheetState.targetValue != SheetValue.Hidden -> {
|
||||
bottomSheetState.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val systemUiController = rememberSystemUiController()
|
||||
val navigationBarColor = TangemTheme.colors.background.primary
|
||||
|
||||
LaunchedEffect(key1 = bottomSheetState.targetValue, navigationBarColor) {
|
||||
when (bottomSheetState.targetValue) {
|
||||
SheetValue.Hidden,
|
||||
SheetValue.Expanded,
|
||||
-> systemUiController.setNavigationBarColor(
|
||||
color = Color.Transparent,
|
||||
darkIcons = navigationBarColor.luminance() > 0.5f,
|
||||
navigationBarContrastEnforced = true,
|
||||
)
|
||||
SheetValue.PartiallyExpanded,
|
||||
-> systemUiController.setNavigationBarColor(navigationBarColor)
|
||||
}
|
||||
}
|
||||
|
||||
DisposableEffect(showManageTokensBottomSheet) {
|
||||
onDispose {
|
||||
if (showManageTokensBottomSheet) {
|
||||
systemUiController.setNavigationBarColor(
|
||||
color = Color.Transparent,
|
||||
darkIcons = navigationBarColor.luminance() > 0.5f,
|
||||
navigationBarContrastEnforced = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// expand bottom sheet when keyboard appears
|
||||
LaunchedEffect(keyboardShown.value is Keyboard.Opened) {
|
||||
if (keyboardShown.value is Keyboard.Opened && alertConfig == null) {
|
||||
bottomSheetState.expand()
|
||||
}
|
||||
}
|
||||
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
// hide keyboard when bottom sheet is about to be hidden
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow {
|
||||
bottomSheetState.currentValue == SheetValue.Expanded &&
|
||||
bottomSheetState.targetValue == SheetValue.PartiallyExpanded
|
||||
}.collect { sheetHasBeenHidden ->
|
||||
if (sheetHasBeenHidden) {
|
||||
keyboardController?.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val isSheetHidden = bottomSheetState.targetValue == SheetValue.PartiallyExpanded
|
||||
LaunchedEffect(isSheetHidden) {
|
||||
onBottomSheetStateChange(
|
||||
if (isSheetHidden) {
|
||||
BottomSheetState.COLLAPSED
|
||||
} else {
|
||||
BottomSheetState.EXPANDED
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use a standard method when this is fixed https://issuetracker.google.com/issues/314796718
|
||||
* Current material3 version: 1.2.0
|
||||
*/
|
||||
@Composable
|
||||
@ExperimentalMaterial3Api
|
||||
private fun rememberSheetStateEnhanced(
|
||||
skipPartiallyExpanded: Boolean = false,
|
||||
confirmValueChange: (SheetValue) -> Boolean = { true },
|
||||
initialValue: SheetValue = SheetValue.Hidden,
|
||||
skipHiddenState: Boolean = false,
|
||||
): SheetState {
|
||||
val density = LocalDensity.current
|
||||
return remember(initialValue, skipPartiallyExpanded, confirmValueChange, skipHiddenState) {
|
||||
SheetState(
|
||||
skipPartiallyExpanded = skipPartiallyExpanded,
|
||||
density = density,
|
||||
initialValue = initialValue,
|
||||
confirmValueChange = confirmValueChange,
|
||||
skipHiddenState = skipHiddenState,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WalletSnackbarHost(
|
||||
snackbarHostState: SnackbarHostState,
|
||||
|
|
@ -328,6 +638,7 @@ private fun WalletScreen_Preview(@PreviewParameter(WalletScreenPreviewProvider::
|
|||
TangemThemePreview {
|
||||
WalletScreen(
|
||||
state = data,
|
||||
marketsListComponent = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit d8e6da9ab3a0442657822352cb6274237d88e77d
|
||||
Subproject commit 0ed07b85e64805707b2ef6a92f42bc2c4e8b7753
|
||||
Loading…
Add table
Add a link
Reference in a new issue