diff --git a/app/src/main/java/one/mixin/android/MixinApplication.kt b/app/src/main/java/one/mixin/android/MixinApplication.kt index c99a26be56..bd8ddb13b7 100644 --- a/app/src/main/java/one/mixin/android/MixinApplication.kt +++ b/app/src/main/java/one/mixin/android/MixinApplication.kt @@ -40,6 +40,7 @@ import dagger.hilt.components.SingletonComponent import io.reactivex.plugins.RxJavaPlugins import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -146,12 +147,26 @@ open class MixinApplication : private var activityReferences: Int = 0 private var isActivityChangingConfigurations = false - @Inject - @ApplicationScope + @EntryPoint + @InstallIn(SingletonComponent::class) + interface ApplicationScopeEntryPoint { + @one.mixin.android.di.ApplicationScope + fun getApplicationScope(): CoroutineScope + } + + private fun getAppScope(): CoroutineScope { + return try { + EntryPointAccessors.fromApplication(this, ApplicationScopeEntryPoint::class.java).getApplicationScope() + } catch (e: Exception) { + CoroutineScope(Dispatchers.Main + SupervisorJob()) + } + } + lateinit var applicationScope: CoroutineScope override fun onCreate() { super.onCreate() + applicationScope = getAppScope() init() registerActivityLifecycleCallbacks(this) SignalProtocolLoggerProvider.setProvider(MixinSignalProtocolLogger()) diff --git a/app/src/main/java/one/mixin/android/api/response/perps/PerpsOrderItem.kt b/app/src/main/java/one/mixin/android/api/response/perps/PerpsOrderItem.kt index 8800b4aaca..1a0ff3b534 100644 --- a/app/src/main/java/one/mixin/android/api/response/perps/PerpsOrderItem.kt +++ b/app/src/main/java/one/mixin/android/api/response/perps/PerpsOrderItem.kt @@ -1,10 +1,12 @@ package one.mixin.android.api.response.perps import android.os.Parcelable +import androidx.compose.runtime.Immutable import androidx.room.ColumnInfo import com.google.gson.annotations.SerializedName import kotlinx.parcelize.Parcelize +@Immutable @Parcelize data class PerpsOrderItem( @SerializedName("order_id") diff --git a/app/src/main/java/one/mixin/android/api/response/perps/PerpsPosition.kt b/app/src/main/java/one/mixin/android/api/response/perps/PerpsPosition.kt index d11b68667f..398f4a96df 100644 --- a/app/src/main/java/one/mixin/android/api/response/perps/PerpsPosition.kt +++ b/app/src/main/java/one/mixin/android/api/response/perps/PerpsPosition.kt @@ -74,4 +74,10 @@ data class PerpsPosition( @SerializedName("updated_at") @ColumnInfo(name = "updated_at") val updatedAt: String, -) : Parcelable +) : Parcelable { + companion object { + const val STATE_OPEN = "open" + const val STATE_OPENING = "opening" + const val STATE_ADDING = "adding" + } +} diff --git a/app/src/main/java/one/mixin/android/api/response/perps/PerpsPositionItem.kt b/app/src/main/java/one/mixin/android/api/response/perps/PerpsPositionItem.kt index 321301ea17..a70da95e91 100644 --- a/app/src/main/java/one/mixin/android/api/response/perps/PerpsPositionItem.kt +++ b/app/src/main/java/one/mixin/android/api/response/perps/PerpsPositionItem.kt @@ -1,10 +1,12 @@ package one.mixin.android.api.response.perps import android.os.Parcelable +import androidx.compose.runtime.Immutable import androidx.room.ColumnInfo import com.google.gson.annotations.SerializedName import kotlinx.parcelize.Parcelize +@Immutable @Parcelize data class PerpsPositionItem( @SerializedName("position_id") diff --git a/app/src/main/java/one/mixin/android/db/perps/PerpsPositionDao.kt b/app/src/main/java/one/mixin/android/db/perps/PerpsPositionDao.kt index 0d2f20d5ea..0ef4439edf 100644 --- a/app/src/main/java/one/mixin/android/db/perps/PerpsPositionDao.kt +++ b/app/src/main/java/one/mixin/android/db/perps/PerpsPositionDao.kt @@ -22,7 +22,7 @@ interface PerpsPositionDao : BaseDao { SELECT p.*, m.display_symbol, m.icon_url, m.token_symbol, m.price_scale FROM positions p LEFT JOIN markets m ON m.market_id = p.market_id - WHERE p.wallet_id = :walletId AND (p.state = 'open' or p.state = 'processing' or p.state = 'adding') + WHERE p.wallet_id = :walletId AND (p.state = 'open' or p.state = 'opening' or p.state = 'adding') ORDER BY p.created_at DESC """) suspend fun getOpenPositions(walletId: String): List @@ -32,7 +32,7 @@ interface PerpsPositionDao : BaseDao { SELECT p.*, m.display_symbol, m.icon_url, m.token_symbol, m.price_scale FROM positions p LEFT JOIN markets m ON m.market_id = p.market_id - WHERE p.wallet_id = :walletId AND (p.state = 'open' or p.state = 'processing' or p.state = 'adding') + WHERE p.wallet_id = :walletId AND (p.state = 'open' or p.state = 'opening' or p.state = 'adding') ORDER BY p.created_at DESC """ ) @@ -42,7 +42,7 @@ interface PerpsPositionDao : BaseDao { SELECT p.*, m.display_symbol, m.icon_url, m.token_symbol, m.price_scale FROM positions p LEFT JOIN markets m ON m.market_id = p.market_id - WHERE p.wallet_id = :walletId AND (p.state = 'open' or p.state = 'processing' or p.state = 'adding') + WHERE p.wallet_id = :walletId AND (p.state = 'open' or p.state = 'opening' or p.state = 'adding') ORDER BY p.created_at DESC """) fun getOpenPositionsPaged(walletId: String): PagingSource @@ -69,22 +69,22 @@ interface PerpsPositionDao : BaseDao { @Query("DELETE FROM positions WHERE wallet_id = :walletId") suspend fun deleteByWallet(walletId: String) - @Query("DELETE FROM positions WHERE wallet_id = :walletId AND (state = 'open' or state = 'processing' or state = 'adding')") + @Query("DELETE FROM positions WHERE wallet_id = :walletId AND (state = 'open' or state = 'opening' or state = 'adding')") suspend fun deleteOpenByWallet(walletId: String) - @Query("DELETE FROM positions WHERE wallet_id = :walletId AND (state = 'open' or state = 'processing' or state = 'adding') AND position_id NOT IN (:positionIds)") + @Query("DELETE FROM positions WHERE wallet_id = :walletId AND (state = 'open' or state = 'opening' or state = 'adding') AND position_id NOT IN (:positionIds)") suspend fun deleteOpenByWalletAndNotIn(walletId: String, positionIds: List) - @Query("SELECT COALESCE(SUM(CAST(unrealized_pnl AS REAL)), 0) FROM positions WHERE wallet_id = :walletId AND (state = 'open' OR state = 'processing' OR state = 'adding')") + @Query("SELECT COALESCE(SUM(CAST(unrealized_pnl AS REAL)), 0) FROM positions WHERE wallet_id = :walletId AND (state = 'open' OR state = 'opening' OR state = 'adding')") suspend fun getTotalUnrealizedPnl(walletId: String): Double? - @Query("SELECT COALESCE(SUM(CAST(unrealized_pnl AS REAL)), 0) FROM positions WHERE wallet_id = :walletId AND (state = 'open' or state = 'processing' or state = 'adding')") + @Query("SELECT COALESCE(SUM(CAST(unrealized_pnl AS REAL)), 0) FROM positions WHERE wallet_id = :walletId AND (state = 'open' or state = 'opening' or state = 'adding')") fun observeTotalUnrealizedPnl(walletId: String): Flow - @Query("SELECT SUM(CAST(entry_price AS REAL) * ABS(CAST(quantity AS REAL))) FROM positions WHERE wallet_id = :walletId AND (state = 'open' or state = 'processing' or state = 'adding')") + @Query("SELECT SUM(CAST(entry_price AS REAL) * ABS(CAST(quantity AS REAL))) FROM positions WHERE wallet_id = :walletId AND (state = 'open' or state = 'opening' or state = 'adding')") suspend fun getTotalOpenPositionValue(walletId: String): Double? - @Query("SELECT COALESCE(SUM(CAST(entry_price AS REAL) * ABS(CAST(quantity AS REAL))), 0) FROM positions WHERE wallet_id = :walletId AND (state = 'open' or state = 'processing' or state = 'adding')") + @Query("SELECT COALESCE(SUM(CAST(entry_price AS REAL) * ABS(CAST(quantity AS REAL))), 0) FROM positions WHERE wallet_id = :walletId AND (state = 'open' or state = 'opening' or state = 'adding')") fun observeTotalOpenPositionValue(walletId: String): Flow @Query("DELETE FROM positions WHERE position_id = :positionId") diff --git a/app/src/main/java/one/mixin/android/ui/conversation/link/LinkBottomSheetDialogFragment.kt b/app/src/main/java/one/mixin/android/ui/conversation/link/LinkBottomSheetDialogFragment.kt index 3becd50957..20dbcb2beb 100644 --- a/app/src/main/java/one/mixin/android/ui/conversation/link/LinkBottomSheetDialogFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/conversation/link/LinkBottomSheetDialogFragment.kt @@ -1107,7 +1107,7 @@ class LinkBottomSheetDialogFragment : SchemeBottomSheet() { private suspend fun handleTradeScheme(uri: Uri) { val type = uri.getQueryParameter("type") - if (type.equals("perps", true)) { + if (type.equals("perps", true) || type.equals("perpetual", true)) { val marketId = uri.getQueryParameter("market") if (marketId.isNullOrBlank() || !marketId.isUUID()) { showError(R.string.Invalid_payment_link) diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/TradeFragment.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/TradeFragment.kt index b1af27f75d..578280f11d 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/TradeFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/TradeFragment.kt @@ -519,8 +519,8 @@ class TradeFragment : BaseFragment() { onShowMarketList = { isLong -> PerpsMarketListBottomSheetDialogFragment.newInstance(isLong).show(parentFragmentManager, PerpsMarketListBottomSheetDialogFragment.TAG) }, - onShowAllMarkets = { initialCategory -> - PerpsMarketListBottomSheetDialogFragment.newInstance(initialCategory).show(parentFragmentManager, PerpsMarketListBottomSheetDialogFragment.TAG) + onShowAllMarkets = { initialCategory, initialSort -> + PerpsMarketListBottomSheetDialogFragment.newInstance(initialCategory, initialSort).show(parentFragmentManager, PerpsMarketListBottomSheetDialogFragment.TAG) }, onShowAllOpenPositions = { navTo(AllPositionsFragment.newOpenInstance(), AllPositionsFragment.TAG) diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/TradePage.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/TradePage.kt index cf3e7f654c..5b28c70c02 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/TradePage.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/TradePage.kt @@ -63,6 +63,7 @@ import one.mixin.android.ui.home.web3.components.OutlinedTab import one.mixin.android.ui.home.web3.components.PageScaffold import one.mixin.android.ui.home.web3.trade.perps.PerpetualContent import one.mixin.android.ui.home.web3.trade.perps.PerpetualViewModel +import one.mixin.android.ui.home.web3.widget.MarketSort import one.mixin.android.util.analytics.AnalyticsTracker import one.mixin.android.vo.WalletCategory import java.math.BigDecimal @@ -103,7 +104,7 @@ fun TradePage( onShowTradingGuide: (Int) -> Unit, onShowHelpBottomSheet: (() -> Unit, () -> Unit) -> Unit, onShowMarketList: (Boolean) -> Unit, - onShowAllMarkets: (String?) -> Unit, + onShowAllMarkets: (String?, MarketSort?) -> Unit, onShowAllOpenPositions: () -> Unit, onShowAllClosedPositions: () -> Unit, onOpenPositionClick: (PerpsPositionItem) -> Unit, diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/AllPositionsPage.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/AllPositionsPage.kt index e5aafdfca9..ec6d160048 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/AllPositionsPage.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/AllPositionsPage.kt @@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.Icon @@ -45,6 +46,8 @@ import androidx.paging.LoadState import androidx.paging.PagingData import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems +import androidx.paging.compose.itemContentType +import androidx.paging.compose.itemKey import kotlinx.coroutines.delay import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.isActive @@ -306,24 +309,32 @@ private fun LazyListScope.openPositionItems( onPositionClick: (PerpsPositionItem) -> Unit, ) { if (positions.itemCount == 0) return - item { - Column( + items( + count = positions.itemCount, + key = positions.itemKey { it.positionId }, + contentType = positions.itemContentType { "open_position" }, + ) { index -> + val position = positions[index] ?: return@items + val shape = when { + positions.itemCount == 1 -> RoundedCornerShape(8.dp) + index == 0 -> RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp) + index == positions.itemCount - 1 -> RoundedCornerShape(bottomStart = 8.dp, bottomEnd = 8.dp) + else -> RoundedCornerShape(0.dp) + } + Box( modifier = Modifier .fillMaxWidth() - .clip(RoundedCornerShape(8.dp)) + .clip(shape) .cardBackground( backgroundColor = MixinAppTheme.colors.background, borderColor = MixinAppTheme.colors.borderColor, + cornerRadius = if (index == 0 || index == positions.itemCount - 1) 8.dp else 0.dp, ) - .padding(vertical = 8.dp) ) { - for (index in 0 until positions.itemCount) { - val position = positions[index] ?: continue - OpenPositionItem( - position = position, - onClick = { onPositionClick(position) }, - ) - } + OpenPositionItem( + position = position, + onClick = { onPositionClick(position) }, + ) } } } @@ -332,7 +343,13 @@ private fun LazyListScope.closedPositionItems( positions: LazyPagingItems, onPositionClick: (PerpsOrderItem) -> Unit, ) { - items(count = positions.itemCount) { index -> + items( + count = positions.itemCount, + key = positions.itemKey { it.orderId }, + contentType = positions.itemContentType { order -> + if (order.orderType == PerpsOrder.TYPE_CLOSE) "close" else "open" + }, + ) { index -> val order = positions[index] ?: return@items if (order.orderType == PerpsOrder.TYPE_CLOSE) { ClosedActivityItem( diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/ClosedActivityItem.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/ClosedActivityItem.kt index b00244e567..8474737cd9 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/ClosedActivityItem.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/ClosedActivityItem.kt @@ -116,7 +116,7 @@ fun ClosedActivityItem( modifier = Modifier .clip(RoundedCornerShape(4.dp)) .background(leverageBackgroundColor) - .padding(horizontal = 3.dp, vertical = 2.dp), + .padding(horizontal = 3.dp, vertical = 1.dp), ) } Spacer(modifier = Modifier.height(4.dp)) diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/OpenPositionItem.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/OpenPositionItem.kt index db761d6047..b5dd8782d4 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/OpenPositionItem.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/OpenPositionItem.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import one.mixin.android.Constants import one.mixin.android.R +import one.mixin.android.api.response.perps.PerpsPosition import one.mixin.android.api.response.perps.PerpsPositionItem import one.mixin.android.compose.CoilImage import one.mixin.android.compose.theme.MixinAppTheme @@ -56,8 +57,8 @@ fun OpenPositionItem( ?.toPlainString() ?: position.quantity.removePrefix("-") val isLong = position.side.equals("long", true) - val isOpening = position.state.equals("processing", true) - val isAdding = position.state.equals("adding", true) + val isOpening = position.state.equals(PerpsPosition.STATE_OPENING, true) + val isAdding = position.state.equals(PerpsPosition.STATE_ADDING, true) val isPending = isOpening || isAdding val sideColor = if (isLong) { if (quoteColorPref) MixinAppTheme.colors.walletRed else MixinAppTheme.colors.walletGreen @@ -133,7 +134,7 @@ fun OpenPositionItem( modifier = Modifier .clip(RoundedCornerShape(4.dp)) .background(leverageBackgroundColor) - .padding(horizontal = 3.dp, vertical = 2.dp) + .padding(horizontal = 3.dp, vertical = 1.dp) ) if (tpSlTagText != null) { Spacer(modifier = Modifier.width(4.dp)) @@ -227,13 +228,13 @@ private fun TpSlStatusTag( val backgroundColor = Color(LocalContext.current.colorAttr(R.attr.bg_market_gradient_start)) Text( text = text, - fontSize = 11.sp, + fontSize = 12.sp, fontWeight = FontWeight.W500, color = Color.White, lineHeight = 14.sp, modifier = Modifier .clip(RoundedCornerShape(4.dp)) .background(backgroundColor) - .padding(horizontal = 4.dp, vertical = 2.dp), + .padding(horizontal = 3.dp, vertical = 1.dp), ) } diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/OpenPositionPage.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/OpenPositionPage.kt index 31c2acc53b..df3c5111ac 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/OpenPositionPage.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/OpenPositionPage.kt @@ -145,6 +145,7 @@ fun OpenPositionPage( var takeProfitPrice by remember { mutableStateOf("") } var stopLossPrice by remember { mutableStateOf("") } var errorInfo by remember { mutableStateOf(null) } + var isProcessing by remember { mutableStateOf(false) } val scope = rememberCoroutineScope() val savedLeverage = remember(marketId) { @@ -672,35 +673,40 @@ fun OpenPositionPage( .fillMaxWidth() .height(48.dp), onClick = { + if (isProcessing) return@MixinButton + isProcessing = true AnalyticsTracker.trackPerpsPreview(leverage.toInt().toPerpsLeverageValue()) errorInfo = null - val token = currentToken ?: return@MixinButton - val amount = usdtAmount.toBigDecimalOrNull() ?: return@MixinButton + val token = currentToken ?: run { isProcessing = false; return@MixinButton } + val amount = usdtAmount.toBigDecimalOrNull() ?: run { isProcessing = false; return@MixinButton } - if (amount <= BigDecimal.ZERO) return@MixinButton + if (amount <= BigDecimal.ZERO) { isProcessing = false; return@MixinButton } if (minimumMargin > BigDecimal.ZERO && amount < minimumMargin) { errorInfo = minimumMarginError + isProcessing = false return@MixinButton } if (maximumMargin > BigDecimal.ZERO && amount > maximumMargin) { errorInfo = maximumMarginError + isProcessing = false return@MixinButton } - if (amount > (token.balance.toBigDecimalOrNull() ?: BigDecimal.ZERO)) return@MixinButton + if (amount > (token.balance.toBigDecimalOrNull() ?: BigDecimal.ZERO)) { isProcessing = false; return@MixinButton } val m = currentMarket val walletId = Session.getAccountId() ?: "" // Privacy Wallet - if (walletId.isEmpty()) return@MixinButton + if (walletId.isEmpty()) { isProcessing = false; return@MixinButton } - val activity = context as? FragmentActivity ?: return@MixinButton + val activity = context as? FragmentActivity ?: run { isProcessing = false; return@MixinButton } val price = m.last.toBigDecimalOrNull() ?: BigDecimal.ZERO - if (price == BigDecimal.ZERO) return@MixinButton + if (price == BigDecimal.ZERO) { isProcessing = false; return@MixinButton } scope.launch { val hasOpeningPosition = viewModel.getOpenPositionsFromDb(walletId) .any { it.marketId == m.marketId } if (hasOpeningPosition) { errorInfo = waitingOtherOrdersError + isProcessing = false return@launch } @@ -732,6 +738,7 @@ fun OpenPositionPage( }.show(activity.supportFragmentManager, PerpsConfirmBottomSheetDialogFragment.TAG) }, onError = { errorCode, errorMessage -> + isProcessing = false errorInfo = if (errorCode > 0) { context.getMixinErrorStringByCode(errorCode, errorMessage) } else { diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/OpenedOrderItem.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/OpenedOrderItem.kt index d7cdbfc45a..2073aa471d 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/OpenedOrderItem.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/OpenedOrderItem.kt @@ -124,7 +124,7 @@ fun OpenedOrderItem( modifier = Modifier .clip(RoundedCornerShape(4.dp)) .background(leverageBackgroundColor) - .padding(horizontal = 3.dp, vertical = 2.dp), + .padding(horizontal = 3.dp, vertical = 1.dp), ) } diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpetualContent.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpetualContent.kt index 3dd214ae40..80893547b8 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpetualContent.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpetualContent.kt @@ -55,6 +55,7 @@ import one.mixin.android.compose.theme.MixinAppTheme import one.mixin.android.extension.defaultSharedPreferences import one.mixin.android.extension.putString import one.mixin.android.session.Session +import one.mixin.android.ui.home.web3.widget.MarketSort import one.mixin.android.ui.wallet.alert.components.cardBackground import one.mixin.android.util.analytics.AnalyticsTracker import one.mixin.android.widget.components.MixinButton @@ -68,7 +69,7 @@ private const val CLOSED_POSITION_PREVIEW_LIMIT = 10 fun PerpetualContent( onShowTradingGuide: () -> Unit, onShowMarketList: (isLong: Boolean) -> Unit, - onShowAllMarkets: (String?) -> Unit, + onShowAllMarkets: (String?, MarketSort?) -> Unit, onShowAllOpenPositions: () -> Unit, onShowAllClosedPositions: () -> Unit, onOpenPositionClick: (PerpsPositionItem) -> Unit, @@ -102,6 +103,9 @@ fun PerpetualContent( val openPositionsCount = openPositions.size val openPositionsPreview = openPositions.take(3) val marketsPreview = markets.take(3) + val topMoversPreview = remember(markets) { + markets.topMoversPreview() + } val sourceOrder = remember(markets) { markets.withIndex().associate { it.value.marketId to it.index } } @@ -308,6 +312,25 @@ fun PerpetualContent( } } + if (topMoversPreview.isNotEmpty()) { + Spacer(modifier = Modifier.height(16.dp)) + Column( + Modifier + .fillMaxWidth() + .wrapContentHeight() + .clip(RoundedCornerShape(8.dp)) + .cardBackground(Color.Transparent, MixinAppTheme.colors.borderColor) + .padding(vertical = 16.dp) + ) { + TopMoversCard( + markets = topMoversPreview, + quoteColorReversed = quoteColorReversed, + onViewAllClick = { onShowAllMarkets(null, MarketSort.TWENTY_FOUR_HOURS_PERCENTAGE_DESCENDING) }, + onMarketItemClick = onMarketItemClick, + ) + } + } + Spacer(modifier = Modifier.height(16.dp)) Column( @@ -324,8 +347,8 @@ fun PerpetualContent( markets = marketsPreview, totalCount = markets.size, quoteColorReversed = quoteColorReversed, - onTitleClick = { onShowAllMarkets(null) }, - onViewAllClick = { onShowAllMarkets(null) }, + onTitleClick = { onShowAllMarkets(null, null) }, + onViewAllClick = { onShowAllMarkets(null, null) }, onMarketItemClick = onMarketItemClick, ) } else if (isLoading) { @@ -373,10 +396,10 @@ fun PerpetualContent( totalCount = stocksMarkets.size, quoteColorReversed = quoteColorReversed, onTitleClick = { - onShowAllMarkets(PerpsMarketListBottomSheetDialogFragment.CATEGORY_STOCKS) + onShowAllMarkets(PerpsMarketListBottomSheetDialogFragment.CATEGORY_STOCKS, null) }, onViewAllClick = { - onShowAllMarkets(PerpsMarketListBottomSheetDialogFragment.CATEGORY_STOCKS) + onShowAllMarkets(PerpsMarketListBottomSheetDialogFragment.CATEGORY_STOCKS, null) }, onMarketItemClick = onMarketItemClick, ) @@ -399,10 +422,10 @@ fun PerpetualContent( totalCount = commoditiesMarkets.size, quoteColorReversed = quoteColorReversed, onTitleClick = { - onShowAllMarkets(PerpsMarketListBottomSheetDialogFragment.CATEGORY_COMMODITIES) + onShowAllMarkets(PerpsMarketListBottomSheetDialogFragment.CATEGORY_COMMODITIES, null) }, onViewAllClick = { - onShowAllMarkets(PerpsMarketListBottomSheetDialogFragment.CATEGORY_COMMODITIES) + onShowAllMarkets(PerpsMarketListBottomSheetDialogFragment.CATEGORY_COMMODITIES, null) }, onMarketItemClick = onMarketItemClick, ) @@ -638,7 +661,6 @@ private fun calculatePnlPercent( .toDouble() } - @Composable private fun ViewAllAction(onClick: () -> Unit) { Row( diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpetualViewModel.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpetualViewModel.kt index 48ee455ba2..3e554d085a 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpetualViewModel.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpetualViewModel.kt @@ -343,11 +343,23 @@ class PerpetualViewModel @Inject constructor( if (response.isSuccess && data != null) { Timber.d("Perps order opened: ${data.orderId}, payUrl: ${data.paymentUrl}") + val entryPriceDecimal = entryPrice.toBigDecimalOrNull() ?: BigDecimal.ZERO + val amountDecimal = amount.toBigDecimalOrNull() ?: BigDecimal.ZERO + val quantityValue = if (entryPriceDecimal > BigDecimal.ZERO) { + amountDecimal + .multiply(BigDecimal(leverage)) + .divide(entryPriceDecimal, 8, java.math.RoundingMode.HALF_UP) + .stripTrailingZeros() + .toPlainString() + } else { + "0" + } + val position = PerpsPosition( positionId = data.orderId, marketId = marketId, side = side, - quantity = amount, + quantity = quantityValue, settleAssetId = assetId, botId = "", entryPrice = entryPrice, @@ -358,7 +370,7 @@ class PerpetualViewModel @Inject constructor( stopLossPrice = stopLossPrice, liquidationPrice = null, leverage = leverage, - state = "processing", + state = PerpsPosition.STATE_OPENING, markPrice = entryPrice, unrealizedPnl = "0", roe = "0", @@ -425,12 +437,12 @@ class PerpetualViewModel @Inject constructor( if (localPosition != null) { perpsPositionDao.upsertSuspend( localPosition.copy( - state = "adding", + state = PerpsPosition.STATE_ADDING, updatedAt = now, ) ) } else { - perpsPositionDao.updateStatus(positionId, "adding", now) + perpsPositionDao.updateStatus(positionId, PerpsPosition.STATE_ADDING, now) } } onSuccess(data) diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsFormat.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsFormat.kt index 75575cda7c..68ecfb42b6 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsFormat.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsFormat.kt @@ -143,6 +143,14 @@ fun formatPerpsSignedPercent(value: Double, withSign: Boolean = true): String { private fun formatPerpsPercentDecimal(value: BigDecimal): String { val safeValue = value.abs() + if (safeValue >= BigDecimal(1000)) { + val thousands = safeValue.divide(BigDecimal(1000), 1, RoundingMode.FLOOR) + return if (thousands.stripTrailingZeros().scale() <= 0) { + "${thousands.toBigInteger()}K" + } else { + "${thousands.stripTrailingZeros().toPlainString()}K" + } + } val scaled = safeValue.setScale(2, RoundingMode.FLOOR) if (scaled.compareTo(BigDecimal.ZERO) == 0) return "0.0" return scaled.stripTrailingZeros().toPlainString() diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketDetailPage.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketDetailPage.kt index 3a4eb0ddac..4a6861d0cf 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketDetailPage.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketDetailPage.kt @@ -61,6 +61,7 @@ import one.mixin.android.R import one.mixin.android.api.response.perps.PerpsMarket import one.mixin.android.api.response.perps.PerpsOrder import one.mixin.android.api.response.perps.PerpsOrderItem +import one.mixin.android.api.response.perps.PerpsPosition import one.mixin.android.api.response.perps.PerpsPositionItem import one.mixin.android.api.response.perps.toPosition import one.mixin.android.compose.CoilImage @@ -116,6 +117,7 @@ fun PerpsMarketDetailPage( } }.collectAsStateWithLifecycle(initialValue = emptyList()) var previousOpenPositionsCount by remember(walletId) { mutableStateOf(null) } + var isAddingProcessing by remember { mutableStateOf(false) } val currentPosition = openPositions?.firstOrNull { it.marketId == marketId } val hasLoadedOpenPositions = openPositions != null val closedPositions = allClosedPositions.filter { it.marketId == marketId } @@ -436,9 +438,9 @@ fun PerpsMarketDetailPage( .padding(bottom = 20.dp, top = 20.dp) ) { if (currentPosition != null) { - val isOpen = currentPosition.state == "open" - val isAdding = currentPosition.state == "adding" - val isPending = currentPosition.state == "processing" || isAdding + val isOpen = currentPosition.state == PerpsPosition.STATE_OPEN + val isAdding = currentPosition.state == PerpsPosition.STATE_ADDING + val isPending = currentPosition.state == PerpsPosition.STATE_OPENING || isAdding if (isPending) { MixinButton( modifier = Modifier @@ -465,12 +467,15 @@ fun PerpsMarketDetailPage( modifier = Modifier .weight(1f) .height(48.dp), - enabled = true, + enabled = !isAddingProcessing, onClick = { - val activity = context as? FragmentActivity ?: return@MixinButton + if (isAddingProcessing) return@MixinButton + isAddingProcessing = true + val activity = context as? FragmentActivity ?: run { isAddingProcessing = false; return@MixinButton } val positionForAdd = currentPosition PerpsAddBottomSheetDialogFragment.newInstance(positionForAdd) .setOnAdd { token, amount -> + isAddingProcessing = false val referencePrice = market?.last ?: positionForAdd.markPrice ?: positionForAdd.entryPrice @@ -503,6 +508,7 @@ fun PerpsMarketDetailPage( ).show(activity.supportFragmentManager, PerpsConfirmBottomSheetDialogFragment.TAG) }, onError = { errorCode, errorMessage -> + isAddingProcessing = false val message = if (errorCode > 0) { context.getMixinErrorStringByCode(errorCode, errorMessage) } else { diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketItem.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketItem.kt index ae55dc5b02..bd16887eb3 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketItem.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketItem.kt @@ -97,7 +97,7 @@ fun PerpsMarketItem( modifier = Modifier .clip(RoundedCornerShape(4.dp)) .background(MixinAppTheme.colors.backgroundGrayLight) - .padding(horizontal = 3.dp, vertical = 2.dp) + .padding(horizontal = 3.dp, vertical = 1.dp) ) } Text( diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListBottomSheetDialogFragment.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListBottomSheetDialogFragment.kt index fc7d45849c..c3f6ad2b8d 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListBottomSheetDialogFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListBottomSheetDialogFragment.kt @@ -57,6 +57,7 @@ class PerpsMarketListBottomSheetDialogFragment : MixinBottomSheetDialogFragment( const val TAG = "PerpsMarketListBottomSheetDialogFragment" private const val ARGS_IS_LONG = "args_is_long" private const val ARGS_INITIAL_CATEGORY = "args_initial_category" + private const val ARGS_INITIAL_SORT = "args_initial_sort" const val CATEGORY_STOCKS = "stocks" const val CATEGORY_COMMODITIES = "commodities" @@ -64,8 +65,12 @@ class PerpsMarketListBottomSheetDialogFragment : MixinBottomSheetDialogFragment( putBoolean(ARGS_IS_LONG, isLong) } - fun newInstance(initialCategory: String? = null) = PerpsMarketListBottomSheetDialogFragment().withArgs { + fun newInstance( + initialCategory: String? = null, + initialSort: MarketSort? = null, + ) = PerpsMarketListBottomSheetDialogFragment().withArgs { initialCategory?.let { putString(ARGS_INITIAL_CATEGORY, it) } + initialSort?.let { putInt(ARGS_INITIAL_SORT, it.value) } } } @@ -87,6 +92,12 @@ class PerpsMarketListBottomSheetDialogFragment : MixinBottomSheetDialogFragment( private val initialCategory by lazy { arguments?.getString(ARGS_INITIAL_CATEGORY) } + private val initialSort by lazy { + arguments + ?.takeIf { it.containsKey(ARGS_INITIAL_SORT) } + ?.getInt(ARGS_INITIAL_SORT) + ?.let { MarketSort.fromValueOrNull(it) } + } private var allMarkets = listOf() private var currentQuery = "" private var currentCategory = MarketCategory.ALL @@ -117,6 +128,7 @@ class PerpsMarketListBottomSheetDialogFragment : MixinBottomSheetDialogFragment( marketRv.adapter = adapter (marketRv.itemAnimator as? SimpleItemAnimator)?.supportsChangeAnimations = false applyInitialCategory() + currentSort = initialSort categoryScroll.scrollToCenterCheckedRadio(categoryGroup) searchEt.listener = object : SearchView.OnSearchViewListener { diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsPositionShareBottomFragment.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsPositionShareBottomFragment.kt index 8a4d7bd2a1..eb4a6e1e70 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsPositionShareBottomFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsPositionShareBottomFragment.kt @@ -424,7 +424,7 @@ class PerpsPositionShareBottomFragment : MixinBottomSheetDialogFragment() { } private fun buildPerpsAppCardMessage(): ForwardMessage { - val action = "${Constants.Scheme.HTTPS_TRADE}?type=perpetual&market=${shareData.marketId}" + val action = "${Constants.Scheme.HTTPS_TRADE}?type=perps&market=${shareData.marketId}" val side = if (shareData.side.equals("long", ignoreCase = true)) getString(R.string.Long) else getString(R.string.Short) val market = shareData.displaySymbol.ifBlank { shareData.tokenSymbol } val title = getString(R.string.perps_share_card_title, shareData.tokenSymbol) diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PositionDetailPage.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PositionDetailPage.kt index f573c827df..c7971e5187 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PositionDetailPage.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PositionDetailPage.kt @@ -44,6 +44,7 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import one.mixin.android.R import one.mixin.android.api.response.perps.PerpsOrder import one.mixin.android.api.response.perps.PerpsOrderItem +import one.mixin.android.api.response.perps.PerpsPosition import one.mixin.android.api.response.perps.PerpsPositionItem import one.mixin.android.compose.CoilImage import one.mixin.android.compose.theme.MixinAppTheme @@ -137,7 +138,8 @@ fun PositionDetailPage( } } - val isPending = position.state == "processing" || position.state == "adding" + val isAdding = position.state == PerpsPosition.STATE_ADDING + val isPending = position.state == PerpsPosition.STATE_OPENING || isAdding val leverageTextColor = if (isPending) MixinAppTheme.colors.textAssist else sideColor val leverageBackgroundColor = if (isPending) { MixinAppTheme.colors.backgroundGrayLight @@ -296,7 +298,7 @@ fun PositionDetailPage( ) { if (isPending) { Text( - text = stringResource(if (position.state == "adding") R.string.adding_position else R.string.Pending), + text = stringResource(if (isAdding) R.string.adding_position else R.string.Pending), color = MixinAppTheme.colors.textAssist, fontWeight = FontWeight.W500, modifier = Modifier diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversCard.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversCard.kt new file mode 100644 index 0000000000..43e18fcfb9 --- /dev/null +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversCard.kt @@ -0,0 +1,214 @@ +package one.mixin.android.ui.home.web3.trade.perps + +import android.graphics.BlurMaskFilter +import android.graphics.Paint +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicText +import androidx.compose.foundation.text.TextAutoSize +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.nativeCanvas +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import one.mixin.android.R +import one.mixin.android.api.response.perps.PerpsMarket +import one.mixin.android.compose.CoilImage +import one.mixin.android.compose.theme.MixinAppTheme +import java.math.BigDecimal + +private const val TOP_MOVERS_COLUMNS = 4 + +@Composable +fun TopMoversCard( + markets: List, + quoteColorReversed: Boolean, + onViewAllClick: () -> Unit, + onMarketItemClick: (PerpsMarket) -> Unit, +) { + if (markets.isEmpty()) return + + Row( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onViewAllClick) + .padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = stringResource(R.string.perps_top_movers), + fontSize = 14.sp, + color = MixinAppTheme.colors.textPrimary, + ) + Icon( + painter = painterResource(R.drawable.ic_arrow_right), + contentDescription = null, + tint = MixinAppTheme.colors.textAssist, + modifier = Modifier.size(16.dp), + ) + } + Spacer(modifier = Modifier.height(12.dp)) + + val rows = markets.chunked(TOP_MOVERS_COLUMNS) + rows.forEachIndexed { index, rowMarkets -> + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), + verticalAlignment = Alignment.Top, + ) { + rowMarkets.forEach { market -> + Box( + modifier = Modifier.weight(1f), + contentAlignment = Alignment.TopCenter, + ) { + TopMoverGridItem( + market = market, + quoteColorReversed = quoteColorReversed, + onClick = { onMarketItemClick(market) }, + ) + } + } + repeat(TOP_MOVERS_COLUMNS - rowMarkets.size) { + Spacer(modifier = Modifier.weight(1f)) + } + } + if (index != rows.lastIndex) { + Spacer(modifier = Modifier.height(12.dp)) + } + } +} + +@Composable +private fun TopMoverGridItem( + market: PerpsMarket, + quoteColorReversed: Boolean, + onClick: () -> Unit, +) { + val changePercent = market.changePercent() + val isPositive = changePercent >= BigDecimal.ZERO + val changeColor = if (isPositive) { + if (quoteColorReversed) MixinAppTheme.colors.walletRed else MixinAppTheme.colors.walletGreen + } else { + if (quoteColorReversed) MixinAppTheme.colors.walletGreen else MixinAppTheme.colors.walletRed + } + val changeText = formatPerpsSignedPercent(changePercent) + + Column( + modifier = Modifier + .offset(y = 6.dp) + .clip(RoundedCornerShape(8.dp)) + .clickable(onClick = onClick) + .padding(horizontal = 4.dp, vertical = 8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Box( + modifier = Modifier.size(width = 42.dp, height = 46.dp), + contentAlignment = Alignment.TopCenter, + ) { + CoilImage( + model = market.iconUrl, + placeholder = R.drawable.ic_avatar_place_holder, + modifier = Modifier + .size(42.dp) + .clip(CircleShape), + ) + BasicText( + text = "${market.leverage}x", + style = TextStyle( + fontSize = 12.sp, + lineHeight = 14.sp, + color = MixinAppTheme.colors.textAssist, + fontWeight = FontWeight.W500, + ), + maxLines = 1, + autoSize = TextAutoSize.StepBased( + minFontSize = 8.sp, + maxFontSize = 12.sp, + stepSize = 0.5.sp + ), + modifier = Modifier + .offset(y = 32.dp) + .topMoverLeverageShadow() + .clip(RoundedCornerShape(4.dp)) + .background(MixinAppTheme.colors.background) + .padding(horizontal = 3.dp, vertical = 1.dp), + ) + } + Spacer(modifier = Modifier.height(4.dp)) + BasicText( + text = market.tokenSymbol, + style = TextStyle( + fontSize = 14.sp, + lineHeight = 18.sp, + color = MixinAppTheme.colors.textPrimary, + textAlign = TextAlign.Center, + ), + maxLines = 1, + autoSize = TextAutoSize.StepBased( + minFontSize = 8.sp, + maxFontSize = 14.sp, + stepSize = 0.5.sp + ), + ) + Spacer(modifier = Modifier.height(2.dp)) + BasicText( + text = changeText, + style = TextStyle( + fontSize = 13.sp, + color = changeColor, + textAlign = TextAlign.Center, + ), + maxLines = 1, + autoSize = TextAutoSize.StepBased( + minFontSize = 8.sp, + maxFontSize = 13.sp, + stepSize = 0.5.sp + ), + ) + } +} + +private fun Modifier.topMoverLeverageShadow(): Modifier = drawBehind { + val blur = 2.dp.toPx() + val offsetY = (-1).dp.toPx() + val radius = 4.dp.toPx() + val paint = Paint().apply { + color = Color.Black.copy(alpha = 0.04f).toArgb() + maskFilter = BlurMaskFilter(blur, BlurMaskFilter.Blur.NORMAL) + } + drawContext.canvas.nativeCanvas.drawRoundRect( + 0f, + offsetY, + size.width, + size.height + offsetY, + radius, + radius, + paint, + ) +} diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversPreview.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversPreview.kt new file mode 100644 index 0000000000..d6f8c50d12 --- /dev/null +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversPreview.kt @@ -0,0 +1,16 @@ +package one.mixin.android.ui.home.web3.trade.perps + +import one.mixin.android.api.response.perps.PerpsMarket + +private const val TOP_MOVERS_GROUP_SIZE = 4 + +fun List.topMoversPreview(): List { + val topMarkets = sortedByDescending { it.changePercent() } + .take(TOP_MOVERS_GROUP_SIZE) + val topMarketIds = topMarkets.map { it.marketId }.toSet() + val bottomMarkets = sortedBy { it.changePercent() } + .filterNot { it.marketId in topMarketIds } + .take(TOP_MOVERS_GROUP_SIZE) + + return topMarkets + bottomMarkets +} diff --git a/app/src/main/res/drawable/market_logo_dark.xml b/app/src/main/res/drawable/market_logo_dark.xml index b506c2d967..e3834a3a11 100644 --- a/app/src/main/res/drawable/market_logo_dark.xml +++ b/app/src/main/res/drawable/market_logo_dark.xml @@ -1,35 +1,18 @@ - + android:width="86dp" + android:height="20dp" + android:viewportWidth="86" + android:viewportHeight="20"> + android:pathData="M22.671,1C22.85,1 22.994,1.145 23,1.325L23,1.335V18.665C23,18.715 22.989,18.764 22.968,18.809C22.892,18.973 22.702,19.045 22.54,18.972L22.53,18.968L18.318,16.925C18.121,16.83 17.995,16.63 17.99,16.409L17.989,16.395V3.31C17.989,3.076 18.126,2.865 18.336,2.772L18.348,2.766L22.548,1.025C22.587,1.008 22.629,1 22.671,1ZM0.329,1C0.371,1 0.413,1.008 0.452,1.025L4.652,2.766C4.869,2.856 5.011,3.071 5.011,3.31V16.395C5.011,16.621 4.883,16.828 4.682,16.925L0.47,18.968C0.306,19.047 0.11,18.976 0.032,18.809C0.011,18.764 0,18.715 0,18.665V1.335C0,1.15 0.147,1 0.329,1ZM11.87,4.785L15.754,7.074C15.983,7.209 16.124,7.458 16.124,7.728V12.305C16.124,12.575 15.983,12.824 15.754,12.959L11.87,15.248C11.641,15.382 11.359,15.382 11.131,15.248L7.246,12.959C7.017,12.824 6.876,12.575 6.876,12.305V7.728C6.876,7.458 7.017,7.209 7.246,7.074L11.131,4.785C11.359,4.651 11.641,4.651 11.87,4.785Z" + android:fillType="evenOdd"> @@ -37,18 +20,103 @@ + android:pathData="M58.451,5.807V9.999H56.781V2.845H58.377V4.061H58.46C58.623,3.66 58.883,3.342 59.24,3.106C59.6,2.87 60.044,2.752 60.573,2.752C61.062,2.752 61.488,2.858 61.851,3.069C62.217,3.28 62.5,3.586 62.7,3.986C62.903,4.387 63.003,4.873 63,5.444V9.999H61.33V5.705C61.33,5.227 61.207,4.853 60.961,4.582C60.718,4.312 60.381,4.177 59.95,4.177C59.658,4.177 59.398,4.242 59.171,4.373C58.946,4.5 58.77,4.685 58.64,4.927C58.514,5.169 58.451,5.463 58.451,5.807Z" + android:strokeAlpha="0.9" + android:fillColor="#D3D4D5" + android:fillAlpha="0.9"/> + + + + + + + + + + + + + + android:pathData="M50.847,18.583C50.447,18.583 50.103,18.494 49.814,18.317C49.525,18.14 49.303,17.896 49.147,17.585C48.992,17.274 48.914,16.919 48.914,16.52C48.914,16.114 48.994,15.755 49.153,15.445C49.314,15.132 49.538,14.888 49.825,14.713C50.114,14.536 50.451,14.447 50.836,14.447C51.136,14.447 51.406,14.499 51.646,14.604C51.887,14.708 52.084,14.854 52.238,15.041C52.392,15.229 52.487,15.447 52.524,15.697H51.869C51.819,15.515 51.708,15.354 51.535,15.213C51.365,15.071 51.136,14.999 50.847,14.999C50.591,14.999 50.367,15.062 50.175,15.187C49.984,15.31 49.835,15.485 49.728,15.71C49.622,15.934 49.569,16.197 49.569,16.499C49.569,16.808 49.621,17.077 49.725,17.307C49.83,17.536 49.979,17.714 50.169,17.84C50.362,17.967 50.588,18.031 50.847,18.031C51.017,18.031 51.172,18.003 51.311,17.947C51.449,17.892 51.567,17.812 51.663,17.708C51.759,17.604 51.828,17.479 51.869,17.333H52.524C52.487,17.569 52.395,17.781 52.249,17.971C52.105,18.158 51.913,18.307 51.674,18.419C51.437,18.528 51.161,18.583 50.847,18.583Z" + android:strokeAlpha="0.9" + android:fillColor="#D3D4D5" + android:fillAlpha="0.9"/> + android:pathData="M45.994,18.593C45.724,18.593 45.479,18.545 45.258,18.45C45.038,18.353 44.863,18.213 44.733,18.031C44.604,17.847 44.539,17.624 44.539,17.364C44.539,17.135 44.587,16.949 44.683,16.807C44.78,16.663 44.908,16.55 45.069,16.468C45.23,16.386 45.408,16.326 45.603,16.286C45.799,16.244 45.996,16.211 46.194,16.187C46.453,16.156 46.663,16.132 46.825,16.117C46.987,16.099 47.106,16.07 47.18,16.031C47.256,15.991 47.294,15.921 47.294,15.822V15.801C47.294,15.545 47.219,15.345 47.069,15.203C46.921,15.06 46.696,14.989 46.394,14.989C46.081,14.989 45.836,15.053 45.658,15.182C45.48,15.31 45.355,15.447 45.283,15.593L44.661,15.385C44.772,15.142 44.92,14.953 45.106,14.817C45.292,14.68 45.496,14.584 45.716,14.531C45.939,14.475 46.157,14.447 46.372,14.447C46.509,14.447 46.666,14.463 46.844,14.494C47.023,14.524 47.197,14.585 47.363,14.679C47.532,14.773 47.671,14.914 47.783,15.104C47.894,15.293 47.949,15.546 47.949,15.864V18.499H47.294V17.958H47.26C47.216,18.044 47.142,18.137 47.038,18.236C46.935,18.335 46.797,18.419 46.625,18.489C46.452,18.558 46.242,18.593 45.994,18.593ZM46.094,18.041C46.353,18.041 46.572,17.993 46.75,17.898C46.929,17.802 47.064,17.679 47.155,17.528C47.248,17.377 47.294,17.218 47.294,17.051V16.489C47.266,16.52 47.205,16.549 47.111,16.575C47.018,16.599 46.911,16.621 46.788,16.64C46.668,16.657 46.55,16.673 46.436,16.687C46.323,16.699 46.231,16.709 46.161,16.718C45.991,16.739 45.831,16.773 45.683,16.82C45.537,16.865 45.418,16.933 45.328,17.025C45.239,17.116 45.194,17.239 45.194,17.395C45.194,17.609 45.279,17.77 45.447,17.879C45.617,17.987 45.833,18.041 46.094,18.041Z" + android:strokeAlpha="0.9" + android:fillColor="#D3D4D5" + android:fillAlpha="0.9"/> + android:pathData="M44.034,14.5L42.457,18.5H41.79L40.213,14.5H40.924L42.101,17.687H42.146L43.323,14.5H44.034Z" + android:strokeAlpha="0.9" + android:fillColor="#D3D4D5" + android:fillAlpha="0.9"/> + android:pathData="M38.723,18.5V14.5H39.379V18.5H38.723ZM39.056,13.833C38.929,13.833 38.819,13.792 38.726,13.711C38.635,13.629 38.59,13.531 38.59,13.417C38.59,13.302 38.635,13.204 38.726,13.122C38.819,13.041 38.929,13 39.056,13C39.184,13 39.293,13.041 39.384,13.122C39.477,13.204 39.523,13.302 39.523,13.417C39.523,13.531 39.477,13.629 39.384,13.711C39.293,13.792 39.184,13.833 39.056,13.833Z" + android:strokeAlpha="0.9" + android:fillColor="#D3D4D5" + android:fillAlpha="0.9"/> + + diff --git a/app/src/main/res/drawable/market_logo_light.xml b/app/src/main/res/drawable/market_logo_light.xml index 0998c1b404..b201047a9b 100644 --- a/app/src/main/res/drawable/market_logo_light.xml +++ b/app/src/main/res/drawable/market_logo_light.xml @@ -1,35 +1,18 @@ - + android:width="86dp" + android:height="20dp" + android:viewportWidth="86" + android:viewportHeight="20"> + android:pathData="M22.671,1C22.85,1 22.994,1.145 23,1.325L23,1.335V18.665C23,18.715 22.989,18.764 22.968,18.809C22.892,18.973 22.702,19.045 22.54,18.972L22.53,18.968L18.318,16.925C18.121,16.83 17.995,16.63 17.99,16.409L17.989,16.395V3.31C17.989,3.076 18.126,2.865 18.336,2.772L18.348,2.766L22.548,1.025C22.587,1.008 22.629,1 22.671,1ZM0.329,1C0.371,1 0.413,1.008 0.452,1.025L4.652,2.766C4.869,2.856 5.011,3.071 5.011,3.31V16.395C5.011,16.621 4.883,16.828 4.682,16.925L0.47,18.968C0.306,19.047 0.11,18.976 0.032,18.809C0.011,18.764 0,18.715 0,18.665V1.335C0,1.15 0.147,1 0.329,1ZM11.87,4.785L15.754,7.074C15.983,7.209 16.124,7.458 16.124,7.728V12.305C16.124,12.575 15.983,12.824 15.754,12.959L11.87,15.248C11.641,15.382 11.359,15.382 11.131,15.248L7.246,12.959C7.017,12.824 6.876,12.575 6.876,12.305V7.728C6.876,7.458 7.017,7.209 7.246,7.074L11.131,4.785C11.359,4.651 11.641,4.651 11.87,4.785Z" + android:fillType="evenOdd"> @@ -37,18 +20,103 @@ + android:pathData="M58.451,5.807V9.999H56.781V2.845H58.377V4.061H58.46C58.623,3.66 58.883,3.342 59.24,3.106C59.6,2.87 60.044,2.752 60.573,2.752C61.062,2.752 61.488,2.858 61.851,3.069C62.217,3.28 62.5,3.586 62.7,3.986C62.903,4.387 63.003,4.873 63,5.444V9.999H61.33V5.705C61.33,5.227 61.207,4.853 60.961,4.582C60.718,4.312 60.381,4.177 59.95,4.177C59.658,4.177 59.398,4.242 59.171,4.373C58.946,4.5 58.77,4.685 58.64,4.927C58.514,5.169 58.451,5.463 58.451,5.807Z" + android:strokeAlpha="0.9" + android:fillColor="#333333" + android:fillAlpha="0.9"/> + + + + + + + + + + + + + + android:pathData="M50.847,18.583C50.447,18.583 50.103,18.494 49.814,18.317C49.525,18.14 49.303,17.896 49.147,17.585C48.992,17.274 48.914,16.919 48.914,16.52C48.914,16.114 48.994,15.755 49.153,15.445C49.314,15.132 49.538,14.888 49.825,14.713C50.114,14.536 50.451,14.447 50.836,14.447C51.136,14.447 51.406,14.499 51.646,14.604C51.887,14.708 52.084,14.854 52.238,15.041C52.392,15.229 52.487,15.447 52.524,15.697H51.869C51.819,15.515 51.708,15.354 51.535,15.213C51.365,15.071 51.136,14.999 50.847,14.999C50.591,14.999 50.367,15.062 50.175,15.187C49.984,15.31 49.835,15.485 49.728,15.71C49.622,15.934 49.569,16.197 49.569,16.499C49.569,16.808 49.621,17.077 49.725,17.307C49.83,17.536 49.979,17.714 50.169,17.84C50.362,17.967 50.588,18.031 50.847,18.031C51.017,18.031 51.172,18.003 51.311,17.947C51.449,17.892 51.567,17.812 51.663,17.708C51.759,17.604 51.828,17.479 51.869,17.333H52.524C52.487,17.569 52.395,17.781 52.249,17.971C52.105,18.158 51.913,18.307 51.674,18.419C51.437,18.528 51.161,18.583 50.847,18.583Z" + android:strokeAlpha="0.9" + android:fillColor="#333333" + android:fillAlpha="0.9"/> + android:pathData="M45.994,18.593C45.724,18.593 45.479,18.545 45.258,18.45C45.038,18.353 44.863,18.213 44.733,18.031C44.604,17.847 44.539,17.624 44.539,17.364C44.539,17.135 44.587,16.949 44.683,16.807C44.78,16.663 44.908,16.55 45.069,16.468C45.23,16.386 45.408,16.326 45.603,16.286C45.799,16.244 45.996,16.211 46.194,16.187C46.453,16.156 46.663,16.132 46.825,16.117C46.987,16.099 47.106,16.07 47.18,16.031C47.256,15.991 47.294,15.921 47.294,15.822V15.801C47.294,15.545 47.219,15.345 47.069,15.203C46.921,15.06 46.696,14.989 46.394,14.989C46.081,14.989 45.836,15.053 45.658,15.182C45.48,15.31 45.355,15.447 45.283,15.593L44.661,15.385C44.772,15.142 44.92,14.953 45.106,14.817C45.292,14.68 45.496,14.584 45.716,14.531C45.939,14.475 46.157,14.447 46.372,14.447C46.509,14.447 46.666,14.463 46.844,14.494C47.023,14.524 47.197,14.585 47.363,14.679C47.532,14.773 47.671,14.914 47.783,15.104C47.894,15.293 47.949,15.546 47.949,15.864V18.499H47.294V17.958H47.26C47.216,18.044 47.142,18.137 47.038,18.236C46.935,18.335 46.797,18.419 46.625,18.489C46.452,18.558 46.242,18.593 45.994,18.593ZM46.094,18.041C46.353,18.041 46.572,17.993 46.75,17.898C46.929,17.802 47.064,17.679 47.155,17.528C47.248,17.377 47.294,17.218 47.294,17.051V16.489C47.266,16.52 47.205,16.549 47.111,16.575C47.018,16.599 46.911,16.621 46.788,16.64C46.668,16.657 46.55,16.673 46.436,16.687C46.323,16.699 46.231,16.709 46.161,16.718C45.991,16.739 45.831,16.773 45.683,16.82C45.537,16.865 45.418,16.933 45.328,17.025C45.239,17.116 45.194,17.239 45.194,17.395C45.194,17.609 45.279,17.77 45.447,17.879C45.617,17.987 45.833,18.041 46.094,18.041Z" + android:strokeAlpha="0.9" + android:fillColor="#333333" + android:fillAlpha="0.9"/> + android:pathData="M44.034,14.5L42.457,18.5H41.79L40.213,14.5H40.924L42.101,17.687H42.146L43.323,14.5H44.034Z" + android:strokeAlpha="0.9" + android:fillColor="#333333" + android:fillAlpha="0.9"/> + android:pathData="M38.723,18.5V14.5H39.379V18.5H38.723ZM39.056,13.833C38.929,13.833 38.819,13.792 38.726,13.711C38.635,13.629 38.59,13.531 38.59,13.417C38.59,13.302 38.635,13.204 38.726,13.122C38.819,13.041 38.929,13 39.056,13C39.184,13 39.293,13.041 39.384,13.122C39.477,13.204 39.523,13.302 39.523,13.417C39.523,13.531 39.477,13.629 39.384,13.711C39.293,13.792 39.184,13.833 39.056,13.833Z" + android:strokeAlpha="0.9" + android:fillColor="#333333" + android:fillAlpha="0.9"/> + + diff --git a/app/src/main/res/drawable/perpetual_logo.xml b/app/src/main/res/drawable/perpetual_logo.xml index 818129b86a..8a4f128c27 100644 --- a/app/src/main/res/drawable/perpetual_logo.xml +++ b/app/src/main/res/drawable/perpetual_logo.xml @@ -1,46 +1,112 @@ - + android:width="86dp" + android:height="20dp" + android:viewportWidth="86" + android:viewportHeight="20"> + android:fillType="evenOdd" + android:fillAlpha="0.9"/> + android:fillAlpha="0.9"/> + android:fillAlpha="0.9"/> + android:fillAlpha="0.9"/> + android:fillAlpha="0.9"/> + android:fillAlpha="0.9"/> + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_market_share_bottom.xml b/app/src/main/res/layout/fragment_market_share_bottom.xml index 2a86e7b235..904818681f 100644 --- a/app/src/main/res/layout/fragment_market_share_bottom.xml +++ b/app/src/main/res/layout/fragment_market_share_bottom.xml @@ -65,14 +65,13 @@ android:layout_width="match_parent" android:layout_height="42dp" android:gravity="center_vertical" - android:orientation="horizontal" - android:paddingStart="20dp" - android:paddingEnd="20dp"> + android:orientation="horizontal"> diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index c6c4ce45e4..70477cf1fa 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -2501,4 +2501,5 @@ 收益额 邀请好友一起交易 通过此链接赚取 %1$s 分佣 + 涨跌榜 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 993155e680..3a21fe3828 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -1104,4 +1104,5 @@ 觸發止損:控制虧損 止盈 止損 + 漲跌榜 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 68d0c47ca7..43747a3bba 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2471,6 +2471,7 @@ Choose a Country or Region Anonymous Number Trending + Top Movers Market 🚀 Perps %1$s - 📈 Market: %1$s