Updated on 2026-08-14
This commit is contained in:
parent
70419c49a2
commit
7f6f605186
38 changed files with 1217 additions and 214 deletions
|
|
@ -55,7 +55,7 @@
|
|||
android:launchMode="singleTop"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/SplashTheme"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
android:windowSoftInputMode="adjustNothing">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ import kotlinx.coroutines.launch
|
|||
internal fun AddCustomTokenTestContent(state: AddCustomTokenStateHolder.TestContent, modifier: Modifier = Modifier) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val bottomSheetScaffoldState = rememberBottomSheetScaffoldState(
|
||||
bottomSheetState = BottomSheetState(initialValue = BottomSheetValue.Collapsed),
|
||||
bottomSheetState = BottomSheetState(initialValue = BottomSheetValue.Collapsed, LocalDensity.current),
|
||||
)
|
||||
|
||||
BackHandler(
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import androidx.compose.foundation.clickable
|
|||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
|
|
@ -166,7 +165,7 @@ private fun AmountItem(
|
|||
.clickable(
|
||||
enabled = onChangeApproveType != null,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(),
|
||||
indication = ripple(),
|
||||
onClick = { isExpandSelector = true },
|
||||
),
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -104,13 +104,13 @@ data class TokenMarketInfoResponse(
|
|||
|
||||
data class Links(
|
||||
@Json(name = "official_links")
|
||||
val officialLinks: List<Link>?,
|
||||
val officialLinks: List<Link>? = null,
|
||||
@Json(name = "social")
|
||||
val social: List<Link>?,
|
||||
val social: List<Link>? = null,
|
||||
@Json(name = "repository")
|
||||
val repository: List<Link>?,
|
||||
val repository: List<Link>? = null,
|
||||
@Json(name = "blockchain_site")
|
||||
val blockchainSite: List<Link>?,
|
||||
val blockchainSite: List<Link>? = null,
|
||||
)
|
||||
|
||||
data class Link(
|
||||
|
|
|
|||
|
|
@ -117,6 +117,7 @@
|
|||
<string name="common_import">Importe</string>
|
||||
<string name="common_in_progress">En progreso</string>
|
||||
<string name="common_later">Más tarde</string>
|
||||
<string name="common_left">%1$s quedan</string>
|
||||
<string name="common_locked">Bloqueado</string>
|
||||
<string name="common_main_network">Red principal</string>
|
||||
<string name="common_network_fee_title">Tarifa de la red</string>
|
||||
|
|
@ -295,6 +296,7 @@
|
|||
<string name="give_permission_unlimited">Ilimitado</string>
|
||||
<string name="home_button_order">Ordenar </string>
|
||||
<string name="home_button_scan">Escanee</string>
|
||||
<string name="information_generated_with_ai">Esta información fue generada con IA</string>
|
||||
<string name="initial_message_change_access_code_body">Toque para cambiar la contraseña</string>
|
||||
<string name="initial_message_change_passcode_body">Toque para cambiar la contraseña</string>
|
||||
<string name="initial_message_create_wallet_body">Para crear la billetera, toque la tarjeta como se muestra arriba y no la retire hasta el final de la operación</string>
|
||||
|
|
@ -348,6 +350,7 @@
|
|||
<string name="markets_common_my_portfolio">Mi portafolio</string>
|
||||
<string name="markets_common_title">Mercado</string>
|
||||
<string name="markets_generate_addresses_notification">Para generar direcciones para las redes seleccionadas, debe escanear su tarjeta Tangem</string>
|
||||
<string name="markets_hint">Para agregar tokens, abra esta página o toque la barra de búsqueda</string>
|
||||
<string name="markets_insights_info_description_message">Los datos de este apartado proceden de las siguientes redes: %s</string>
|
||||
<string name="markets_loading_error_title">No se pueden cargar los datos…</string>
|
||||
<string name="markets_loading_no_data_title">Sin datos</string>
|
||||
|
|
@ -358,12 +361,12 @@
|
|||
<string name="markets_search_token_no_result_title">Sin resultado</string>
|
||||
<string name="markets_select_network">Seleccione una red</string>
|
||||
<string name="markets_select_wallet">Seleccione una billetera</string>
|
||||
<string name="markets_selector_interval_1m_title">1 mes</string>
|
||||
<string name="markets_selector_interval_1m_title">1 mo</string>
|
||||
<string name="markets_selector_interval_1y_title">1 año</string>
|
||||
<string name="markets_selector_interval_24h_title">24 horas</string>
|
||||
<string name="markets_selector_interval_3m_title">3 meses</string>
|
||||
<string name="markets_selector_interval_6m_title">6 meses</string>
|
||||
<string name="markets_selector_interval_7d_title">7 días</string>
|
||||
<string name="markets_selector_interval_24h_title">24h</string>
|
||||
<string name="markets_selector_interval_3m_title">3mos</string>
|
||||
<string name="markets_selector_interval_6m_title">6mos</string>
|
||||
<string name="markets_selector_interval_7d_title">7d</string>
|
||||
<string name="markets_selector_interval_all_title">Todo</string>
|
||||
<string name="markets_sort_by_experienced_buyers_title">Compradores experimentados</string>
|
||||
<string name="markets_sort_by_rating_title">Evaluación</string>
|
||||
|
|
@ -395,7 +398,7 @@
|
|||
<string name="markets_token_details_liquidity_description">El cambio en la cantidad de liquidez disponible para el token durante el período de tiempo especificado</string>
|
||||
<string name="markets_token_details_liquidity_index">Índice de liquidez</string>
|
||||
<string name="markets_token_details_low">Bajo</string>
|
||||
<string name="markets_token_details_market_capitalization">Capitalización de mercado</string>
|
||||
<string name="markets_token_details_market_capitalization">Capital. de mercado</string>
|
||||
<string name="markets_token_details_market_capitalization_description">El valor total de una criptomoneda calculado multiplicando su precio por la cantidad de monedas en circulación</string>
|
||||
<string name="markets_token_details_market_rating">Evaluación de mercado</string>
|
||||
<string name="markets_token_details_market_rating_description">Posición en la clasificación de criptomonedas entre todas las monedas según la capitalización de mercado</string>
|
||||
|
|
@ -412,6 +415,7 @@
|
|||
<string name="markets_token_details_trading_volume_24h_description">La cantidad total de una criptomoneda que se ha negociado en las últimas 24 horas, lo que indica el nivel de actividad y liquidez en el mercado.</string>
|
||||
<string name="markets_tooltip_message">Tire hacia arriba o toque la barra de búsqueda para agregar tokens directamente desde el mercado</string>
|
||||
<string name="markets_tooltip_title">Agregar tokens</string>
|
||||
<string name="nfc_error_unavailable">NFC no está disponible en su dispositivo</string>
|
||||
<string name="onboarding_access_code_feature_1_description">Deba configurar un único código de acceso para proteger todas sus tarjetas</string>
|
||||
<string name="onboarding_access_code_feature_1_title">Proteger</string>
|
||||
<string name="onboarding_access_code_feature_2_description">Puede configurar un código de acceso individual en cada tarjeta más tarde</string>
|
||||
|
|
@ -637,6 +641,7 @@
|
|||
<string name="settings_forget_wallet_footer">Esto eliminará la wallet de la aplicación. La wallet en sí puede\nañadirse de nuevo.</string>
|
||||
<string name="settings_wallet_name_title">Nombre</string>
|
||||
<string name="staking_amount_requirement_error">El monto del staking debe ser al menos %s</string>
|
||||
<string name="staking_amount_tron_integer_error">El monto del staking se redondeará a %1$s TRX debido a las reglas de la red.</string>
|
||||
<string name="staking_claim_unstaked">Unstaking de la reclamación</string>
|
||||
<string name="staking_details_annual_percentage_rate">Porcentaje de rendimiento anual</string>
|
||||
<string name="staking_details_annual_percentage_rate_info">El porcentaje de rendimiento anual que podrá ganar como participante en el staking.</string>
|
||||
|
|
@ -660,6 +665,7 @@
|
|||
<string name="staking_details_unbonding_period_info">El período que debes esperar después de solicitar el retiro de fondos del staking antes de que los tokens estén disponibles.</string>
|
||||
<string name="staking_details_warmup_period">Periodo de calentamiento</string>
|
||||
<string name="staking_details_warmup_period_info">El tiempo permitido para activar la participación en la apuesta.</string>
|
||||
<string name="staking_legal">Al utilizar la función de staking, usted acepta %1$s y %2$s del proveedor</string>
|
||||
<string name="staking_locked">Bloqueado</string>
|
||||
<string name="staking_migrate">Migrar</string>
|
||||
<string name="staking_native">Native staking</string>
|
||||
|
|
@ -671,6 +677,7 @@
|
|||
<string name="staking_notification_earn_rewards_title">Gane recompensas por staking</string>
|
||||
<string name="staking_notification_unstake_text">Sus fondos estarán disponibles para su uso después del período de desvinculación de %s.</string>
|
||||
<string name="staking_notification_withdraw_text">Ya puede retirar sus fondos, estarán disponibles para usar de inmediato.</string>
|
||||
<string name="staking_notifications_revote_tron_text">Hacer staking en la red Tron con un nuevo validador transferirá automáticamente todos los fondos de staking a este validador.</string>
|
||||
<string name="staking_ready_to_withdraw">Listo para retirar</string>
|
||||
<string name="staking_rebond">Reunir</string>
|
||||
<string name="staking_restake">Haga el staking de nuevo</string>
|
||||
|
|
@ -690,8 +697,11 @@
|
|||
<string name="staking_rewards">Recompensas</string>
|
||||
<string name="staking_stake_locked">El stake está bloqueado</string>
|
||||
<string name="staking_stake_more">Hacer más staking</string>
|
||||
<string name="staking_tap_to_unlock">Toque para desbloquear</string>
|
||||
<string name="staking_tap_to_withdraw">Toque para retirar</string>
|
||||
<string name="staking_title_stake">Stake %s</string>
|
||||
<string name="staking_title_unstake">El unstaking de %s</string>
|
||||
<string name="staking_transaction_in_progress_text">¡La transacción se está procesando! La validación está en curso en la cadena de bloques. Esto puede tardar unos minutos.</string>
|
||||
<string name="staking_unbonding">Desunión</string>
|
||||
<string name="staking_unlocked_locked">Desbloquear</string>
|
||||
<string name="staking_unstaked">Sin staking</string>
|
||||
|
|
@ -701,6 +711,7 @@
|
|||
<string name="staking_vote">Votar</string>
|
||||
<string name="staking_vote_locked">Voto bloqueado</string>
|
||||
<string name="staking_withdraw">Retirar</string>
|
||||
<string name="staking_your_stakes">Sus stakes</string>
|
||||
<string name="story_awe_description">Almacene sus activos crypto de forma segura manteniendo las claves privadas contenidas en su tarjeta</string>
|
||||
<string name="story_awe_title">Billetera de hardware revolucionaria</string>
|
||||
<string name="story_backup_description">Hasta 3 tarjetas físicas por billetera</string>
|
||||
|
|
|
|||
|
|
@ -117,6 +117,7 @@
|
|||
<string name="common_import">Importez</string>
|
||||
<string name="common_in_progress">En cours</string>
|
||||
<string name="common_later">Plus tard</string>
|
||||
<string name="common_left">Il reste %1$s</string>
|
||||
<string name="common_locked">Verrouillé</string>
|
||||
<string name="common_main_network">Réseau principal</string>
|
||||
<string name="common_network_fee_title">Commissions du réseau</string>
|
||||
|
|
@ -295,6 +296,7 @@
|
|||
<string name="give_permission_unlimited">Illimité</string>
|
||||
<string name="home_button_order">Commandez</string>
|
||||
<string name="home_button_scan">Scannez</string>
|
||||
<string name="information_generated_with_ai">Ces informations ont été générées avec l\'IA</string>
|
||||
<string name="initial_message_change_access_code_body">Touchez, pour modifier le code d\'accès</string>
|
||||
<string name="initial_message_change_passcode_body">Touchez, pour modifier le mot de passe</string>
|
||||
<string name="initial_message_create_wallet_body">Pour créer le portefeuille, appuyez sur la carte comme indiqué ci-dessus et ne la retirez pas jusqu\'à la fin de l\'opération</string>
|
||||
|
|
@ -348,6 +350,7 @@
|
|||
<string name="markets_common_my_portfolio">Mon portfolio</string>
|
||||
<string name="markets_common_title">Marché</string>
|
||||
<string name="markets_generate_addresses_notification">Pour générer des adresses pour les réseaux sélectionnés, vous devez scanner votre carte Tangem</string>
|
||||
<string name="markets_hint">Pour ajouter des jetons, faites-le apparaître ou appuyez sur la barre de recherche</string>
|
||||
<string name="markets_insights_info_description_message">Les données de cette section proviennent des réseaux suivants : %s</string>
|
||||
<string name="markets_loading_error_title">Impossible de charger les données…</string>
|
||||
<string name="markets_loading_no_data_title">Aucune donnée</string>
|
||||
|
|
@ -358,11 +361,11 @@
|
|||
<string name="markets_search_token_no_result_title">Aucun résultat</string>
|
||||
<string name="markets_select_network">Sélectionnez un réseau</string>
|
||||
<string name="markets_select_wallet">Sélectionnez un portefeuille</string>
|
||||
<string name="markets_selector_interval_1m_title">1 mois</string>
|
||||
<string name="markets_selector_interval_1m_title">1 mo</string>
|
||||
<string name="markets_selector_interval_1y_title">1 an</string>
|
||||
<string name="markets_selector_interval_24h_title">24 heures</string>
|
||||
<string name="markets_selector_interval_3m_title">3 mois</string>
|
||||
<string name="markets_selector_interval_6m_title">6 mois</string>
|
||||
<string name="markets_selector_interval_24h_title">24h</string>
|
||||
<string name="markets_selector_interval_3m_title">3mos</string>
|
||||
<string name="markets_selector_interval_6m_title">6mos</string>
|
||||
<string name="markets_selector_interval_7d_title">7j</string>
|
||||
<string name="markets_selector_interval_all_title">Tout</string>
|
||||
<string name="markets_sort_by_experienced_buyers_title">Acheteurs expérimentés</string>
|
||||
|
|
@ -395,8 +398,8 @@
|
|||
<string name="markets_token_details_liquidity_description">Le changement dans la quantité de liquidité disponible pour le jeton pendant la période spécifiée</string>
|
||||
<string name="markets_token_details_liquidity_index">Indice de liquidité</string>
|
||||
<string name="markets_token_details_low">Faible</string>
|
||||
<string name="markets_token_details_market_capitalization">Capitalisation boursière</string>
|
||||
<string name="markets_token_details_market_capitalization_description">La valeur totale d\'une crypto-monnaie calculée en multipliant son prix actuel par le nbre de pièces en circulation</string>
|
||||
<string name="markets_token_details_market_capitalization">Cap. boursière</string>
|
||||
<string name="markets_token_details_market_capitalization_description">La valeur d\'une crypto-monnaie est calculée en multipliant son prix par le nombre de pièces en circulation</string>
|
||||
<string name="markets_token_details_market_rating">Évaluation du marché</string>
|
||||
<string name="markets_token_details_market_rating_description">Position dans le classement des crypto-monnaies entre toutes les pièces en fonction de la capitalisation boursière</string>
|
||||
<string name="markets_token_details_max_supply">Approvisionnement maximal</string>
|
||||
|
|
@ -412,6 +415,7 @@
|
|||
<string name="markets_token_details_trading_volume_24h_description">Le montant total d\'une crypto-monnaie qui a été échangé au cours des dernières 24 heures, indiquant le niveau d\'activité et de liquidité du marché</string>
|
||||
<string name="markets_tooltip_message">Tirez vers le haut ou appuyez sur la barre de recherche pour ajouter des jetons directement depuis le marché</string>
|
||||
<string name="markets_tooltip_title">Ajouter des jetons</string>
|
||||
<string name="nfc_error_unavailable">NFC n\'est pas disponible sur votre appareil</string>
|
||||
<string name="onboarding_access_code_feature_1_description">Vous devez définir un seul code d\'accès pour protéger toutes vos cartes</string>
|
||||
<string name="onboarding_access_code_feature_1_title">Protéger</string>
|
||||
<string name="onboarding_access_code_feature_2_description">Vous pourrez définir un code d\'accès individuel sur chaque carte plus tard</string>
|
||||
|
|
@ -637,6 +641,7 @@
|
|||
<string name="settings_forget_wallet_footer">Cela supprimera le portefeuille de l\'application. Le portefeuille lui-même peut être ajouté à nouveau.</string>
|
||||
<string name="settings_wallet_name_title">Nom</string>
|
||||
<string name="staking_amount_requirement_error">Le montant à staker doit être au moins %s</string>
|
||||
<string name="staking_amount_tron_integer_error">Le montant du staking sera arrondi à %1$s TRX en raison des règles du réseau.</string>
|
||||
<string name="staking_claim_unstaked">Réclamation déstakée</string>
|
||||
<string name="staking_details_annual_percentage_rate">Pourcentage de rendement annuel</string>
|
||||
<string name="staking_details_annual_percentage_rate_info">Le pourcentage de rendement annuel que vous pouvez gagner en participant au staking.</string>
|
||||
|
|
@ -660,6 +665,7 @@
|
|||
<string name="staking_details_unbonding_period_info">La période que vous devez attendre après avoir demandé le retrait des fonds du staking avant que les jetons ne soient disponibles.</string>
|
||||
<string name="staking_details_warmup_period">Période d\'échauffement</string>
|
||||
<string name="staking_details_warmup_period_info">Le temps imparti pour activer la participation au staking.</string>
|
||||
<string name="staking_legal">En utilisant la fonctionnalité de staking, vous acceptez les %1$s et %2$s du fournisseur</string>
|
||||
<string name="staking_locked">Bloqué</string>
|
||||
<string name="staking_migrate">Migrer</string>
|
||||
<string name="staking_native">Native staking</string>
|
||||
|
|
@ -671,6 +677,7 @@
|
|||
<string name="staking_notification_earn_rewards_title">Gagnez des récompenses de staking</string>
|
||||
<string name="staking_notification_unstake_text">Vos fonds seront disponibles pour utilisation après la période de désengagement %s.</string>
|
||||
<string name="staking_notification_withdraw_text">Vous pouvez désormais retirer vos fonds, ils seront disponibles à l\'utilisation immédiatement</string>
|
||||
<string name="staking_notifications_revote_tron_text">Le staking dans le réseau Tron avec un nouveau validateur transférera automatiquement tous les fonds précédemment stakés vers ce validateur</string>
|
||||
<string name="staking_ready_to_withdraw">Prêt à retirer</string>
|
||||
<string name="staking_rebond">Réassocier</string>
|
||||
<string name="staking_restake">Restakez</string>
|
||||
|
|
@ -690,8 +697,11 @@
|
|||
<string name="staking_rewards">Récompenses</string>
|
||||
<string name="staking_stake_locked">Stake verrouillé</string>
|
||||
<string name="staking_stake_more">Staker plus</string>
|
||||
<string name="staking_tap_to_unlock">Appuyez pour déverrouiller</string>
|
||||
<string name="staking_tap_to_withdraw">Appuyez pour retirer</string>
|
||||
<string name="staking_title_stake">Stake %s</string>
|
||||
<string name="staking_title_unstake">déstaker %s</string>
|
||||
<string name="staking_transaction_in_progress_text">La transaction est en cours de traitement ! La validation est actuellement en cours dans la blockchain. Cela peut prendre quelques minutes.</string>
|
||||
<string name="staking_unbonding">Dissociation</string>
|
||||
<string name="staking_unlocked_locked">Débloquer</string>
|
||||
<string name="staking_unstaked">Non-staké</string>
|
||||
|
|
@ -701,6 +711,7 @@
|
|||
<string name="staking_vote">Voter</string>
|
||||
<string name="staking_vote_locked">Vote bloqué</string>
|
||||
<string name="staking_withdraw">Retirer</string>
|
||||
<string name="staking_your_stakes">Vos stakes</string>
|
||||
<string name="story_awe_description">Stockez vos actifs crypto en toute sécurité tout en conservant les clés privées contenues dans votre carte</string>
|
||||
<string name="story_awe_title">Portefeuille matériel révolutionnaire</string>
|
||||
<string name="story_backup_description">Jusqu\'à 3 cartes physiques pour un portefeuille</string>
|
||||
|
|
|
|||
|
|
@ -358,13 +358,13 @@
|
|||
<string name="markets_search_token_no_result_title">検索結果はありません</string>
|
||||
<string name="markets_select_network">ネットワークを選択</string>
|
||||
<string name="markets_select_wallet">ウォレットを選択</string>
|
||||
<string name="markets_selector_interval_1m_title">1ヶ月</string>
|
||||
<string name="markets_selector_interval_1y_title">1年</string>
|
||||
<string name="markets_selector_interval_24h_title">24時間</string>
|
||||
<string name="markets_selector_interval_3m_title">3ヶ月</string>
|
||||
<string name="markets_selector_interval_6m_title">6ヶ月</string>
|
||||
<string name="markets_selector_interval_7d_title">7日</string>
|
||||
<string name="markets_selector_interval_all_title">すべて</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">全部</string>
|
||||
<string name="markets_sort_by_experienced_buyers_title">経験豊富な買い手</string>
|
||||
<string name="markets_sort_by_rating_title">格付け</string>
|
||||
<string name="markets_sort_by_title">並べ替え</string>
|
||||
|
|
@ -637,6 +637,7 @@
|
|||
<string name="settings_forget_wallet_footer">これにより、ウォレットがアプリから削除されます。ウォレットは再度追加できます。</string>
|
||||
<string name="settings_wallet_name_title">名前</string>
|
||||
<string name="staking_amount_requirement_error">ステーキング金額は %s 以上である必要があります</string>
|
||||
<string name="staking_amount_tron_integer_error">ネットワークルールにより、ステーキング金額は%1$s TRX に切り上げられます。</string>
|
||||
<string name="staking_claim_unstaked">ステーキング解除分を請求する</string>
|
||||
<string name="staking_details_annual_percentage_rate">年率</string>
|
||||
<string name="staking_details_annual_percentage_rate_info">ステーキングに参加することで得られる年間収益率。</string>
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
package com.tangem.core.ui.components
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.ime
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Immutable
|
||||
sealed interface Keyboard {
|
||||
val height: Dp
|
||||
|
||||
|
|
@ -30,6 +29,34 @@ val Keyboard.isOpened: Boolean
|
|||
@Composable
|
||||
fun keyboardAsState(): State<Keyboard> {
|
||||
val bottom = WindowInsets.ime.getBottom(LocalDensity.current)
|
||||
val isImeVisible = bottom > 0
|
||||
return rememberUpdatedState(if (isImeVisible) Keyboard.Opened(bottom.dp) else Keyboard.Closed)
|
||||
val bottomDp = with(LocalDensity.current) { bottom.toDp() }
|
||||
|
||||
val keyboardStateInternal by rememberUpdatedState(
|
||||
if (bottom > 0) Keyboard.Opened(bottomDp) else Keyboard.Closed,
|
||||
)
|
||||
|
||||
val keyboardState = remember { mutableStateOf(keyboardStateInternal) }
|
||||
|
||||
LaunchedEffect(keyboardStateInternal) {
|
||||
val falsePositive = Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q &&
|
||||
keyboardStateInternal is Keyboard.Opened &&
|
||||
keyboardStateInternal.height < 50.dp
|
||||
// FIX android <=10 devices can randomly send ime paddings,
|
||||
// which leads to a false positive keyboard opening ([REDACTED_TASK_KEY])
|
||||
if (falsePositive) return@LaunchedEffect
|
||||
|
||||
keyboardState.value = keyboardStateInternal
|
||||
}
|
||||
|
||||
return keyboardState
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberIsKeyboardVisible(): State<Boolean> {
|
||||
val keyboard by keyboardAsState()
|
||||
return remember(keyboard) {
|
||||
derivedStateOf {
|
||||
keyboard.isOpened
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ import androidx.compose.foundation.indication
|
|||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
|
|
@ -47,7 +47,7 @@ fun TangemSwitch(
|
|||
modifier = modifier
|
||||
.clickable(
|
||||
interactionSource = interactionSource,
|
||||
indication = rememberRipple(
|
||||
indication = ripple(
|
||||
bounded = false,
|
||||
color = Color.Transparent,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
|
|||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
|
|
@ -39,7 +39,7 @@ fun TangemRadioButton(
|
|||
modifier = modifier.clickable(
|
||||
enabled = isEnabled,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(bounded = false, radius = TangemTheme.dimens.size16),
|
||||
indication = ripple(bounded = false, radius = TangemTheme.dimens.size16),
|
||||
onClick = onClick,
|
||||
),
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.core.ui.components.bottomsheets
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
|
|
@ -14,7 +15,6 @@ import androidx.compose.ui.unit.dp
|
|||
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.res.LocalBottomSheetAlwaysVisible
|
||||
import com.tangem.core.ui.res.LocalWindowSize
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.utils.WindowInsetsZero
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
|
|
@ -150,17 +150,15 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicBottomSheet(
|
|||
) {
|
||||
val model = config.content as? T ?: return
|
||||
|
||||
val statusBarHeight = with(LocalDensity.current) { WindowInsets.statusBars.getTop(this).toDp() }
|
||||
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
|
||||
|
||||
ModalBottomSheet(
|
||||
// FIXME temporary solution to fix height of the bottom sheet
|
||||
modifier = modifier.heightIn(max = LocalWindowSize.current.height - statusBarHeight),
|
||||
modifier = modifier.statusBarsPadding(),
|
||||
onDismissRequest = config.onDismissRequest,
|
||||
sheetState = sheetState,
|
||||
containerColor = containerColor,
|
||||
shape = TangemTheme.shapes.bottomSheetLarge,
|
||||
windowInsets = WindowInsetsZero,
|
||||
contentWindowInsets = { WindowInsetsZero },
|
||||
dragHandle = { TangemBottomSheetDraggableHeader(color = containerColor) },
|
||||
) {
|
||||
Column(
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@ import androidx.compose.foundation.clickable
|
|||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
|
|
@ -86,7 +86,7 @@ fun InputRowBestRate(
|
|||
.padding(vertical = TangemTheme.dimens.spacing10)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(bounded = false),
|
||||
indication = ripple(bounded = false),
|
||||
onClick = onIconClick,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@ import androidx.compose.foundation.layout.Column
|
|||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment.Companion.CenterVertically
|
||||
|
|
@ -94,7 +94,7 @@ fun InputRowDefault(
|
|||
.clickable(
|
||||
enabled = onIconClick != null,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(bounded = false),
|
||||
indication = ripple(bounded = false),
|
||||
) { onIconClick?.invoke() },
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@ import androidx.compose.foundation.clickable
|
|||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
@ -98,7 +98,7 @@ fun InputRowEnter(
|
|||
)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(bounded = false),
|
||||
indication = ripple(bounded = false),
|
||||
) { onIconClick?.invoke() },
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
@ -104,7 +104,7 @@ fun InputRowEnterAmount(
|
|||
)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(bounded = false),
|
||||
indication = ripple(bounded = false),
|
||||
) { onIconClick?.invoke() },
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ import androidx.compose.foundation.background
|
|||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment.Companion.CenterVertically
|
||||
|
|
@ -111,7 +111,7 @@ fun InputRowImage(
|
|||
.align(CenterVertically)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(bounded = false),
|
||||
indication = ripple(bounded = false),
|
||||
) { onIconClick?.invoke() },
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import androidx.compose.foundation.background
|
|||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
@ -64,7 +64,7 @@ fun InputRowImageSelector(
|
|||
.clickable(
|
||||
onClick = onSelect,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(),
|
||||
indication = ripple(),
|
||||
)
|
||||
.padding(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -9,9 +9,9 @@ import androidx.compose.foundation.clickable
|
|||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
|
|
@ -65,7 +65,7 @@ fun PasteButton(isPasteButtonVisible: Boolean, onClick: (String) -> Unit, modifi
|
|||
)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(radius = TangemTheme.dimens.radius8),
|
||||
indication = ripple(radius = TangemTheme.dimens.radius8),
|
||||
enabled = isPasteEnabled,
|
||||
onClick = {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
|
|
@ -91,7 +91,7 @@ fun CrossIcon(onClick: (String) -> Unit, modifier: Modifier = Modifier) {
|
|||
.size(TangemTheme.dimens.size24)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(radius = TangemTheme.dimens.radius12),
|
||||
indication = ripple(radius = TangemTheme.dimens.radius12),
|
||||
onClick = { onClick("") },
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
|
|||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
|
|
@ -125,7 +125,7 @@ fun NotificationWithBackground(config: NotificationConfig, modifier: Modifier =
|
|||
}
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(bounded = false),
|
||||
indication = ripple(bounded = false),
|
||||
) {
|
||||
config.onCloseClick?.invoke()
|
||||
},
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ import androidx.compose.foundation.background
|
|||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
|
|
@ -95,7 +95,7 @@ private fun Content(config: NotificationConfig) {
|
|||
.size(TangemTheme.dimens.size20)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(radius = TangemTheme.dimens.radius10),
|
||||
indication = ripple(radius = TangemTheme.dimens.radius10),
|
||||
onClick = it,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,126 @@
|
|||
@file:Suppress("UnnecessaryParentheses")
|
||||
|
||||
package com.tangem.core.ui.components.progressbar
|
||||
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.layout.layout
|
||||
import androidx.compose.ui.semantics.ProgressBarRangeInfo
|
||||
import androidx.compose.ui.semantics.progressBarRangeInfo
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.offset
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import kotlin.math.abs
|
||||
|
||||
/**
|
||||
* Progress indicators express an unspecified wait time or display the length of a process.
|
||||
*
|
||||
* By default there is no animation between [progress] values. You can use
|
||||
*
|
||||
* @param progress The progress of this progress indicator, where 0.0 represents no progress and 1.0
|
||||
* represents full progress. Values outside of this range are coerced into the range.
|
||||
* @param modifier the [Modifier] to be applied to this progress indicator
|
||||
* @param color The color of the progress indicator.
|
||||
* @param backgroundColor The color of the background behind the indicator, visible when the
|
||||
* progress has not reached that area of the overall indicator yet.
|
||||
* @param strokeCap stroke cap to use for the ends of this progress indicator
|
||||
*/
|
||||
@Composable
|
||||
fun LinearProgressIndicator(
|
||||
progress: () -> Float,
|
||||
modifier: Modifier = Modifier,
|
||||
color: Color = TangemTheme.colors.text.accent,
|
||||
backgroundColor: Color = TangemTheme.colors.background.tertiary,
|
||||
strokeCap: StrokeCap = StrokeCap.Round,
|
||||
) {
|
||||
val coercedProgress = { progress().coerceIn(0f, 1f) }
|
||||
Canvas(
|
||||
modifier
|
||||
.increaseSemanticsBounds()
|
||||
.semantics(mergeDescendants = true) {
|
||||
progressBarRangeInfo = ProgressBarRangeInfo(coercedProgress(), 0f..1f)
|
||||
}
|
||||
.size(height = 4.dp, width = 24.dp),
|
||||
) {
|
||||
val strokeWidth = size.height
|
||||
drawLinearIndicatorBackground(backgroundColor, strokeWidth, strokeCap)
|
||||
drawLinearIndicator(0f, coercedProgress(), color, strokeWidth, strokeCap)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun Modifier.increaseSemanticsBounds(): Modifier {
|
||||
val padding = 10.dp
|
||||
return this
|
||||
.layout { measurable, constraints ->
|
||||
val paddingPx = padding.roundToPx()
|
||||
// We need to add vertical padding to the semantics bounds in other to meet
|
||||
// screenreader green box minimum size, but we also want to
|
||||
// preserve a visual appearance and layout size below that minimum
|
||||
// in order to maintain backwards compatibility. This custom
|
||||
// layout effectively implements "negative padding".
|
||||
val newConstraint = constraints.offset(0, paddingPx * 2)
|
||||
val placeable = measurable.measure(newConstraint)
|
||||
|
||||
// But when actually placing the placeable, create the layout without additional
|
||||
// space. Place the placeable where it would've been without any extra padding.
|
||||
val height = placeable.height - paddingPx * 2
|
||||
val width = placeable.width
|
||||
layout(width, height) {
|
||||
placeable.place(0, -paddingPx)
|
||||
}
|
||||
}
|
||||
.semantics(mergeDescendants = true) {}
|
||||
.padding(vertical = padding)
|
||||
}
|
||||
|
||||
private fun DrawScope.drawLinearIndicator(
|
||||
startFraction: Float,
|
||||
endFraction: Float,
|
||||
color: Color,
|
||||
strokeWidth: Float,
|
||||
strokeCap: StrokeCap,
|
||||
) {
|
||||
val width = size.width
|
||||
val height = size.height
|
||||
// Start drawing from the vertical center of the stroke
|
||||
val yOffset = height / 2
|
||||
|
||||
val isLtr = layoutDirection == LayoutDirection.Ltr
|
||||
val barStart = (if (isLtr) startFraction else 1f - endFraction) * width
|
||||
val barEnd = (if (isLtr) endFraction else 1f - startFraction) * width
|
||||
|
||||
// if there isn't enough space to draw the stroke caps, fall back to StrokeCap.Butt
|
||||
if (strokeCap == StrokeCap.Butt || height > width) {
|
||||
// Progress line
|
||||
drawLine(color, Offset(barStart, yOffset), Offset(barEnd, yOffset), strokeWidth)
|
||||
} else {
|
||||
// need to adjust barStart and barEnd for the stroke caps
|
||||
val strokeCapOffset = strokeWidth / 2
|
||||
val coerceRange = strokeCapOffset..(width - strokeCapOffset)
|
||||
val adjustedBarStart = barStart.coerceIn(coerceRange)
|
||||
val adjustedBarEnd = barEnd.coerceIn(coerceRange)
|
||||
|
||||
if (abs(endFraction - startFraction) > 0) {
|
||||
// Progress line
|
||||
drawLine(
|
||||
color,
|
||||
Offset(adjustedBarStart, yOffset),
|
||||
Offset(adjustedBarEnd, yOffset),
|
||||
strokeWidth,
|
||||
strokeCap,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun DrawScope.drawLinearIndicatorBackground(color: Color, strokeWidth: Float, strokeCap: StrokeCap) =
|
||||
drawLinearIndicator(0f, 1f, color, strokeWidth, strokeCap)
|
||||
|
|
@ -6,10 +6,10 @@ import androidx.compose.foundation.clickable
|
|||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
|
|
@ -67,7 +67,7 @@ fun RoundableCornersRow(
|
|||
.size(TangemTheme.dimens.size16)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(bounded = false, radius = TangemTheme.dimens.radius10),
|
||||
indication = ripple(bounded = false, radius = TangemTheme.dimens.radius10),
|
||||
onClick = iconClick,
|
||||
),
|
||||
painter = painterResource(id = R.drawable.ic_alert_24),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,122 @@
|
|||
@file:Suppress("all")
|
||||
|
||||
package com.tangem.core.ui.components.sheetscaffold
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.gestures.AnchoredDraggableState
|
||||
import androidx.compose.foundation.gestures.DraggableAnchors
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.Measurable
|
||||
import androidx.compose.ui.layout.MeasureResult
|
||||
import androidx.compose.ui.layout.MeasureScope
|
||||
import androidx.compose.ui.node.LayoutModifierNode
|
||||
import androidx.compose.ui.node.ModifierNodeElement
|
||||
import androidx.compose.ui.platform.InspectorInfo
|
||||
import androidx.compose.ui.platform.debugInspectorInfo
|
||||
import androidx.compose.ui.unit.Constraints
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/**
|
||||
* This Modifier allows configuring an [AnchoredDraggableState]'s anchors based on this layout
|
||||
* node's size and offsetting it. It considers lookahead and reports the appropriate size and
|
||||
* measurement for the appropriate phase.
|
||||
*
|
||||
* @param state The state the anchors should be attached to
|
||||
* @param orientation The orientation the component should be offset in
|
||||
* @param anchors Lambda to calculate the anchors based on this layout's size and the incoming
|
||||
* constraints. These can be useful to avoid subcomposition.
|
||||
*/
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
internal fun <T> Modifier.draggableAnchors(
|
||||
state: AnchoredDraggableState<T>,
|
||||
orientation: Orientation,
|
||||
anchors: (size: IntSize, constraints: Constraints) -> Pair<DraggableAnchors<T>, T>,
|
||||
) = this then DraggableAnchorsElement(state, anchors, orientation)
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
private class DraggableAnchorsElement<T>(
|
||||
private val state: AnchoredDraggableState<T>,
|
||||
private val anchors: (size: IntSize, constraints: Constraints) -> Pair<DraggableAnchors<T>, T>,
|
||||
private val orientation: Orientation,
|
||||
) : ModifierNodeElement<DraggableAnchorsNode<T>>() {
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
override fun create() = DraggableAnchorsNode(state, anchors, orientation)
|
||||
|
||||
override fun update(node: DraggableAnchorsNode<T>) {
|
||||
node.state = state
|
||||
node.anchors = anchors
|
||||
node.orientation = orientation
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
|
||||
if (other !is DraggableAnchorsElement<*>) return false
|
||||
|
||||
if (state != other.state) return false
|
||||
if (anchors !== other.anchors) return false
|
||||
if (orientation != other.orientation) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = state.hashCode()
|
||||
result = 31 * result + anchors.hashCode()
|
||||
result = 31 * result + orientation.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
override fun InspectorInfo.inspectableProperties() {
|
||||
debugInspectorInfo {
|
||||
properties["state"] = state
|
||||
properties["anchors"] = anchors
|
||||
properties["orientation"] = orientation
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
private class DraggableAnchorsNode<T>(
|
||||
var state: AnchoredDraggableState<T>,
|
||||
var anchors: (size: IntSize, constraints: Constraints) -> Pair<DraggableAnchors<T>, T>,
|
||||
var orientation: Orientation,
|
||||
) : Modifier.Node(), LayoutModifierNode {
|
||||
private var didLookahead: Boolean = false
|
||||
|
||||
override fun onDetach() {
|
||||
didLookahead = false
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
override fun MeasureScope.measure(measurable: Measurable, constraints: Constraints): MeasureResult {
|
||||
val placeable = measurable.measure(constraints)
|
||||
// If we are in a lookahead pass, we only want to update the anchors here and not in
|
||||
// post-lookahead. If there is no lookahead happening (!isLookingAhead && !didLookahead),
|
||||
// update the anchors in the main pass.
|
||||
if (!isLookingAhead || !didLookahead) {
|
||||
val size = IntSize(placeable.width, placeable.height)
|
||||
val newAnchorResult = anchors(size, constraints)
|
||||
state.updateAnchors(newAnchorResult.first, newAnchorResult.second)
|
||||
}
|
||||
didLookahead = isLookingAhead || didLookahead
|
||||
return layout(placeable.width, placeable.height) {
|
||||
// In a lookahead pass, we use the position of the current target as this is where any
|
||||
// ongoing animations would move. If the component is in a settled state, lookahead
|
||||
// and post-lookahead will converge.
|
||||
val offset = if (isLookingAhead) {
|
||||
state.anchors.positionOf(state.targetValue)
|
||||
} else {
|
||||
state.requireOffset()
|
||||
}
|
||||
|
||||
val xOffset = if (orientation == Orientation.Horizontal) offset else 0f
|
||||
val yOffset = if (orientation == Orientation.Vertical) offset else 0f
|
||||
|
||||
placeable.place(xOffset.roundToInt(), yOffset.roundToInt())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,334 @@
|
|||
@file:Suppress("all")
|
||||
|
||||
package com.tangem.core.ui.components.sheetscaffold
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.gestures.DraggableAnchors
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.gestures.anchoredDraggable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.contentColorFor
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.layout.Layout
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import androidx.compose.ui.util.fastMap
|
||||
import androidx.compose.ui.util.fastMaxOfOrNull
|
||||
import com.tangem.core.ui.components.sheetscaffold.TangemSheetValue.*
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/**
|
||||
* <a href="https://m3.material.io/components/bottom-sheets/overview" class="external"
|
||||
* target="_blank">Material Design standard bottom sheet scaffold</a>.
|
||||
*
|
||||
* Standard bottom sheets co-exist with the screen’s main UI region and allow for simultaneously
|
||||
* viewing and interacting with both regions. They are commonly used to keep a feature or secondary
|
||||
* content visible on screen when content in main UI region is frequently scrolled or panned.
|
||||
*
|
||||
* 
|
||||
*
|
||||
* This component provides API to put together several material components to construct your screen,
|
||||
* by ensuring proper layout strategy for them and collecting necessary data so these components
|
||||
* will work together correctly.
|
||||
*
|
||||
* @param sheetContent the content of the bottom sheet
|
||||
* @param modifier the [Modifier] to be applied to this scaffold
|
||||
* @param scaffoldState the state of the bottom sheet scaffold
|
||||
* @param sheetPeekHeight the height of the bottom sheet when it is collapsed
|
||||
* @param sheetMaxWidth [Dp] that defines what the maximum width the sheet will take. Pass in
|
||||
* [Dp.Unspecified] for a sheet that spans the entire screen width.
|
||||
* @param sheetShape the shape of the bottom sheet
|
||||
* @param sheetContainerColor the background color of the bottom sheet
|
||||
* @param sheetContentColor the preferred content color provided by the bottom sheet to its
|
||||
* children. Defaults to the matching content color for [sheetContainerColor], or if that is not a
|
||||
* color from the theme, this will keep the same content color set above the bottom sheet.
|
||||
* @param sheetTonalElevation when [sheetContainerColor] is [ColorScheme.surface], a translucent
|
||||
* primary color overlay is applied on top of the container. A higher tonal elevation value will
|
||||
* result in a darker color in light theme and lighter color in dark theme. See also: [Surface].
|
||||
* @param sheetShadowElevation the shadow elevation of the bottom sheet
|
||||
* @param sheetSwipeEnabled whether the sheet swiping is enabled and should react to the user's
|
||||
* input
|
||||
* @param topBar top app bar of the screen, typically a [SmallTopAppBar]
|
||||
* @param snackbarHost component to host [Snackbar]s that are pushed to be shown via
|
||||
* [SnackbarHostState.showSnackbar], typically a [SnackbarHost]
|
||||
* @param containerColor the color used for the background of this scaffold. Use [Color.Transparent]
|
||||
* to have no color.
|
||||
* @param contentColor the preferred color for content inside this scaffold. Defaults to either the
|
||||
* matching content color for [containerColor], or to the current [LocalContentColor] if
|
||||
* [containerColor] is not a color from the theme.
|
||||
* @param content content of the screen. The lambda receives a [PaddingValues] that should be
|
||||
* applied to the content root via [Modifier.padding] and [Modifier.consumeWindowInsets] to
|
||||
* properly offset top and bottom bars. If using [Modifier.verticalScroll], apply this modifier to
|
||||
* the child of the scroll, and not on the scroll itself.
|
||||
*/
|
||||
@Composable
|
||||
fun TangemBottomSheetScaffold(
|
||||
sheetContent: @Composable ColumnScope.() -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
scaffoldState: TangemBottomSheetScaffoldState = rememberTangemBottomSheetScaffoldState(),
|
||||
sheetPeekHeight: Dp,
|
||||
sheetMaxWidth: Dp = 640.dp,
|
||||
sheetShape: Shape = TangemTheme.shapes.bottomSheetLarge,
|
||||
sheetContainerColor: Color = Color.White, // FIXME
|
||||
sheetContentColor: Color = contentColorFor(sheetContainerColor),
|
||||
sheetTonalElevation: Dp = 0.dp,
|
||||
sheetShadowElevation: Dp = 1.dp,
|
||||
sheetSwipeEnabled: Boolean = true,
|
||||
topBar: @Composable (() -> Unit)? = null,
|
||||
snackbarHost: @Composable (SnackbarHostState) -> Unit = { SnackbarHost(it) },
|
||||
containerColor: Color = TangemTheme.colors.background.secondary,
|
||||
contentColor: Color = contentColorFor(containerColor),
|
||||
content: @Composable (PaddingValues) -> Unit,
|
||||
) {
|
||||
BottomSheetScaffoldLayout(
|
||||
modifier = modifier,
|
||||
topBar = topBar,
|
||||
body = { content(PaddingValues(bottom = sheetPeekHeight)) },
|
||||
snackbarHost = { snackbarHost(scaffoldState.snackbarHostState) },
|
||||
sheetOffset = { scaffoldState.bottomSheetState.requireOffset() },
|
||||
sheetState = scaffoldState.bottomSheetState,
|
||||
containerColor = containerColor,
|
||||
contentColor = contentColor,
|
||||
bottomSheet = {
|
||||
StandardBottomSheet(
|
||||
state = scaffoldState.bottomSheetState,
|
||||
peekHeight = sheetPeekHeight,
|
||||
sheetMaxWidth = sheetMaxWidth,
|
||||
sheetSwipeEnabled = sheetSwipeEnabled,
|
||||
shape = sheetShape,
|
||||
containerColor = sheetContainerColor,
|
||||
contentColor = sheetContentColor,
|
||||
tonalElevation = sheetTonalElevation,
|
||||
shadowElevation = sheetShadowElevation,
|
||||
content = sheetContent,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* State of the [TangemBottomSheetScaffold] composable.
|
||||
*
|
||||
* @param bottomSheetState the state of the persistent bottom sheet
|
||||
* @param snackbarHostState the [SnackbarHostState] used to show snackbars inside the scaffold
|
||||
*/
|
||||
@Stable
|
||||
class TangemBottomSheetScaffoldState(
|
||||
val bottomSheetState: TangemSheetState,
|
||||
val snackbarHostState: SnackbarHostState,
|
||||
)
|
||||
|
||||
/**
|
||||
* Create and [remember] a [TangemBottomSheetScaffoldState].
|
||||
*
|
||||
* @param bottomSheetState the state of the standard bottom sheet. See
|
||||
* [rememberTangemStandardBottomSheetState]
|
||||
* @param snackbarHostState the [SnackbarHostState] used to show snackbars inside the scaffold
|
||||
*/
|
||||
@Composable
|
||||
fun rememberTangemBottomSheetScaffoldState(
|
||||
bottomSheetState: TangemSheetState = rememberTangemStandardBottomSheetState(),
|
||||
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
|
||||
): TangemBottomSheetScaffoldState {
|
||||
return remember(bottomSheetState, snackbarHostState) {
|
||||
TangemBottomSheetScaffoldState(
|
||||
bottomSheetState = bottomSheetState,
|
||||
snackbarHostState = snackbarHostState,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and [remember] a [TangemSheetState] for [TangemBottomSheetScaffold].
|
||||
*
|
||||
* @param initialValue the initial value of the state. Should be either [PartiallyExpanded] or
|
||||
* [Expanded] if [skipHiddenState] is true
|
||||
* @param confirmValueChange optional callback invoked to confirm or veto a pending state change
|
||||
* @param [skipHiddenState] whether Hidden state is skipped for [TangemBottomSheetScaffold]
|
||||
*/
|
||||
@Composable
|
||||
fun rememberTangemStandardBottomSheetState(
|
||||
initialValue: TangemSheetValue = PartiallyExpanded,
|
||||
confirmValueChange: (TangemSheetValue) -> Boolean = { true },
|
||||
skipHiddenState: Boolean = true,
|
||||
) = rememberSheetState(
|
||||
confirmValueChange = confirmValueChange,
|
||||
initialValue = initialValue,
|
||||
skipHiddenState = skipHiddenState,
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
private fun StandardBottomSheet(
|
||||
state: TangemSheetState,
|
||||
peekHeight: Dp,
|
||||
sheetMaxWidth: Dp,
|
||||
sheetSwipeEnabled: Boolean,
|
||||
shape: Shape,
|
||||
containerColor: Color,
|
||||
contentColor: Color,
|
||||
tonalElevation: Dp,
|
||||
shadowElevation: Dp,
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val orientation = Orientation.Vertical
|
||||
|
||||
val peekHeightPx = with(LocalDensity.current) { peekHeight.toPx() }
|
||||
val nestedScroll =
|
||||
if (sheetSwipeEnabled) {
|
||||
Modifier.nestedScroll(
|
||||
remember(state.anchoredDraggableState) {
|
||||
consumeSwipeWithinBottomSheetBoundsNestedScrollConnection(
|
||||
sheetState = state,
|
||||
orientation = orientation,
|
||||
onFling = { scope.launch { state.settle(it) } },
|
||||
)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.widthIn(max = sheetMaxWidth)
|
||||
.fillMaxWidth()
|
||||
.requiredHeightIn(min = peekHeight)
|
||||
.then(nestedScroll)
|
||||
.draggableAnchors(
|
||||
state = state.anchoredDraggableState,
|
||||
orientation = orientation,
|
||||
anchors = { sheetSize, constraints ->
|
||||
val layoutHeight = constraints.maxHeight.toFloat()
|
||||
val sheetHeight = sheetSize.height.toFloat()
|
||||
|
||||
val newAnchors = DraggableAnchors {
|
||||
if (!state.skipPartiallyExpanded) {
|
||||
PartiallyExpanded at (layoutHeight - peekHeightPx)
|
||||
}
|
||||
if (sheetHeight != peekHeightPx) {
|
||||
Expanded at maxOf(layoutHeight - sheetHeight, 0f)
|
||||
}
|
||||
if (!state.skipHiddenState) {
|
||||
Hidden at layoutHeight
|
||||
}
|
||||
}
|
||||
val newTarget =
|
||||
when (val oldTarget = state.anchoredDraggableState.targetValue) {
|
||||
Hidden -> if (newAnchors.hasAnchorFor(Hidden)) Hidden else oldTarget
|
||||
PartiallyExpanded ->
|
||||
when {
|
||||
newAnchors.hasAnchorFor(PartiallyExpanded) -> PartiallyExpanded
|
||||
newAnchors.hasAnchorFor(Expanded) -> Expanded
|
||||
newAnchors.hasAnchorFor(Hidden) -> Hidden
|
||||
else -> oldTarget
|
||||
}
|
||||
Expanded ->
|
||||
when {
|
||||
newAnchors.hasAnchorFor(Expanded) -> Expanded
|
||||
newAnchors.hasAnchorFor(PartiallyExpanded) -> PartiallyExpanded
|
||||
newAnchors.hasAnchorFor(Hidden) -> Hidden
|
||||
else -> oldTarget
|
||||
}
|
||||
}
|
||||
newAnchors to newTarget
|
||||
},
|
||||
)
|
||||
.anchoredDraggable(
|
||||
state = state.anchoredDraggableState,
|
||||
orientation = orientation,
|
||||
enabled = sheetSwipeEnabled,
|
||||
),
|
||||
shape = shape,
|
||||
color = containerColor,
|
||||
contentColor = contentColor,
|
||||
tonalElevation = tonalElevation,
|
||||
shadowElevation = shadowElevation,
|
||||
) {
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BottomSheetScaffoldLayout(
|
||||
modifier: Modifier,
|
||||
topBar: @Composable (() -> Unit)?,
|
||||
body: @Composable () -> Unit,
|
||||
bottomSheet: @Composable () -> Unit,
|
||||
snackbarHost: @Composable () -> Unit,
|
||||
sheetOffset: () -> Float,
|
||||
sheetState: TangemSheetState,
|
||||
containerColor: Color,
|
||||
contentColor: Color,
|
||||
) {
|
||||
Layout(
|
||||
contents =
|
||||
listOf<@Composable () -> Unit>(
|
||||
topBar ?: {},
|
||||
{
|
||||
Surface(
|
||||
modifier = modifier,
|
||||
color = containerColor,
|
||||
contentColor = contentColor,
|
||||
content = body,
|
||||
)
|
||||
},
|
||||
bottomSheet,
|
||||
snackbarHost,
|
||||
),
|
||||
) {
|
||||
(topBarMeasurables, bodyMeasurables, bottomSheetMeasurables, snackbarHostMeasurables),
|
||||
constraints,
|
||||
->
|
||||
val layoutWidth = constraints.maxWidth
|
||||
val layoutHeight = constraints.maxHeight
|
||||
val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)
|
||||
|
||||
val sheetPlaceables = bottomSheetMeasurables.fastMap { it.measure(looseConstraints) }
|
||||
|
||||
val topBarPlaceables = topBarMeasurables.fastMap { it.measure(looseConstraints) }
|
||||
val topBarHeight = topBarPlaceables.fastMaxOfOrNull { it.height } ?: 0
|
||||
|
||||
val bodyConstraints = looseConstraints.copy(maxHeight = layoutHeight - topBarHeight)
|
||||
val bodyPlaceables = bodyMeasurables.fastMap { it.measure(bodyConstraints) }
|
||||
|
||||
val snackbarPlaceables = snackbarHostMeasurables.fastMap { it.measure(looseConstraints) }
|
||||
|
||||
layout(layoutWidth, layoutHeight) {
|
||||
val sheetWidth = sheetPlaceables.fastMaxOfOrNull { it.width } ?: 0
|
||||
val sheetOffsetX = Integer.max(0, (layoutWidth - sheetWidth) / 2)
|
||||
|
||||
val snackbarWidth = snackbarPlaceables.fastMaxOfOrNull { it.width } ?: 0
|
||||
val snackbarHeight = snackbarPlaceables.fastMaxOfOrNull { it.height } ?: 0
|
||||
val snackbarOffsetX = (layoutWidth - snackbarWidth) / 2
|
||||
val snackbarOffsetY =
|
||||
when (sheetState.currentValue) {
|
||||
PartiallyExpanded -> sheetOffset().roundToInt() - snackbarHeight
|
||||
Expanded,
|
||||
Hidden,
|
||||
-> layoutHeight - snackbarHeight
|
||||
}
|
||||
|
||||
// Placement order is important for elevation
|
||||
bodyPlaceables.fastForEach { it.placeRelative(0, topBarHeight) }
|
||||
topBarPlaceables.fastForEach { it.placeRelative(0, 0) }
|
||||
sheetPlaceables.fastForEach { it.placeRelative(sheetOffsetX, 0) }
|
||||
snackbarPlaceables.fastForEach { it.placeRelative(snackbarOffsetX, snackbarOffsetY) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,339 @@
|
|||
@file:Suppress("all")
|
||||
|
||||
package com.tangem.core.ui.components.sheetscaffold
|
||||
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.gestures.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.saveable.Saver
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.Velocity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.sheetscaffold.TangemSheetValue.*
|
||||
import kotlinx.coroutines.CancellationException
|
||||
|
||||
/**
|
||||
* State of a sheet composable
|
||||
*
|
||||
* Contains states relating to its swipe position as well as animations between state values.
|
||||
*
|
||||
* @param skipPartiallyExpanded Whether the partially expanded state, if the sheet is large enough,
|
||||
* should be skipped. If true, the sheet will always expand to the [Expanded] state and move to
|
||||
* the [Hidden] state if available when hiding the sheet, either programmatically or by user
|
||||
* interaction.
|
||||
* @param initialValue The initial value of the state.
|
||||
* @param density The density that this state can use to convert values to and from dp.
|
||||
* @param confirmValueChange Optional callback invoked to confirm or veto a pending state change.
|
||||
* @param skipHiddenState Whether the hidden state should be skipped. If true, the sheet will always
|
||||
* expand to the [Expanded] state and move to the [PartiallyExpanded] if available, either
|
||||
* programmatically or by user interaction.
|
||||
*/
|
||||
@Stable
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
class TangemSheetState(
|
||||
internal val skipPartiallyExpanded: Boolean,
|
||||
density: Density,
|
||||
initialValue: TangemSheetValue = Hidden,
|
||||
confirmValueChange: (TangemSheetValue) -> Boolean = { true },
|
||||
internal val skipHiddenState: Boolean = false,
|
||||
) {
|
||||
init {
|
||||
if (skipPartiallyExpanded) {
|
||||
require(initialValue != PartiallyExpanded) {
|
||||
"The initial value must not be set to PartiallyExpanded if skipPartiallyExpanded " +
|
||||
"is set to true."
|
||||
}
|
||||
}
|
||||
if (skipHiddenState) {
|
||||
require(initialValue != Hidden) {
|
||||
"The initial value must not be set to Hidden if skipHiddenState is set to true."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The current value of the state.
|
||||
*
|
||||
* If no swipe or animation is in progress, this corresponds to the state the bottom sheet is
|
||||
* currently in. If a swipe or an animation is in progress, this corresponds the state the sheet
|
||||
* was in before the swipe or animation started.
|
||||
*/
|
||||
val currentValue: TangemSheetValue
|
||||
get() = anchoredDraggableState.currentValue
|
||||
|
||||
/**
|
||||
* The target value of the bottom sheet state.
|
||||
*
|
||||
* If a swipe is in progress, this is the value that the sheet would animate to if the swipe
|
||||
* finishes. If an animation is running, this is the target value of that animation. Finally, if
|
||||
* no swipe or animation is in progress, this is the same as the [currentValue].
|
||||
*/
|
||||
val targetValue: TangemSheetValue
|
||||
get() = anchoredDraggableState.targetValue
|
||||
|
||||
/** Whether the modal bottom sheet is visible. */
|
||||
val isVisible: Boolean
|
||||
get() = anchoredDraggableState.currentValue != Hidden
|
||||
|
||||
/**
|
||||
* Require the current offset (in pixels) of the bottom sheet.
|
||||
*
|
||||
* The offset will be initialized during the first measurement phase of the provided sheet
|
||||
* content.
|
||||
*
|
||||
* These are the phases: Composition { -> Effects } -> Layout { Measurement -> Placement } ->
|
||||
* Drawing
|
||||
*
|
||||
* During the first composition, an [IllegalStateException] is thrown. In subsequent
|
||||
* compositions, the offset will be derived from the anchors of the previous pass. Always prefer
|
||||
* accessing the offset from a LaunchedEffect as it will be scheduled to be executed the next
|
||||
* frame, after layout.
|
||||
*
|
||||
* @throws IllegalStateException If the offset has not been initialized yet
|
||||
*/
|
||||
fun requireOffset(): Float = anchoredDraggableState.requireOffset()
|
||||
|
||||
/** Whether the sheet has an expanded state defined. */
|
||||
val hasExpandedState: Boolean
|
||||
get() = anchoredDraggableState.anchors.hasAnchorFor(Expanded)
|
||||
|
||||
/** Whether the modal bottom sheet has a partially expanded state defined. */
|
||||
val hasPartiallyExpandedState: Boolean
|
||||
get() = anchoredDraggableState.anchors.hasAnchorFor(PartiallyExpanded)
|
||||
|
||||
/**
|
||||
* Fully expand the bottom sheet with animation and suspend until it is fully expanded or
|
||||
* animation has been cancelled.
|
||||
* *
|
||||
*
|
||||
* @throws [CancellationException] if the animation is interrupted
|
||||
*/
|
||||
suspend fun expand() {
|
||||
anchoredDraggableState.animateTo(Expanded)
|
||||
}
|
||||
|
||||
/**
|
||||
* Animate the bottom sheet and suspend until it is partially expanded or animation has been
|
||||
* cancelled.
|
||||
*
|
||||
* @throws [CancellationException] if the animation is interrupted
|
||||
* @throws [IllegalStateException] if [skipPartiallyExpanded] is set to true
|
||||
*/
|
||||
suspend fun partialExpand() {
|
||||
check(!skipPartiallyExpanded) {
|
||||
"Attempted to animate to partial expanded when skipPartiallyExpanded was enabled. Set" +
|
||||
" skipPartiallyExpanded to false to use this function."
|
||||
}
|
||||
animateTo(PartiallyExpanded)
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand the bottom sheet with animation and suspend until it is [PartiallyExpanded] if defined
|
||||
* else [Expanded].
|
||||
*
|
||||
* @throws [CancellationException] if the animation is interrupted
|
||||
*/
|
||||
suspend fun show() {
|
||||
val targetValue =
|
||||
when {
|
||||
hasPartiallyExpandedState -> PartiallyExpanded
|
||||
else -> Expanded
|
||||
}
|
||||
animateTo(targetValue)
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the bottom sheet with animation and suspend until it is fully hidden or animation has
|
||||
* been cancelled.
|
||||
*
|
||||
* @throws [CancellationException] if the animation is interrupted
|
||||
*/
|
||||
suspend fun hide() {
|
||||
check(!skipHiddenState) {
|
||||
"Attempted to animate to hidden when skipHiddenState was enabled. Set skipHiddenState" +
|
||||
" to false to use this function."
|
||||
}
|
||||
animateTo(Hidden)
|
||||
}
|
||||
|
||||
/**
|
||||
* Animate to a [targetValue]. If the [targetValue] is not in the set of anchors, the
|
||||
* [currentValue] will be updated to the [targetValue] without updating the offset.
|
||||
*
|
||||
* @param targetValue The target value of the animation
|
||||
* @throws CancellationException if the interaction interrupted by another interaction like a
|
||||
* gesture interaction or another programmatic interaction like a [animateTo] or [snapTo]
|
||||
* call.
|
||||
*/
|
||||
internal suspend fun animateTo(
|
||||
targetValue: TangemSheetValue,
|
||||
velocity: Float = anchoredDraggableState.lastVelocity,
|
||||
) {
|
||||
anchoredDraggableState.animateToWithDecay(targetValue, velocity)
|
||||
}
|
||||
|
||||
/**
|
||||
* Snap to a [targetValue] without any animation.
|
||||
*
|
||||
* @param targetValue The target value of the animation
|
||||
* @throws CancellationException if the interaction interrupted by another interaction like a
|
||||
* gesture interaction or another programmatic interaction like a [animateTo] or [snapTo]
|
||||
* call.
|
||||
*/
|
||||
internal suspend fun snapTo(targetValue: TangemSheetValue) {
|
||||
anchoredDraggableState.snapTo(targetValue)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the closest anchor taking into account the velocity and settle at it with an animation.
|
||||
*/
|
||||
internal suspend fun settle(velocity: Float) {
|
||||
anchoredDraggableState.settle(velocity)
|
||||
}
|
||||
|
||||
internal var anchoredDraggableState =
|
||||
AnchoredDraggableState(
|
||||
initialValue = initialValue,
|
||||
snapAnimationSpec = BottomSheetAnimationSpec,
|
||||
decayAnimationSpec = exponentialDecay(
|
||||
frictionMultiplier = 10f,
|
||||
absVelocityThreshold = 0.5f,
|
||||
),
|
||||
confirmValueChange = confirmValueChange,
|
||||
positionalThreshold = { with(density) { 56.dp.toPx() } },
|
||||
velocityThreshold = { with(density) { 125.dp.toPx() } },
|
||||
)
|
||||
|
||||
internal val offset: Float
|
||||
get() = anchoredDraggableState.offset
|
||||
|
||||
companion object {
|
||||
/** The default [Saver] implementation for [TangemSheetState]. */
|
||||
fun Saver(
|
||||
skipPartiallyExpanded: Boolean,
|
||||
confirmValueChange: (TangemSheetValue) -> Boolean,
|
||||
density: Density,
|
||||
skipHiddenState: Boolean,
|
||||
) = Saver<TangemSheetState, TangemSheetValue>(
|
||||
save = { it.currentValue },
|
||||
restore = { savedValue ->
|
||||
TangemSheetState(
|
||||
skipPartiallyExpanded,
|
||||
density,
|
||||
savedValue,
|
||||
confirmValueChange,
|
||||
skipHiddenState,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Possible values of [TangemSheetState]. */
|
||||
enum class TangemSheetValue {
|
||||
/** The sheet is not visible. */
|
||||
Hidden,
|
||||
|
||||
/** The sheet is visible at full height. */
|
||||
Expanded,
|
||||
|
||||
/** The sheet is partially visible. */
|
||||
PartiallyExpanded,
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
internal fun consumeSwipeWithinBottomSheetBoundsNestedScrollConnection(
|
||||
sheetState: TangemSheetState,
|
||||
orientation: Orientation,
|
||||
onFling: (velocity: Float) -> Unit,
|
||||
): NestedScrollConnection = object : NestedScrollConnection {
|
||||
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
||||
val delta = available.toFloat()
|
||||
return if (delta < 0 && source == NestedScrollSource.UserInput) {
|
||||
sheetState.anchoredDraggableState.dispatchRawDelta(delta).toOffset()
|
||||
} else {
|
||||
Offset.Zero
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
|
||||
return if (source == NestedScrollSource.UserInput) {
|
||||
sheetState.anchoredDraggableState.dispatchRawDelta(available.toFloat()).toOffset()
|
||||
} else {
|
||||
Offset.Zero
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun onPreFling(available: Velocity): Velocity {
|
||||
val toFling = available.toFloat()
|
||||
val currentOffset = sheetState.requireOffset()
|
||||
val minAnchor = sheetState.anchoredDraggableState.anchors.minAnchor()
|
||||
return if (toFling < 0 && currentOffset > minAnchor) {
|
||||
onFling(toFling)
|
||||
// since we go to the anchor with tween settling, consume all for the best UX
|
||||
available
|
||||
} else {
|
||||
Velocity.Zero
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
|
||||
onFling(available.toFloat())
|
||||
return available
|
||||
}
|
||||
|
||||
private fun Float.toOffset(): Offset = Offset(
|
||||
x = if (orientation == Orientation.Horizontal) this else 0f,
|
||||
y = if (orientation == Orientation.Vertical) this else 0f,
|
||||
)
|
||||
|
||||
@JvmName("velocityToFloat")
|
||||
private fun Velocity.toFloat() = if (orientation == Orientation.Horizontal) x else y
|
||||
|
||||
@JvmName("offsetToFloat")
|
||||
private fun Offset.toFloat(): Float = if (orientation == Orientation.Horizontal) x else y
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun rememberSheetState(
|
||||
skipPartiallyExpanded: Boolean = false,
|
||||
confirmValueChange: (TangemSheetValue) -> Boolean = { true },
|
||||
initialValue: TangemSheetValue = Hidden,
|
||||
skipHiddenState: Boolean = false,
|
||||
): TangemSheetState {
|
||||
val density = LocalDensity.current
|
||||
return rememberSaveable(
|
||||
skipPartiallyExpanded,
|
||||
confirmValueChange,
|
||||
skipHiddenState,
|
||||
saver =
|
||||
TangemSheetState.Saver(
|
||||
skipPartiallyExpanded = skipPartiallyExpanded,
|
||||
confirmValueChange = confirmValueChange,
|
||||
density = density,
|
||||
skipHiddenState = skipHiddenState,
|
||||
),
|
||||
) {
|
||||
TangemSheetState(
|
||||
skipPartiallyExpanded,
|
||||
density,
|
||||
initialValue,
|
||||
confirmValueChange,
|
||||
skipHiddenState,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** The default animation spec used by [TangemSheetState]. */
|
||||
private val BottomSheetAnimationSpec: AnimationSpec<Float> =
|
||||
spring(
|
||||
dampingRatio = Spring.DampingRatioNoBouncy,
|
||||
stiffness = Spring.StiffnessMedium,
|
||||
)
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
package com.tangem.core.ui.reordarable
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.lazy.LazyItemScope
|
||||
import androidx.compose.foundation.lazy.grid.LazyGridItemScope
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.zIndex
|
||||
import org.burnoutcrew.reorderable.ReorderableState
|
||||
|
||||
/**
|
||||
* Fixed version of ReorderableItem from reorderable library.
|
||||
* The original version of ReorderableItem is somehow causing crashes.
|
||||
* Probably because of compose compiler's bug and new foundation `animateItem` API.
|
||||
* @see [org.burnoutcrew.reorderable.ReorderableItem]
|
||||
*/
|
||||
@Suppress("ComposableFunctionName")
|
||||
@Composable
|
||||
inline fun LazyItemScope.ReorderableItem(
|
||||
reorderableState: ReorderableState<*>,
|
||||
key: Any?,
|
||||
modifier: Modifier = Modifier,
|
||||
index: Int? = null,
|
||||
orientationLocked: Boolean = true,
|
||||
content: @Composable BoxScope.(isDragging: Boolean) -> Unit,
|
||||
) = ReorderableItem(reorderableState, key, modifier, Modifier.animateItem(), orientationLocked, index, content)
|
||||
|
||||
/**
|
||||
* Fixed version of ReorderableItem from reorderable library.
|
||||
* The original version of ReorderableItem is somehow causing crashes.
|
||||
* Probably because of compose compiler's bug and new foundation `animateItem` API.
|
||||
* @see [org.burnoutcrew.reorderable.ReorderableItem]
|
||||
*/
|
||||
@Suppress("ComposableFunctionName")
|
||||
@Composable
|
||||
inline fun LazyGridItemScope.ReorderableItem(
|
||||
reorderableState: ReorderableState<*>,
|
||||
key: Any?,
|
||||
modifier: Modifier = Modifier,
|
||||
index: Int? = null,
|
||||
content: @Composable BoxScope.(isDragging: Boolean) -> Unit,
|
||||
) = ReorderableItem(reorderableState, key, modifier, Modifier.animateItem(), false, index, content)
|
||||
|
||||
/**
|
||||
* Fixed version of ReorderableItem from reorderable library.
|
||||
* The original version of ReorderableItem is somehow causing crashes.
|
||||
* Probably because of compose compiler's bug and new foundation `animateItem` API.
|
||||
* @see [org.burnoutcrew.reorderable.ReorderableItem]
|
||||
*/
|
||||
@Composable
|
||||
inline fun ReorderableItem(
|
||||
state: ReorderableState<*>,
|
||||
key: Any?,
|
||||
modifier: Modifier = Modifier,
|
||||
defaultDraggingModifier: Modifier = Modifier,
|
||||
orientationLocked: Boolean = true,
|
||||
index: Int? = null,
|
||||
content: @Composable BoxScope.(isDragging: Boolean) -> Unit,
|
||||
) {
|
||||
val isDragging = if (index != null) {
|
||||
index == state.draggingItemIndex
|
||||
} else {
|
||||
key == state.draggingItemKey
|
||||
}
|
||||
val draggingModifier =
|
||||
if (isDragging) {
|
||||
Modifier
|
||||
.zIndex(1f)
|
||||
.graphicsLayer {
|
||||
translationX = if (!orientationLocked || !state.isVerticalScroll) state.draggingItemLeft else 0f
|
||||
translationY = if (!orientationLocked || state.isVerticalScroll) state.draggingItemTop else 0f
|
||||
}
|
||||
} else {
|
||||
val cancel = if (index != null) {
|
||||
index == state.dragCancelledAnimation.position?.index
|
||||
} else {
|
||||
key == state.dragCancelledAnimation.position?.key
|
||||
}
|
||||
if (cancel) {
|
||||
Modifier.zIndex(1f)
|
||||
.graphicsLayer {
|
||||
translationX = if (!orientationLocked || !state.isVerticalScroll) {
|
||||
state.dragCancelledAnimation.offset.x
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
translationY = if (!orientationLocked || state.isVerticalScroll) {
|
||||
state.dragCancelledAnimation.offset.y
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
}
|
||||
} else {
|
||||
defaultDraggingModifier
|
||||
}
|
||||
}
|
||||
Box(modifier = modifier.then(draggingModifier)) {
|
||||
content(isDragging)
|
||||
}
|
||||
}
|
||||
|
|
@ -36,7 +36,6 @@ import com.tangem.core.ui.extensions.resourceReference
|
|||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.utils.disableNestedScroll
|
||||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import com.tangem.features.markets.details.impl.ui.components.InfoBottomSheet
|
||||
import com.tangem.features.markets.details.impl.ui.components.MarketTokenDetailsChart
|
||||
|
|
@ -105,7 +104,6 @@ private fun Content(
|
|||
SpacerH4()
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.disableNestedScroll(),
|
||||
contentPadding = PaddingValues(bottom = bottomBarHeight),
|
||||
) {
|
||||
item("header") {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import com.tangem.core.ui.components.SpacerW8
|
|||
import com.tangem.core.ui.components.TextShimmer
|
||||
import com.tangem.core.ui.components.block.information.InformationBlock
|
||||
import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons
|
||||
import com.tangem.core.ui.components.progressbar.LinearProgressIndicator
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemAnimations
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
|
@ -119,7 +120,7 @@ private fun Content(state: PricePerformanceUM.Value, modifier: Modifier = Modifi
|
|||
.fillMaxWidth(),
|
||||
progress = { animatedIndicatorFraction },
|
||||
color = TangemTheme.colors.text.accent,
|
||||
trackColor = TangemTheme.colors.background.tertiary,
|
||||
backgroundColor = TangemTheme.colors.background.tertiary,
|
||||
strokeCap = StrokeCap.Round,
|
||||
)
|
||||
Row(
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ private fun Content(state: MarketsListUM, onHeaderSizeChange: (Dp) -> Unit, modi
|
|||
.padding(
|
||||
start = TangemTheme.dimens.spacing16,
|
||||
end = TangemTheme.dimens.spacing16,
|
||||
bottom = TangemTheme.dimens.spacing12,
|
||||
bottom = 8.dp,
|
||||
)
|
||||
.onGloballyPositioned {
|
||||
if (it.size.height > 0) {
|
||||
|
|
@ -95,7 +95,8 @@ private fun Content(state: MarketsListUM, onHeaderSizeChange: (Dp) -> Unit, modi
|
|||
onHeaderSizeChange(it.size.height.toDp())
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
.padding(bottom = 4.dp),
|
||||
state = state.searchBar,
|
||||
)
|
||||
Column(Modifier.padding(horizontal = TangemTheme.dimens.size16)) {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ import com.tangem.core.ui.components.list.InfiniteListHandler
|
|||
import com.tangem.core.ui.event.EventEffect
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.utils.disableNestedScroll
|
||||
import com.tangem.features.markets.impl.R
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.state.ListUM
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -52,7 +51,7 @@ internal fun MarketsListLazyColumn(
|
|||
|
||||
if (state is ListUM.Loading) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.disableNestedScroll(),
|
||||
modifier = modifier,
|
||||
state = rememberLazyListState(),
|
||||
contentPadding = PaddingValues(bottom = bottomBarHeight),
|
||||
userScrollEnabled = false,
|
||||
|
|
@ -63,7 +62,7 @@ internal fun MarketsListLazyColumn(
|
|||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = modifier.disableNestedScroll(),
|
||||
modifier = modifier,
|
||||
state = lazyListState,
|
||||
contentPadding = PaddingValues(bottom = bottomBarHeight),
|
||||
userScrollEnabled = true,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
package com.tangem.feature.referral.ui
|
||||
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.statusBars
|
||||
import androidx.compose.foundation.layout.systemBars
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
|
|
@ -11,7 +8,6 @@ import androidx.compose.material3.SheetState
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import com.tangem.core.ui.res.LocalWindowSize
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.utils.WindowInsetsZero
|
||||
import com.tangem.feature.referral.models.ReferralStateHolder
|
||||
|
|
@ -24,19 +20,18 @@ internal fun ReferralBottomSheet(
|
|||
onDismissRequest: () -> Unit,
|
||||
config: ReferralStateHolder.ReferralInfoState,
|
||||
) {
|
||||
val statusBarHeight = with(LocalDensity.current) { WindowInsets.statusBars.getTop(this).toDp() }
|
||||
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
|
||||
|
||||
if (isVisible) {
|
||||
ModalBottomSheet(
|
||||
modifier = Modifier.height(LocalWindowSize.current.height - statusBarHeight),
|
||||
modifier = Modifier.statusBarsPadding(),
|
||||
onDismissRequest = onDismissRequest,
|
||||
sheetState = sheetState,
|
||||
shape = RoundedCornerShape(
|
||||
topStart = TangemTheme.dimens.radius16,
|
||||
topEnd = TangemTheme.dimens.radius16,
|
||||
),
|
||||
windowInsets = WindowInsetsZero,
|
||||
contentWindowInsets = { WindowInsetsZero },
|
||||
containerColor = TangemTheme.colors.background.primary,
|
||||
) {
|
||||
AgreementBottomSheetContent(
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@ import androidx.compose.material.ExperimentalMaterialApi
|
|||
import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
||||
import androidx.compose.material.pullrefresh.pullRefresh
|
||||
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
|
|
@ -196,7 +196,7 @@ private fun BannerBlock(onClick: () -> Unit) {
|
|||
.clip(RoundedCornerShape(size = TangemTheme.dimens.radius14))
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(),
|
||||
indication = ripple(),
|
||||
onClick = onClick,
|
||||
),
|
||||
) {
|
||||
|
|
@ -258,7 +258,7 @@ private fun StakingRewardBlock(
|
|||
.background(TangemTheme.colors.background.action)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(),
|
||||
indication = ripple(),
|
||||
enabled = rewardBlockType == RewardBlockType.Rewards,
|
||||
onClick = onRewardsClick,
|
||||
),
|
||||
|
|
@ -286,7 +286,7 @@ private fun ActiveStakingBlock(
|
|||
.background(TangemTheme.colors.background.action)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(),
|
||||
indication = ripple(),
|
||||
enabled = balance.isClickable,
|
||||
onClick = {
|
||||
onAnalytic()
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import androidx.compose.foundation.clickable
|
|||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
@ -28,7 +28,7 @@ internal fun ValidatorBlock(validatorState: ValidatorState, onClick: () -> Unit)
|
|||
.clickable(
|
||||
enabled = validatorState.isClickable,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(),
|
||||
indication = ripple(),
|
||||
onClick = onClick,
|
||||
),
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import androidx.compose.material.Icon
|
|||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
@ -103,7 +103,7 @@ fun TransactionCard(
|
|||
.height(TangemTheme.dimens.size116)
|
||||
.width(TangemTheme.dimens.size102)
|
||||
.clickable(
|
||||
indication = rememberRipple(bounded = false),
|
||||
indication = ripple(bounded = false),
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
) { onChangeTokenClick() },
|
||||
)
|
||||
|
|
@ -164,7 +164,7 @@ fun TransactionCardEmpty(
|
|||
.height(TangemTheme.dimens.size116)
|
||||
.width(TangemTheme.dimens.size102)
|
||||
.clickable(
|
||||
indication = rememberRipple(bounded = false),
|
||||
indication = ripple(bounded = false),
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
) { onChangeTokenClick() },
|
||||
)
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ import androidx.compose.foundation.background
|
|||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
|
|
@ -42,7 +42,7 @@ internal fun StakingBalanceBlock(
|
|||
.background(TangemTheme.colors.background.primary)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(),
|
||||
indication = ripple(),
|
||||
onClick = state.onStakeClicked,
|
||||
)
|
||||
.padding(TangemTheme.dimens.spacing12),
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import com.tangem.core.ui.components.buttons.actions.RoundedActionButton
|
|||
import com.tangem.core.ui.components.token.TokenItem
|
||||
import com.tangem.core.ui.event.EventEffect
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.reordarable.ReorderableItem
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.utils.WindowInsetsZero
|
||||
|
|
@ -43,7 +44,6 @@ import com.tangem.feature.wallet.presentation.common.component.DraggableNetworkT
|
|||
import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem
|
||||
import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState
|
||||
import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState
|
||||
import org.burnoutcrew.reorderable.ReorderableItem
|
||||
import org.burnoutcrew.reorderable.ReorderableLazyListState
|
||||
import org.burnoutcrew.reorderable.rememberReorderableLazyListState
|
||||
import org.burnoutcrew.reorderable.reorderable
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import androidx.compose.material.ExperimentalMaterialApi
|
|||
import androidx.compose.material.pullrefresh.pullRefresh
|
||||
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.material3.BottomSheetDefaults
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
@ -44,9 +45,7 @@ import androidx.compose.ui.unit.IntOffset
|
|||
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.*
|
||||
import com.tangem.core.ui.components.atoms.Hand
|
||||
import com.tangem.core.ui.components.atoms.handComposableComponentHeight
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
|
|
@ -54,7 +53,7 @@ import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBot
|
|||
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.sheetscaffold.*
|
||||
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
|
||||
|
|
@ -146,7 +145,7 @@ private fun WalletContent(
|
|||
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
val scaffoldContent: @Composable () -> Unit = {
|
||||
val scaffoldContent: @Composable (PaddingValues?) -> Unit = { paddingValues ->
|
||||
val movableItemModifier = Modifier.changeWalletAnimator(walletsListState)
|
||||
|
||||
val lazyTxHistoryItems = (selectedWallet as? TxHistoryStateHolder)?.let { walletState ->
|
||||
|
|
@ -165,12 +164,20 @@ private fun WalletContent(
|
|||
|
||||
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
|
||||
|
||||
val marketHintAproxHeight = with(LocalDensity.current) {
|
||||
TangemTheme.typography.caption2.lineHeight.toDp() * 2
|
||||
} + 40.dp
|
||||
|
||||
val contentPadding = paddingValues?.let {
|
||||
PaddingValues(
|
||||
bottom = it.calculateBottomPadding() + marketHintAproxHeight + 52.dp,
|
||||
)
|
||||
} ?: PaddingValues(bottom = TangemTheme.dimens.spacing92 + bottomBarHeight)
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.testTag(TestTags.MAIN_SCREEN),
|
||||
state = listState,
|
||||
contentPadding = PaddingValues(
|
||||
bottom = 184.dp + bottomBarHeight,
|
||||
),
|
||||
contentPadding = contentPadding,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
item(
|
||||
|
|
@ -262,7 +269,7 @@ private fun WalletContent(
|
|||
state = state,
|
||||
selectedWallet = selectedWallet,
|
||||
snackbarHostState = snackbarHostState,
|
||||
content = scaffoldContent,
|
||||
content = { scaffoldContent(null) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -345,29 +352,13 @@ private inline fun BaseScaffoldWithMarkets(
|
|||
crossinline bottomSheetContent: @Composable () -> Unit,
|
||||
alertConfig: WalletAlertState?,
|
||||
noinline onBottomSheetStateChange: (BottomSheetState) -> Unit,
|
||||
crossinline content: @Composable () -> Unit,
|
||||
crossinline content: @Composable (PaddingValues) -> 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 = remember(showManageTokensBottomSheet) {
|
||||
{ sheetValue ->
|
||||
when {
|
||||
sheetValue == SheetValue.Hidden && showManageTokensBottomSheet -> false
|
||||
sheetValue != SheetValue.Hidden && !showManageTokensBottomSheet -> false
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
},
|
||||
skipHiddenState = showManageTokensBottomSheet,
|
||||
)
|
||||
val bottomSheetState = rememberTangemStandardBottomSheetState()
|
||||
|
||||
val keyboardShown = keyboardAsState()
|
||||
val isKeyboardVisible by rememberIsKeyboardVisible()
|
||||
|
||||
val scaffoldState = rememberBottomSheetScaffoldState(
|
||||
val scaffoldState = rememberTangemBottomSheetScaffoldState(
|
||||
bottomSheetState = bottomSheetState,
|
||||
snackbarHostState = snackbarHostState,
|
||||
)
|
||||
|
|
@ -398,13 +389,11 @@ private inline fun BaseScaffoldWithMarkets(
|
|||
|
||||
BottomSheetStateEffects(
|
||||
bottomSheetState = bottomSheetState,
|
||||
showManageTokensBottomSheet = showManageTokensBottomSheet,
|
||||
alertConfig = alertConfig,
|
||||
keyboardShown = keyboardShown,
|
||||
onBottomSheetStateChange = onBottomSheetStateChange,
|
||||
)
|
||||
|
||||
BottomSheetScaffold(
|
||||
TangemBottomSheetScaffold(
|
||||
snackbarHost = {
|
||||
WalletSnackbarHost(
|
||||
snackbarHostState = it,
|
||||
|
|
@ -418,27 +407,26 @@ private inline fun BaseScaffoldWithMarkets(
|
|||
sheetContainerColor = backgroundColor.value,
|
||||
scaffoldState = scaffoldState,
|
||||
sheetPeekHeight = peekHeight,
|
||||
sheetDragHandle = {
|
||||
Hand(modifier = Modifier.background(color = backgroundColor.value))
|
||||
},
|
||||
sheetTonalElevation = 8.dp,
|
||||
sheetShadowElevation = 8.dp,
|
||||
sheetShape = TangemTheme.shapes.bottomSheetLarge,
|
||||
sheetContent = {
|
||||
Box(
|
||||
modifier = Modifier.sizeIn(maxHeight = maxHeight - statusBarHeight - handComposableComponentHeight),
|
||||
) {
|
||||
bottomSheetContent()
|
||||
}
|
||||
|
||||
// hide bottom sheet when back pressed
|
||||
BackHandler(
|
||||
keyboardShown.value is Keyboard.Closed &&
|
||||
bottomSheetState.currentValue == SheetValue.Expanded,
|
||||
isKeyboardVisible.not() &&
|
||||
bottomSheetState.currentValue == TangemSheetValue.Expanded,
|
||||
) {
|
||||
coroutineScope.launch { bottomSheetState.partialExpand() }
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.sizeIn(maxHeight = maxHeight - statusBarHeight),
|
||||
) {
|
||||
Hand(Modifier.drawBehind { drawRect(backgroundColor.value) })
|
||||
bottomSheetContent()
|
||||
}
|
||||
},
|
||||
content = { _ ->
|
||||
content = { paddingValues ->
|
||||
val pullRefreshState = rememberPullRefreshState(
|
||||
refreshing = selectedWallet.pullToRefreshConfig.isRefreshing,
|
||||
onRefresh = {
|
||||
|
|
@ -456,13 +444,11 @@ private inline fun BaseScaffoldWithMarkets(
|
|||
)
|
||||
|
||||
Column {
|
||||
WalletTopBar(
|
||||
config = state.topBarConfig,
|
||||
)
|
||||
WalletTopBar(config = state.topBarConfig)
|
||||
Box(
|
||||
modifier = Modifier.pullRefresh(pullRefreshState),
|
||||
) {
|
||||
content()
|
||||
content(paddingValues)
|
||||
|
||||
WalletPullToRefreshIndicator(
|
||||
isRefreshing = selectedWallet.pullToRefreshConfig.isRefreshing,
|
||||
|
|
@ -474,7 +460,8 @@ private inline fun BaseScaffoldWithMarkets(
|
|||
|
||||
BottomSheetScrim(
|
||||
color = BottomSheetDefaults.ScrimColor,
|
||||
visible = bottomSheetState.targetValue == SheetValue.Expanded || state.showMarketsOnboarding,
|
||||
visible = bottomSheetState.targetValue == TangemSheetValue.Expanded ||
|
||||
state.showMarketsOnboarding,
|
||||
onDismissRequest = { coroutineScope.launch { bottomSheetState.partialExpand() } },
|
||||
)
|
||||
|
||||
|
|
@ -492,7 +479,7 @@ private inline fun BaseScaffoldWithMarkets(
|
|||
)
|
||||
|
||||
LaunchedEffect(state.showMarketsOnboarding, bottomSheetState.targetValue) {
|
||||
if (state.showMarketsOnboarding && bottomSheetState.targetValue == SheetValue.Expanded) {
|
||||
if (state.showMarketsOnboarding && bottomSheetState.targetValue == TangemSheetValue.Expanded) {
|
||||
state.onDismissMarketsOnboarding()
|
||||
}
|
||||
}
|
||||
|
|
@ -503,7 +490,7 @@ private inline fun BaseScaffoldWithMarkets(
|
|||
@Composable
|
||||
private fun MarketsTooltip(
|
||||
availableHeight: Dp,
|
||||
bottomSheetState: SheetState,
|
||||
bottomSheetState: TangemSheetState,
|
||||
isVisible: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
|
|
@ -532,7 +519,7 @@ private fun MarketsTooltip(
|
|||
|
||||
val slideOffset = 40.dp.toPx()
|
||||
AnimatedVisibility(
|
||||
modifier = modifier.absoluteOffset(y = tooltipOffset),
|
||||
modifier = modifier.offset { IntOffset(x = 0, y = tooltipOffset.roundToPx()) },
|
||||
visible = visible,
|
||||
enter = slideIn(
|
||||
animationSpec = spring(
|
||||
|
|
@ -560,7 +547,7 @@ internal fun MarketsHint(isVisible: Boolean, modifier: Modifier = Modifier) {
|
|||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(
|
||||
text = "To add tokens pull this up or tap the search bar",
|
||||
text = stringResource(R.string.markets_hint),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
textAlign = TextAlign.Center,
|
||||
|
|
@ -647,70 +634,46 @@ private fun BottomSheetScrim(color: Color, visible: Boolean, onDismissRequest: (
|
|||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Suppress("CyclomaticComplexMethod", "MagicNumber", "LongMethod")
|
||||
@Composable
|
||||
private fun BottomSheetStateEffects(
|
||||
bottomSheetState: SheetState,
|
||||
showManageTokensBottomSheet: Boolean,
|
||||
bottomSheetState: TangemSheetState,
|
||||
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,
|
||||
TangemSheetValue.Hidden,
|
||||
TangemSheetValue.Expanded,
|
||||
-> systemUiController.setNavigationBarColor(
|
||||
color = Color.Transparent,
|
||||
darkIcons = navigationBarColor.luminance() > 0.5f,
|
||||
navigationBarContrastEnforced = true,
|
||||
)
|
||||
SheetValue.PartiallyExpanded,
|
||||
TangemSheetValue.PartiallyExpanded,
|
||||
-> systemUiController.setNavigationBarColor(navigationBarColor)
|
||||
}
|
||||
}
|
||||
|
||||
DisposableEffect(showManageTokensBottomSheet) {
|
||||
// make navigation bar transparent when leaving the screen
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
if (showManageTokensBottomSheet) {
|
||||
systemUiController.setNavigationBarColor(
|
||||
color = Color.Transparent,
|
||||
darkIcons = navigationBarColor.luminance() > 0.5f,
|
||||
navigationBarContrastEnforced = false,
|
||||
)
|
||||
}
|
||||
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) {
|
||||
val isKeyboardVisible by rememberIsKeyboardVisible()
|
||||
|
||||
LaunchedEffect(isKeyboardVisible) {
|
||||
if (isKeyboardVisible && alertConfig == null) {
|
||||
bottomSheetState.expand()
|
||||
}
|
||||
}
|
||||
|
|
@ -719,8 +682,8 @@ private fun BottomSheetStateEffects(
|
|||
// hide keyboard when bottom sheet is about to be hidden
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow {
|
||||
bottomSheetState.currentValue == SheetValue.Expanded &&
|
||||
bottomSheetState.targetValue == SheetValue.PartiallyExpanded
|
||||
bottomSheetState.currentValue == TangemSheetValue.Expanded &&
|
||||
bottomSheetState.targetValue == TangemSheetValue.PartiallyExpanded
|
||||
}.collect { sheetHasBeenHidden ->
|
||||
if (sheetHasBeenHidden) {
|
||||
keyboardController?.hide()
|
||||
|
|
@ -728,7 +691,7 @@ private fun BottomSheetStateEffects(
|
|||
}
|
||||
}
|
||||
|
||||
val isSheetHidden = bottomSheetState.targetValue == SheetValue.PartiallyExpanded
|
||||
val isSheetHidden = bottomSheetState.targetValue == TangemSheetValue.PartiallyExpanded
|
||||
LaunchedEffect(isSheetHidden) {
|
||||
onBottomSheetStateChange(
|
||||
if (isSheetHidden) {
|
||||
|
|
@ -740,30 +703,6 @@ private fun BottomSheetStateEffects(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
|
|
|
|||
|
|
@ -26,10 +26,10 @@ androidxWindowManager = "1.3.0"
|
|||
|
||||
# region Compose
|
||||
compose-compiler = "1.5.9"
|
||||
compose-runtime = "1.6.1"
|
||||
compose-foundation = "1.6.1"
|
||||
compose-material = "1.6.4"
|
||||
compose-material3 = "1.2.0"
|
||||
compose-runtime = "1.7.1"
|
||||
compose-foundation = "1.7.1"
|
||||
compose-material = "1.7.1"
|
||||
compose-material3 = "1.3.0"
|
||||
compose-constraint = "1.0.1"
|
||||
compose-navigation = "2.7.7"
|
||||
compose-accompanist = "0.30.1"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue