diff --git a/.gitignore b/.gitignore index d46fbdb076..44f05d31ee 100644 --- a/.gitignore +++ b/.gitignore @@ -25,5 +25,4 @@ app/*.apk !/core-db/schemas/io.novafoundation.nova.core_db.AppDatabase/8.json !/core-db/schemas/io.novafoundation.nova.core_db.AppDatabase/9.json -google-services.json -/bindings +google-services.json \ No newline at end of file diff --git a/app/src/androidTest/java/io/novafoundation/nova/BlockParsingIntegrationTest.kt b/app/src/androidTest/java/io/novafoundation/nova/BlockParsingIntegrationTest.kt index db0a69b71b..621eb20791 100644 --- a/app/src/androidTest/java/io/novafoundation/nova/BlockParsingIntegrationTest.kt +++ b/app/src/androidTest/java/io/novafoundation/nova/BlockParsingIntegrationTest.kt @@ -50,7 +50,7 @@ class BlockParsingIntegrationTest { keyBuilder = { it.metadata.system().storage("Events").storageKey() }, binding = { scale, runtime -> Log.d(logTag, scale!!) - bindEventRecords(scale, runtime) + bindEventRecords(scale) } ) diff --git a/app/src/androidTest/java/io/novafoundation/nova/MoonbaseSendIntagrationTest.kt b/app/src/androidTest/java/io/novafoundation/nova/MoonbaseSendIntagrationTest.kt index 773e2ef19c..346bcdbd7a 100644 --- a/app/src/androidTest/java/io/novafoundation/nova/MoonbaseSendIntagrationTest.kt +++ b/app/src/androidTest/java/io/novafoundation/nova/MoonbaseSendIntagrationTest.kt @@ -89,7 +89,7 @@ class MoonbaseSendIntagrationTest { val extrinsic = extrinsicBuilderFactory.create(chain, signer, accountId) .nativeTransfer(accountId, chain.utilityAsset.planksFromAmount(BigDecimal.ONE), keepAlive = true) - .buildExtrinsic().extrinsicHex + .buildExtrinsic() val hash = rpcCalls.submitExtrinsic(chain.id, extrinsic) diff --git a/app/src/androidTest/java/io/novafoundation/nova/SwapServiceIntegrationTest.kt b/app/src/androidTest/java/io/novafoundation/nova/SwapServiceIntegrationTest.kt deleted file mode 100644 index 2ddfdc3e3e..0000000000 --- a/app/src/androidTest/java/io/novafoundation/nova/SwapServiceIntegrationTest.kt +++ /dev/null @@ -1,186 +0,0 @@ -package io.novafoundation.nova - -import android.util.Log -import io.novafoundation.nova.common.di.FeatureUtils -import io.novafoundation.nova.common.utils.Percent -import io.novafoundation.nova.feature_swap_api.di.SwapFeatureApi -import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecuteArgs -import io.novafoundation.nova.feature_swap_api.domain.model.SwapLimit -import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuote -import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuoteArgs -import io.novafoundation.nova.feature_swap_core.domain.model.SwapQuoteException -import io.novafoundation.nova.feature_swap_api.domain.model.swapRate -import io.novafoundation.nova.feature_swap_core.domain.model.QuotePath -import io.novafoundation.nova.feature_swap_core.domain.model.SwapDirection -import io.novafoundation.nova.feature_swap_impl.di.SwapFeatureComponent -import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance -import io.novafoundation.nova.feature_wallet_api.di.WalletFeatureApi -import io.novafoundation.nova.feature_wallet_api.domain.model.planksFromAmount -import io.novafoundation.nova.feature_wallet_api.presentation.formatters.formatPlanks -import io.novafoundation.nova.feature_wallet_api.presentation.formatters.formatTokenAmount -import io.novafoundation.nova.feature_wallet_impl.di.WalletFeatureComponent -import io.novafoundation.nova.runtime.ext.Geneses -import io.novafoundation.nova.runtime.ext.commissionAsset -import io.novafoundation.nova.runtime.ext.utilityAsset -import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry -import io.novafoundation.nova.runtime.multiNetwork.asset -import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain -import io.novafoundation.nova.runtime.multiNetwork.chainWithAsset -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.first -import org.junit.Test -import java.math.BigDecimal - -class SwapServiceIntegrationTest : BaseIntegrationTest() { - - private val swapApi = FeatureUtils.getFeature(context, SwapFeatureApi::class.java) - - private val walletApi = FeatureUtils.getFeature(context, WalletFeatureApi::class.java) - - private val tokenRepository = walletApi.provideTokenRepository() - private val arbitraryAssetUseCase = walletApi.arbitraryAssetUseCase - private val swapService = swapApi.swapService - - @Test - fun shouldRetrieveAvailableDirections() = runTest { - val allAvailableAssetIdsToSwap = swapService.assetsAvailableForSwap(this).first() - val allAvailableChainAssetsToSwap = allAvailableAssetIdsToSwap.map { - val (chain, asset) = chainRegistry.chainWithAsset(it.chainId, it.assetId) - - "${chain.name}::${asset.symbol}" - } - - Log.d("SwapServiceIntegrationTest", "All available tokens: ${allAvailableChainAssetsToSwap.joinToString()}") - } - - @Test - fun shouldRetrieveAvailableDirectionsForNativeAsset() = runTest { - val westmint = chainRegistry.westmint() - - findAvailableDirectionsFor(westmint.wnd()) - } - - @Test - fun shouldRetrieveAvailableDirectionsForLocalAsset() = runTest { - val westmint = chainRegistry.westmint() - - findAvailableDirectionsFor(westmint.siri()) - } - - @Test - fun shouldRetrieveAvailableDirectionsForForeignAsset() = runTest { - val westmint = chainRegistry.westmint() - - findAvailableDirectionsFor(westmint.dot()) - } - - @Test - fun shouldCalculateNativeAssetFee() = runTest { - val westmint = chainRegistry.westmint() - val wnd = westmint.wnd() - val siri = westmint.siri() - - val swapArgs = SwapExecuteArgs( - assetIn = wnd, - assetOut = siri, - swapLimit = SwapLimit.SpecifiedIn( - expectedAmountIn = siri.planksFromAmount(0.000001.toBigDecimal()), - amountOutMin = Balance.ZERO, - expectedAmountOut = Balance.ZERO - ), - customFeeAsset = null, - nativeAsset = arbitraryAssetUseCase.assetFlow(westmint.commissionAsset).first(), - path = QuotePath(emptyList()) - ) - - val fee = swapService.estimateFee(swapArgs) - - Log.d("SwapServiceIntegrationTest", "Fee for swapping ${wnd.symbol} to ${wnd.symbol} is ${fee.networkFee.amount.formatPlanks(wnd)}") - } - - @Test - fun shouldCalculateCustomAssetFee() = runTest { - val westmint = chainRegistry.westmint() - val wnd = westmint.wnd() - val siri = westmint.siri() - - val swapArgs = SwapExecuteArgs( - assetIn = siri, - assetOut = wnd, - swapLimit = SwapLimit.SpecifiedIn( - expectedAmountIn = siri.planksFromAmount(0.000001.toBigDecimal()), - amountOutMin = Balance.ZERO, - expectedAmountOut = Balance.ZERO - ), - customFeeAsset = siri, - nativeAsset = arbitraryAssetUseCase.assetFlow(westmint.commissionAsset).first(), - path = QuotePath(emptyList()) - ) - - val fee = swapService.estimateFee(swapArgs) - - Log.d("SwapServiceIntegrationTest", "Fee for swapping ${wnd.symbol} to ${siri.symbol} is ${fee.networkFee.amount.formatPlanks(siri)}") - } - - @Test - fun shouldQuoteLocalAssetSwap() = runTest { - val westmint = chainRegistry.westmint() - - quoteSwap(from = westmint.wnd(), to = westmint.siri(), amount = 0.000001) - } - - @Test(expected = SwapQuoteException.NotEnoughLiquidity::class) - fun shouldQuoteForeignAssetSwap() = runTest { - val westmint = chainRegistry.westmint() - - quoteSwap(from = westmint.wnd(), to = westmint.dot(), amount = 0.000001) - } - - private suspend fun quoteSwap(from: Chain.Asset, to: Chain.Asset, amount: Double) { - val swapQuote = swapService.quote( - args = SwapQuoteArgs( - tokenIn = tokenRepository.getToken(from), - tokenOut = tokenRepository.getToken(to), - amount = from.planksFromAmount(amount.toBigDecimal()), - swapDirection = SwapDirection.SPECIFIED_IN, - slippage = Percent(1.0), - ) - ).getOrThrow() - - Log.d("SwapServiceIntegrationTest", swapQuote.format()) - } - - private suspend fun CoroutineScope.findAvailableDirectionsFor(asset: Chain.Asset) { - val directionsForWnd = swapService.availableSwapDirectionsFor(asset, this).first() - val directionsForWndFormatted = directionsForWnd.map { otherId -> - val otherAsset = chainRegistry.asset(otherId) - - "${asset.symbol} - ${otherAsset.symbol}" - } - - Log.d("SwapServiceIntegrationTest", "Available directions for ${asset.symbol}: ${directionsForWndFormatted.joinToString()}") - } - - private fun SwapQuote.format(): String { - return """ - Swapping ${planksIn.formatPlanks(assetIn)} yields ${planksOut.formatPlanks(assetOut)}. - Swap rate is ${BigDecimal.ONE.formatTokenAmount(assetIn)} = ${swapRate().formatTokenAmount(assetOut)}" - """.trimIndent() - } - - private suspend fun ChainRegistry.westmint(): Chain { - return getChain(Chain.Geneses.WESTMINT) - } - - private fun Chain.siri(): Chain.Asset { - return assets.first { it.symbol.value == "SIRI" } - } - - private fun Chain.dot(): Chain.Asset { - return assets.first { it.symbol.value == "DOT" } - } - - private fun Chain.wnd(): Chain.Asset { - return utilityAsset - } -} diff --git a/app/src/main/java/io/novafoundation/nova/app/di/deps/ComponentHolderModule.kt b/app/src/main/java/io/novafoundation/nova/app/di/deps/ComponentHolderModule.kt index 6189fc1e74..b5dbda7cc7 100644 --- a/app/src/main/java/io/novafoundation/nova/app/di/deps/ComponentHolderModule.kt +++ b/app/src/main/java/io/novafoundation/nova/app/di/deps/ComponentHolderModule.kt @@ -51,8 +51,8 @@ import io.novafoundation.nova.feature_settings_impl.di.SettingsFeatureHolder import io.novafoundation.nova.feature_staking_api.di.StakingFeatureApi import io.novafoundation.nova.feature_staking_impl.di.StakingFeatureHolder import io.novafoundation.nova.feature_swap_api.di.SwapFeatureApi -import io.novafoundation.nova.feature_swap_core.di.SwapCoreApi import io.novafoundation.nova.feature_swap_core.di.SwapCoreHolder +import io.novafoundation.nova.feature_swap_core_api.di.SwapCoreApi import io.novafoundation.nova.feature_swap_impl.di.SwapFeatureHolder import io.novafoundation.nova.feature_versions_api.di.VersionsFeatureApi import io.novafoundation.nova.feature_versions_impl.di.VersionsFeatureHolder diff --git a/app/src/main/java/io/novafoundation/nova/app/root/navigation/Navigator.kt b/app/src/main/java/io/novafoundation/nova/app/root/navigation/Navigator.kt index b31933209d..035bca311e 100644 --- a/app/src/main/java/io/novafoundation/nova/app/root/navigation/Navigator.kt +++ b/app/src/main/java/io/novafoundation/nova/app/root/navigation/Navigator.kt @@ -399,14 +399,14 @@ class Navigator( navController?.navigate(R.id.action_selectAssetSwapFlowFragment_to_swapFlowNetworkFragment, NetworkSwapFlowFragment.createPayload(payload)) } - override fun openBuyNetworks(payload: NetworkFlowPayload) { - navController?.navigate(R.id.action_buyFlow_to_buyFlowNetwork, NetworkFlowFragment.createPayload(payload)) - } - override fun returnToMainSwapScreen() { navController?.navigate(R.id.action_return_to_swap_settings) } + override fun openBuyNetworks(payload: NetworkFlowPayload) { + navController?.navigate(R.id.action_buyFlow_to_buyFlowNetwork, NetworkFlowFragment.createPayload(payload)) + } + override fun openSwapFlow() { val payload = SwapFlowPayload.InitialSelecting navController?.navigate(R.id.action_mainFragment_to_swapFlow, AssetSwapFlowFragment.getBundle(payload)) @@ -416,6 +416,10 @@ class Navigator( navController?.navigate(R.id.action_open_swapSetupAmount, SwapMainSettingsFragment.getBundle(swapSettingsPayload)) } + override fun finishSelectAndOpenSwapSetupAmount(swapSettingsPayload: SwapSettingsPayload) { + navController?.navigate(R.id.action_finish_and_open_swap_settings, SwapMainSettingsFragment.getBundle(swapSettingsPayload)) + } + override fun openNfts() { navController?.navigate(R.id.action_mainFragment_to_nfts_nav_graph) } diff --git a/app/src/main/java/io/novafoundation/nova/app/root/navigation/swap/SwapNavigator.kt b/app/src/main/java/io/novafoundation/nova/app/root/navigation/swap/SwapNavigator.kt index ba72cd2dae..4c37b61c92 100644 --- a/app/src/main/java/io/novafoundation/nova/app/root/navigation/swap/SwapNavigator.kt +++ b/app/src/main/java/io/novafoundation/nova/app/root/navigation/swap/SwapNavigator.kt @@ -4,13 +4,13 @@ import io.novafoundation.nova.app.R import io.novafoundation.nova.app.root.navigation.BaseNavigator import io.novafoundation.nova.app.root.navigation.NavigationHolder import io.novafoundation.nova.app.root.navigation.Navigator -import io.novafoundation.nova.feature_assets.presentation.send.amount.SendPayload import io.novafoundation.nova.feature_assets.presentation.balance.detail.BalanceDetailFragment +import io.novafoundation.nova.feature_assets.presentation.send.amount.SendPayload import io.novafoundation.nova.feature_assets.presentation.swap.asset.AssetSwapFlowFragment import io.novafoundation.nova.feature_assets.presentation.swap.asset.SwapFlowPayload +import io.novafoundation.nova.feature_swap_api.presentation.model.SwapSettingsPayload import io.novafoundation.nova.feature_swap_impl.presentation.SwapRouter -import io.novafoundation.nova.feature_swap_impl.presentation.confirmation.SwapConfirmationFragment -import io.novafoundation.nova.feature_swap_impl.presentation.confirmation.payload.SwapConfirmationPayload +import io.novafoundation.nova.feature_swap_impl.presentation.main.SwapMainSettingsFragment import io.novafoundation.nova.feature_wallet_api.presentation.model.AssetPayload class SwapNavigator( @@ -18,17 +18,25 @@ class SwapNavigator( private val commonDelegate: Navigator ) : BaseNavigator(navigationHolder), SwapRouter { - override fun openSwapConfirmation(payload: SwapConfirmationPayload) { - val bundle = SwapConfirmationFragment.getBundle(payload) - navigationHolder.navController?.navigate(R.id.action_swapMainSettingsFragment_to_swapConfirmationFragment, bundle) - } + override fun openSwapConfirmation() = performNavigation(R.id.action_swapMainSettingsFragment_to_swapConfirmationFragment) + + override fun openSwapRoute() = performNavigation(R.id.action_open_swapRouteFragment) + + override fun openSwapFee() = performNavigation(R.id.action_open_swapFeeFragment) + + override fun openSwapExecution() = performNavigation(R.id.action_swapConfirmationFragment_to_swapExecutionFragment) override fun openSwapOptions() { navigationHolder.navController?.navigate(R.id.action_swapMainSettingsFragment_to_swapOptionsFragment) } + override fun openRetrySwap(payload: SwapSettingsPayload) = performNavigation( + actionId = R.id.action_swapExecutionFragment_to_swapSettingsFragment, + args = SwapMainSettingsFragment.getBundle(payload) + ) + override fun openBalanceDetails(assetPayload: AssetPayload) { - navigationHolder.navController?.navigate(R.id.action_swapConfirmationFragment_to_assetDetails, BalanceDetailFragment.getBundle(assetPayload)) + navigationHolder.navController?.navigate(R.id.action_swapExecutionFragment_to_assetDetails, BalanceDetailFragment.getBundle(assetPayload)) } override fun selectAssetIn(selectedAsset: AssetPayload?) { diff --git a/app/src/main/res/navigation/main_nav_graph.xml b/app/src/main/res/navigation/main_nav_graph.xml index d085d337b2..36bf78a30d 100644 --- a/app/src/main/res/navigation/main_nav_graph.xml +++ b/app/src/main/res/navigation/main_nav_graph.xml @@ -937,6 +937,7 @@ + + + diff --git a/app/src/main/res/navigation/select_swap_token_nav_graph.xml b/app/src/main/res/navigation/select_swap_token_nav_graph.xml index 45123fda55..4c8d3046d8 100644 --- a/app/src/main/res/navigation/select_swap_token_nav_graph.xml +++ b/app/src/main/res/navigation/select_swap_token_nav_graph.xml @@ -1,8 +1,8 @@ + android:id="@+id/select_swap_token_nav_graph" + app:startDestination="@id/selectAssetSwapFlowFragment"> + app:popExitAnim="@anim/fragment_open_exit" + app:popUpTo="@id/swapSettingsFragment" /> + + \ No newline at end of file diff --git a/app/src/main/res/navigation/start_swap_nav_graph.xml b/app/src/main/res/navigation/start_swap_nav_graph.xml index 9d9fdb5c4a..bda06dd883 100644 --- a/app/src/main/res/navigation/start_swap_nav_graph.xml +++ b/app/src/main/res/navigation/start_swap_nav_graph.xml @@ -33,23 +33,19 @@ app:exitAnim="@anim/fragment_open_exit" app:popEnterAnim="@anim/fragment_close_enter" app:popExitAnim="@anim/fragment_close_exit" /> - + tools:layout="@layout/fragment_swap_confirmation"> + android:id="@+id/action_swapConfirmationFragment_to_swapExecutionFragment" + app:popUpTo="@id/swapConfirmationFragment" + app:popUpToInclusive="true" + app:destination="@id/swapExecutionFragment" /> @@ -60,4 +56,50 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/bindings/hydra-dx-math/src/main/java/io/novafoundation/nova/hydra_dx_math/xyk/HYKSwapMathBridge.java b/bindings/hydra-dx-math/src/main/java/io/novafoundation/nova/hydra_dx_math/xyk/HYKSwapMathBridge.java index bef3c36920..f9adec639d 100644 --- a/bindings/hydra-dx-math/src/main/java/io/novafoundation/nova/hydra_dx_math/xyk/HYKSwapMathBridge.java +++ b/bindings/hydra-dx-math/src/main/java/io/novafoundation/nova/hydra_dx_math/xyk/HYKSwapMathBridge.java @@ -23,4 +23,4 @@ public static native String calculate_pool_trade_fee( String feeNumerator, String feeDenominator ); -} +} \ No newline at end of file diff --git a/build.gradle b/build.gradle index 4716250319..aad099365e 100644 --- a/build.gradle +++ b/build.gradle @@ -1,8 +1,8 @@ buildscript { ext { // App version - versionName = '9.0.3' - versionCode = 162 + versionName = '9.1.0' + versionCode = 163 applicationId = "io.novafoundation.nova" releaseApplicationSuffix = "market" @@ -51,7 +51,7 @@ buildscript { web3jVersion = '4.9.5' - substrateSdkVersion = '2.3.0' + substrateSdkVersion = '2.4.0' gifVersion = '1.2.19' diff --git a/common/src/main/java/io/novafoundation/nova/common/data/network/runtime/binding/Events.kt b/common/src/main/java/io/novafoundation/nova/common/data/network/runtime/binding/Events.kt index 76f570b7cc..370c63031a 100644 --- a/common/src/main/java/io/novafoundation/nova/common/data/network/runtime/binding/Events.kt +++ b/common/src/main/java/io/novafoundation/nova/common/data/network/runtime/binding/Events.kt @@ -1,12 +1,8 @@ package io.novafoundation.nova.common.data.network.runtime.binding -import io.novafoundation.nova.common.utils.system -import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot import io.novasama.substrate_sdk_android.runtime.definitions.types.composite.DictEnum import io.novasama.substrate_sdk_android.runtime.definitions.types.composite.Struct -import io.novasama.substrate_sdk_android.runtime.definitions.types.fromHex import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericEvent -import io.novasama.substrate_sdk_android.runtime.metadata.storage import java.math.BigInteger class EventRecord(val phase: Phase, val event: GenericEvent.Instance) @@ -20,14 +16,17 @@ sealed class Phase { object Initialization : Phase() } -@HelperBinding -fun bindEventRecord(dynamicInstance: Any?): EventRecord { +fun bindEventRecords(decoded: Any?): List { + return bindList(decoded, ::bindEventRecord) +} + +private fun bindEventRecord(dynamicInstance: Any?): EventRecord { requireType(dynamicInstance) val phaseDynamic = dynamicInstance.getTyped>("phase") val phase = when (phaseDynamic.name) { - "ApplyExtrinsic" -> Phase.ApplyExtrinsic(phaseDynamic.value.cast()) + "ApplyExtrinsic" -> Phase.ApplyExtrinsic(bindNumber(phaseDynamic.value)) "Finalization" -> Phase.Finalization "Initialization" -> Phase.Initialization else -> incompatible() @@ -37,18 +36,3 @@ fun bindEventRecord(dynamicInstance: Any?): EventRecord { return EventRecord(phase, dynamicEvent) } - -@UseCaseBinding -fun bindEventRecords( - scale: String, - runtime: RuntimeSnapshot, -): List { - val returnType = runtime.metadata.system().storage("Events").type.value ?: incompatible() - - val dynamicInstance = returnType.fromHex(runtime, scale) - requireType>(dynamicInstance) - - return dynamicInstance.mapNotNull { dynamicEventRecord -> - bindOrNull { bindEventRecord(dynamicEventRecord) } - } -} diff --git a/common/src/main/java/io/novafoundation/nova/common/data/repository/AssetsIconModeRepository.kt b/common/src/main/java/io/novafoundation/nova/common/data/repository/AssetsIconModeRepository.kt index 1ff99d29f8..9d8073ddb5 100644 --- a/common/src/main/java/io/novafoundation/nova/common/data/repository/AssetsIconModeRepository.kt +++ b/common/src/main/java/io/novafoundation/nova/common/data/repository/AssetsIconModeRepository.kt @@ -6,6 +6,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map interface AssetsIconModeRepository { + fun assetsIconModeFlow(): Flow fun setAssetsIconMode(assetsViewMode: AssetIconMode) diff --git a/common/src/main/java/io/novafoundation/nova/common/domain/ExtendedLoadingState.kt b/common/src/main/java/io/novafoundation/nova/common/domain/ExtendedLoadingState.kt index b1c23d5159..e63d447658 100644 --- a/common/src/main/java/io/novafoundation/nova/common/domain/ExtendedLoadingState.kt +++ b/common/src/main/java/io/novafoundation/nova/common/domain/ExtendedLoadingState.kt @@ -42,6 +42,10 @@ val ExtendedLoadingState.dataOrNull: T? else -> null } +@get:JvmName("isErrorProp") +val ExtendedLoadingState<*>.isError: Boolean + get() = this is ExtendedLoadingState.Error + fun ExtendedLoadingState.loadedAndEmpty(): Boolean = when (this) { is ExtendedLoadingState.Loaded -> data == null else -> false @@ -55,6 +59,10 @@ fun ExtendedLoadingState<*>.isLoading(): Boolean { return this is ExtendedLoadingState.Loading } +@get:JvmName("isLoadingProp") +val ExtendedLoadingState<*>.isLoading: Boolean + get() = isLoading() + fun ExtendedLoadingState<*>.isError(): Boolean { return this is ExtendedLoadingState.Error } diff --git a/common/src/main/java/io/novafoundation/nova/common/mixin/impl/RetriableUi.kt b/common/src/main/java/io/novafoundation/nova/common/mixin/impl/RetriableUi.kt index 6c3c0b21a0..11ff5eea13 100644 --- a/common/src/main/java/io/novafoundation/nova/common/mixin/impl/RetriableUi.kt +++ b/common/src/main/java/io/novafoundation/nova/common/mixin/impl/RetriableUi.kt @@ -10,14 +10,16 @@ fun BaseFragmentMixin<*>.observeRetries( retriable: Retriable, context: Context = fragment.requireContext(), ) { - retriable.retryEvent.observeEvent { - retryDialog( - context = context, - onRetry = it.onRetry, - onCancel = it.onCancel - ) { - setTitle(it.title) - setMessage(it.message) + with(retriable) { + retryEvent.observeEvent { + retryDialog( + context = context, + onRetry = it.onRetry, + onCancel = it.onCancel + ) { + setTitle(it.title) + setMessage(it.message) + } } } } diff --git a/common/src/main/java/io/novafoundation/nova/common/presentation/AssetIconProvider.kt b/common/src/main/java/io/novafoundation/nova/common/presentation/AssetIconProvider.kt index b764abf57e..d34692a2a0 100644 --- a/common/src/main/java/io/novafoundation/nova/common/presentation/AssetIconProvider.kt +++ b/common/src/main/java/io/novafoundation/nova/common/presentation/AssetIconProvider.kt @@ -10,9 +10,9 @@ interface AssetIconProvider { companion object; - fun getAssetIconOrFallback(iconName: String): Icon + fun getAssetIcon(iconName: String): Icon - fun getAssetIconOrFallback(iconName: String, iconMode: AssetIconMode): Icon + fun getAssetIcon(iconName: String, iconMode: AssetIconMode): Icon } class RealAssetIconProvider( @@ -21,11 +21,11 @@ class RealAssetIconProvider( private val whiteBaseUrl: String ) : AssetIconProvider { - override fun getAssetIconOrFallback(iconName: String): Icon { - return getAssetIconOrFallback(iconName, assetsIconModeRepository.getIconMode()) + override fun getAssetIcon(iconName: String): Icon { + return getAssetIcon(iconName, assetsIconModeRepository.getIconMode()) } - override fun getAssetIconOrFallback(iconName: String, iconMode: AssetIconMode): Icon { + override fun getAssetIcon(iconName: String, iconMode: AssetIconMode): Icon { val iconUrl = when (iconMode) { AssetIconMode.COLORED -> "$coloredBaseUrl/$iconName" AssetIconMode.WHITE -> "$whiteBaseUrl/$iconName" @@ -42,7 +42,7 @@ fun AssetIconProvider.getAssetIconOrFallback( iconName: String?, fallback: Icon = AssetIconProvider.fallbackIcon ): Icon { - return iconName?.let { getAssetIconOrFallback(it) } ?: fallback + return iconName?.let { getAssetIcon(it) } ?: fallback } fun AssetIconProvider.getAssetIconOrFallback( @@ -50,5 +50,5 @@ fun AssetIconProvider.getAssetIconOrFallback( iconMode: AssetIconMode, fallback: Icon = AssetIconProvider.fallbackIcon ): Icon { - return iconName?.let { getAssetIconOrFallback(it, iconMode) } ?: fallback + return iconName?.let { getAssetIcon(it, iconMode) } ?: fallback } diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/DurationExt.kt b/common/src/main/java/io/novafoundation/nova/common/utils/DurationExt.kt index b5ad2e2658..2257ea7ccd 100644 --- a/common/src/main/java/io/novafoundation/nova/common/utils/DurationExt.kt +++ b/common/src/main/java/io/novafoundation/nova/common/utils/DurationExt.kt @@ -10,3 +10,6 @@ val Duration.lastHours: Int val Duration.lastMinutes: Int get() = this.toComponents { _, _, minutes, _, _ -> minutes } + +val Duration.lastSeconds: Int + get() = this.toComponents { _, _, _, seconds, _ -> seconds } diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/FearlessLibExt.kt b/common/src/main/java/io/novafoundation/nova/common/utils/FearlessLibExt.kt index d1e2b9cce2..6a03e3b440 100644 --- a/common/src/main/java/io/novafoundation/nova/common/utils/FearlessLibExt.kt +++ b/common/src/main/java/io/novafoundation/nova/common/utils/FearlessLibExt.kt @@ -29,7 +29,6 @@ import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.Defa import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.ExtrasIncludedInExtrinsic import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.ExtrasIncludedInSignature import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.Extrinsic -import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.Extrinsic.EncodingInstance.CallRepresentation import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericEvent import io.novasama.substrate_sdk_android.runtime.definitions.types.skipAliases @@ -40,6 +39,7 @@ import io.novasama.substrate_sdk_android.runtime.metadata.ExtrinsicMetadata import io.novasama.substrate_sdk_android.runtime.metadata.RuntimeMetadata import io.novasama.substrate_sdk_android.runtime.metadata.callOrNull import io.novasama.substrate_sdk_android.runtime.metadata.fullName +import io.novasama.substrate_sdk_android.runtime.metadata.method import io.novasama.substrate_sdk_android.runtime.metadata.module import io.novasama.substrate_sdk_android.runtime.metadata.module.Constant import io.novasama.substrate_sdk_android.runtime.metadata.module.Event @@ -48,6 +48,7 @@ import io.novasama.substrate_sdk_android.runtime.metadata.module.MetadataFunctio import io.novasama.substrate_sdk_android.runtime.metadata.module.Module import io.novasama.substrate_sdk_android.runtime.metadata.module.StorageEntry import io.novasama.substrate_sdk_android.runtime.metadata.moduleOrNull +import io.novasama.substrate_sdk_android.runtime.metadata.runtimeApiOrNull import io.novasama.substrate_sdk_android.runtime.metadata.splitKey import io.novasama.substrate_sdk_android.runtime.metadata.storageOrNull import io.novasama.substrate_sdk_android.scale.EncodableStruct @@ -185,7 +186,7 @@ fun Constant.decodedValue(runtimeSnapshot: RuntimeSnapshot): Any? { fun String.toHexAccountId(): String = toAccountId().toHexString() -fun Extrinsic.DecodedInstance.tip(): BigInteger? = signature?.signedExtras?.get(DefaultSignedExtensions.CHECK_TX_PAYMENT) as? BigInteger +fun Extrinsic.Instance.tip(): BigInteger? = signature?.signedExtras?.get(DefaultSignedExtensions.CHECK_TX_PAYMENT) as? BigInteger fun Module.constant(name: String) = constantOrNull(name) ?: throw NoSuchElementException() @@ -354,6 +355,13 @@ fun GenericEvent.Instance.instanceOf(moduleName: String, eventName: String): Boo fun GenericEvent.Instance.instanceOf(event: Event): Boolean = event.index == this.event.index +fun RuntimeMetadata.assetConversionAssetIdType(): RuntimeType<*, *>? { + val runtimeApi = runtimeApiOrNull("AssetConversionApi") ?: return null + + return runtimeApi.method("quote_price_tokens_for_exact_tokens") + .inputs.first().type +} + fun structOf(vararg pairs: Pair) = Struct.Instance(mapOf(*pairs)) fun SignedRaw.toEcdsaSignatureData(): Sign.SignatureData { @@ -379,10 +387,6 @@ fun emptyEthereumAddress() = emptyEthereumAccountId().ethereumAccountIdToAddress val SignerPayloadExtrinsic.chainId: String get() = genesisHash.toHexString() -fun CallRepresentation.toCallInstance(): CallRepresentation.Instance? { - return (this as? CallRepresentation.Instance) -} - fun RuntimeMetadata.moduleOrFallback(name: String, vararg fallbacks: String): Module = modules[name] ?: fallbacks.firstOrNull { modules[it] != null } ?.let { modules[it] } ?: throw NoSuchElementException() @@ -395,6 +399,20 @@ suspend fun SocketService.awaitConnected() { networkStateFlow().first { it is SocketStateMachine.State.Connected } } +fun String.hexBytesSize(): Int { + val contentLength = if (startsWith("0x")) { + length - 2 + } else { + length + } + + return contentLength / 2 +} + +fun RuntimeMetadata.hasRuntimeApisMetadata(): Boolean { + return apis != null +} + object Modules { const val VESTING: String = "Vesting" diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/FlowExt.kt b/common/src/main/java/io/novafoundation/nova/common/utils/FlowExt.kt index 06686e8ced..f5cc131b41 100644 --- a/common/src/main/java/io/novafoundation/nova/common/utils/FlowExt.kt +++ b/common/src/main/java/io/novafoundation/nova/common/utils/FlowExt.kt @@ -15,6 +15,7 @@ import io.novafoundation.nova.common.utils.input.modifyInput import io.novafoundation.nova.common.utils.input.valueOrNull import io.novafoundation.nova.common.view.InsertableInputField import io.novafoundation.nova.common.view.input.seekbar.Seekbar +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -44,12 +45,12 @@ import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart -import kotlinx.coroutines.flow.runningFold import kotlinx.coroutines.flow.transform import kotlinx.coroutines.flow.transformLatest import kotlinx.coroutines.flow.transformWhile import kotlinx.coroutines.launch import kotlin.coroutines.coroutineContext +import kotlin.experimental.ExperimentalTypeInference import kotlin.time.Duration inline fun Flow>.filterList(crossinline handler: suspend (T) -> Boolean) = map { list -> @@ -130,11 +131,6 @@ fun List>.mergeIfMultiple(): Flow = when (size) { else -> merge() } -fun List>>.accumulateMaps(): Flow> { - return mergeIfMultiple() - .runningFold(emptyMap()) { acc, directions -> acc + directions } -} - inline fun withFlowScope(crossinline block: suspend (scope: CoroutineScope) -> Flow): Flow { return flowOfAll { val flowScope = CoroutineScope(coroutineContext) @@ -191,6 +187,18 @@ fun Flow.withLoadingShared(sourceSupplier: suspend (T) -> Flow): Fl .onCompletion { state = InnerState.SECONDARY_START } } +fun Flow.zipWithLastNonNull(): Flow> = flow { + var lastNonNull: T? = null + + collect { + emit(lastNonNull to it) + + if (it != null) { + lastNonNull = it + } + } +} + suspend inline fun Flow>.firstLoaded(): T = first { it.dataOrNull != null }.dataOrNull as T suspend fun Flow>.firstIfLoaded(): T? = first().dataOrNull @@ -244,6 +252,37 @@ fun Flow.withLoadingSingle(sourceSupplier: suspend (T) -> R): Flow Flow.wrapInResult(): Flow> { + return map { Result.success(it) } + .catch { emit(Result.failure(it)) } +} + +@Suppress("UNCHECKED_CAST") +@OptIn(ExperimentalTypeInference::class) +inline fun Flow>.transformResult( + @BuilderInference crossinline transform: suspend FlowCollector.(value: T) -> Unit +): Flow> { + return transform { upstream -> + upstream.onFailure { + emit(upstream as Result) + }.onSuccess { + val innerCollector = FlowCollector { + emit(Result.success(it)) + } + + runCatching { + transform(innerCollector, it) + }.onFailure { + if (it is CancellationException) { + throw it + } + + emit(Result.failure(it)) + } + } + } +} + fun Flow.withLoadingResult(source: suspend (T) -> Result): Flow> { return transformLatest { item -> emit(ExtendedLoadingState.Loading) @@ -270,6 +309,10 @@ fun Flow>.diffed(): Flow> { } } +suspend inline fun Flow.awaitTrue() { + first { it } +} + fun Flow.zipWithPrevious(): Flow> = flow { var current: T? = null diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/Fraction.kt b/common/src/main/java/io/novafoundation/nova/common/utils/Fraction.kt new file mode 100644 index 0000000000..7382f0c31c --- /dev/null +++ b/common/src/main/java/io/novafoundation/nova/common/utils/Fraction.kt @@ -0,0 +1,75 @@ +package io.novafoundation.nova.common.utils + +import java.math.BigDecimal + +@JvmInline +value class Fraction private constructor(private val value: Double) : Comparable { + + companion object { + + val ZERO: Fraction = Fraction(0.0) + + fun Double.toFraction(unit: FractionUnit): Fraction { + return Fraction(unit.convertToFraction(this)) + } + + val Double.percents: Fraction + get() = toFraction(FractionUnit.PERCENT) + + val BigDecimal.percents: Fraction + get() = toDouble().percents + + val BigDecimal.fractions: Fraction + get() = toDouble().fractions + + val Double.fractions: Fraction + get() = toFraction(FractionUnit.FRACTION) + + val Int.percents: Fraction + get() = toDouble().toFraction(FractionUnit.PERCENT) + } + + val inPercents: Double + get() = FractionUnit.PERCENT.convertFromFraction(value) + + val inFraction: Double + get() = FractionUnit.FRACTION.convertFromFraction(value) + + val inWholePercents: Int + get() = FractionUnit.PERCENT.convertFromFractionWhole(value) + + override fun compareTo(other: Fraction): Int { + return value.compareTo(other.value) + } +} + +enum class FractionUnit { + + /** + * Default range: 0..1 + */ + FRACTION, + + /** + * Default range: 0..100 + */ + PERCENT +} + +private fun FractionUnit.convertToFraction(value: Double): Double { + return when (this) { + FractionUnit.FRACTION -> value + FractionUnit.PERCENT -> value / 100 + } +} + +private fun FractionUnit.convertFromFraction(value: Double): Double { + return when (this) { + FractionUnit.FRACTION -> value + FractionUnit.PERCENT -> value * 100 + } +} + +private fun FractionUnit.convertFromFractionWhole(value: Double): Int { + return convertFromFraction(value).toInt() +} diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/KotlinExt.kt b/common/src/main/java/io/novafoundation/nova/common/utils/KotlinExt.kt index c6e87dfe0e..7ec587e3f7 100644 --- a/common/src/main/java/io/novafoundation/nova/common/utils/KotlinExt.kt +++ b/common/src/main/java/io/novafoundation/nova/common/utils/KotlinExt.kt @@ -1,6 +1,7 @@ package io.novafoundation.nova.common.utils import android.net.Uri +import android.util.Log import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred import kotlinx.coroutines.Job @@ -30,6 +31,9 @@ import kotlin.contracts.contract import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext import kotlin.math.sqrt +import kotlin.time.Duration +import kotlin.time.ExperimentalTime +import kotlin.time.measureTimedValue private val PERCENTAGE_MULTIPLIER = 100.toBigDecimal() @@ -66,6 +70,14 @@ inline fun Result.mapError(transform: (throwable: Throwable) -> Throwable } } +@OptIn(ExperimentalTime::class) +inline fun measureExecution(label: String, function: () -> R): R { + val (value, time) = measureTimedValue(function) + Log.d("Performance", "$label took $time") + + return value +} + inline fun Result.mapErrorNotInstance(transform: (throwable: Throwable) -> Throwable): Result { return mapError { throwable -> if (throwable !is E) { @@ -110,6 +122,12 @@ suspend fun Iterable.mapAsync(operation: suspend (T) -> R): List { }.awaitAll() } +suspend fun Iterable.flatMapAsync(operation: suspend (T) -> Collection): List { + return coroutineScope { + map { async { operation(it) } } + }.awaitAll().flatten() +} + suspend fun Iterable.forEachAsync(operation: suspend (T) -> R) { mapAsync(operation) } @@ -257,6 +275,10 @@ fun Result.requireException() = exceptionOrNull()!! fun Result.requireValue() = getOrThrow()!! +fun Result.requireInnerNotNull(): Result { + return mapCatching { requireNotNull(it) } +} + /** * Given a list finds a partition point in O(log2(N)) given that there is only a single partition point present. * That is, there is only a single place in the whole array where the value of [partition] changes from false to true @@ -602,3 +624,5 @@ fun Calendar.resetDay() { inline fun CoroutineScope.launchUnit(crossinline block: suspend CoroutineScope.() -> Unit) { launch { block() } } + +fun Iterable.sum(): Duration = fold(Duration.ZERO) { acc, duration -> acc + duration } diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/LazyAsync.kt b/common/src/main/java/io/novafoundation/nova/common/utils/LazyAsync.kt new file mode 100644 index 0000000000..013271b1fa --- /dev/null +++ b/common/src/main/java/io/novafoundation/nova/common/utils/LazyAsync.kt @@ -0,0 +1,43 @@ +package io.novafoundation.nova.common.utils + +fun interface LazyAsync { + + suspend fun get(): T +} + +fun lazyAsync( + mode: AsyncLazyThreadSafetyMode = AsyncLazyThreadSafetyMode.NONE, + initializer: suspend () -> T +): LazyAsync { + return when (mode) { + AsyncLazyThreadSafetyMode.NONE -> UnsafeLazyAsync(initializer) + } +} + +/** + * @see [LazyThreadSafetyMode] + */ +enum class AsyncLazyThreadSafetyMode { + + NONE, +} + +private object UNINITIALIZED_VALUE + +private class UnsafeLazyAsync( + initializer: suspend () -> T +) : LazyAsync { + + private var initializer: (suspend () -> T)? = initializer + private var value: Any? = UNINITIALIZED_VALUE + + override suspend fun get(): T { + if (value === UNINITIALIZED_VALUE) { + value = initializer!!() + initializer = null + } + + @Suppress("UNCHECKED_CAST") + return value as T + } +} diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/Percent.kt b/common/src/main/java/io/novafoundation/nova/common/utils/Percent.kt index 5ea66904ae..8f48ad99a9 100644 --- a/common/src/main/java/io/novafoundation/nova/common/utils/Percent.kt +++ b/common/src/main/java/io/novafoundation/nova/common/utils/Percent.kt @@ -9,6 +9,7 @@ import java.math.BigDecimal * Thus, 0.1 will represent equivalent to 10% */ @JvmInline +@Deprecated("Use Fraction which offers much easier and understandable abstraction over fractions") value class Perbill(val value: Double) : Comparable { companion object { @@ -26,6 +27,7 @@ value class Perbill(val value: Double) : Comparable { * E.g. Percent(10) represents value of 10% */ @JvmInline +@Deprecated("Use Fraction which offers much easier and understandable abstraction over fractions") value class Percent(val value: Double) : Comparable { companion object { @@ -36,21 +38,18 @@ value class Percent(val value: Double) : Comparable { override fun compareTo(other: Percent): Int { return value.compareTo(other.value) } -} -val Percent.fraction: BigDecimal - get() = toPerbill().value.toBigDecimal() + operator fun div(divisor: Int): Percent { + return Percent(value / divisor) + } +} inline fun BigDecimal.asPerbill(): Perbill = Perbill(this.toDouble()) -inline fun BigDecimal.asPercent(): Percent = Percent(this.toDouble()) - inline fun Double.asPerbill(): Perbill = Perbill(this) inline fun Double.asPercent(): Percent = Percent(this) -inline fun Percent.toPerbill(): Perbill = Perbill(value / 100) - inline fun Perbill.toPercent(): Percent = Percent(value * 100) inline fun Perbill?.orZero(): Perbill = this ?: Perbill(0.0) diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/ViewExt.kt b/common/src/main/java/io/novafoundation/nova/common/utils/ViewExt.kt index 76402edeac..841ffc101a 100644 --- a/common/src/main/java/io/novafoundation/nova/common/utils/ViewExt.kt +++ b/common/src/main/java/io/novafoundation/nova/common/utils/ViewExt.kt @@ -342,6 +342,10 @@ fun View.applyImeInsetts() = applyInsetter { fun View.setBackgroundColorRes(@ColorRes colorRes: Int) = setBackgroundColor(context.getColor(colorRes)) +fun View.setBackgroundTintRes(@ColorRes colorRes: Int) { + backgroundTintList = ColorStateList.valueOf(context.getColor(colorRes)) +} + fun View.useInputValue(input: Input, onValue: (I) -> Unit) { setVisible(input is Input.Enabled) isEnabled = input is Input.Enabled.Modifiable diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/ViewSwitcherUtils.kt b/common/src/main/java/io/novafoundation/nova/common/utils/ViewSwitcherUtils.kt new file mode 100644 index 0000000000..659f666ec3 --- /dev/null +++ b/common/src/main/java/io/novafoundation/nova/common/utils/ViewSwitcherUtils.kt @@ -0,0 +1,20 @@ +package io.novafoundation.nova.common.utils + +import android.widget.TextSwitcher +import android.widget.TextView + +fun TextSwitcher.setText(text: String, colorRes: Int) { + nextTextView.setTextColorRes(colorRes) + setText(text) +} + +fun TextSwitcher.setCurrentText(text: String, colorRes: Int) { + currentTextView.setTextColorRes(colorRes) + setCurrentText(text) +} + +val TextSwitcher.currentTextView: TextView + get() = currentView as TextView + +val TextSwitcher.nextTextView: TextView + get() = nextView as TextView diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/formatting/NumberFormatters.kt b/common/src/main/java/io/novafoundation/nova/common/utils/formatting/NumberFormatters.kt index 1c58aa033f..97b60e56fe 100644 --- a/common/src/main/java/io/novafoundation/nova/common/utils/formatting/NumberFormatters.kt +++ b/common/src/main/java/io/novafoundation/nova/common/utils/formatting/NumberFormatters.kt @@ -4,6 +4,7 @@ import android.content.Context import android.text.format.DateUtils import io.novafoundation.nova.common.R import io.novafoundation.nova.common.resources.ResourceManager +import io.novafoundation.nova.common.utils.Fraction import io.novafoundation.nova.common.utils.Perbill import io.novafoundation.nova.common.utils.Percent import io.novafoundation.nova.common.utils.daysFromMillis @@ -15,6 +16,7 @@ import io.novafoundation.nova.common.utils.formatting.duration.DurationFormatter import io.novafoundation.nova.common.utils.formatting.duration.HoursDurationFormatter import io.novafoundation.nova.common.utils.formatting.duration.MinutesDurationFormatter import io.novafoundation.nova.common.utils.formatting.duration.RoundMinutesDurationFormatter +import io.novafoundation.nova.common.utils.formatting.duration.SecondsDurationFormatter import io.novafoundation.nova.common.utils.formatting.duration.ZeroDurationFormatter import io.novafoundation.nova.common.utils.fractionToPercentage import io.novafoundation.nova.common.utils.isNonNegative @@ -104,22 +106,26 @@ fun BigDecimal.formatAsChange(): String { return prefix + formatAsPercentage() } -fun BigDecimal.formatAsPercentage(): String { - return defaultAbbreviationFormatter.format(this) + "%" +fun BigDecimal.formatAsPercentage(includeSymbol: Boolean = true): String { + return defaultAbbreviationFormatter.format(this) + if (includeSymbol) "%" else "" } fun Percent.format(): String { return value.toBigDecimal().formatAsPercentage() } -fun Percent.formatWithoutSymbol(): String { - return defaultAbbreviationFormatter.format(value.toBigDecimal()) +fun Fraction.formatPercents(): String { + return inPercents.toBigDecimal().formatAsPercentage() } fun Perbill.format(): String { return toPercent().format() } +fun Fraction.formatPercents(includeSymbol: Boolean = true): String { + return inPercents.toBigDecimal().formatAsPercentage(includeSymbol) +} + fun BigDecimal.formatFractionAsPercentage(): String { return fractionToPercentage().formatAsPercentage() } @@ -258,12 +264,14 @@ fun baseDurationFormatter( ), hoursDurationFormatter: BoundedDurationFormatter = HoursDurationFormatter(context), minutesDurationFormatter: BoundedDurationFormatter = MinutesDurationFormatter(context), + secondsDurationFormatter: BoundedDurationFormatter = SecondsDurationFormatter(context), zeroDurationFormatter: BoundedDurationFormatter = ZeroDurationFormatter(DayDurationFormatter(context)) ): DurationFormatter { val compoundFormatter = CompoundDurationFormatter( dayDurationFormatter, hoursDurationFormatter, minutesDurationFormatter, + secondsDurationFormatter, zeroDurationFormatter ) diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/formatting/duration/SecondsDurationFormatter.kt b/common/src/main/java/io/novafoundation/nova/common/utils/formatting/duration/SecondsDurationFormatter.kt new file mode 100644 index 0000000000..0a0a2ae346 --- /dev/null +++ b/common/src/main/java/io/novafoundation/nova/common/utils/formatting/duration/SecondsDurationFormatter.kt @@ -0,0 +1,19 @@ +package io.novafoundation.nova.common.utils.formatting.duration + +import android.content.Context +import io.novafoundation.nova.common.R +import io.novafoundation.nova.common.utils.lastSeconds +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds + +class SecondsDurationFormatter( + private val context: Context +) : BoundedDurationFormatter { + + override val threshold: Duration = 1.seconds + + override fun format(duration: Duration): String { + val seconds = duration.lastSeconds + return context.resources.getQuantityString(R.plurals.common_seconds_format, seconds, seconds) + } +} diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/graph/Graph.kt b/common/src/main/java/io/novafoundation/nova/common/utils/graph/Graph.kt index 552fe99791..622f15362f 100644 --- a/common/src/main/java/io/novafoundation/nova/common/utils/graph/Graph.kt +++ b/common/src/main/java/io/novafoundation/nova/common/utils/graph/Graph.kt @@ -1,6 +1,5 @@ package io.novafoundation.nova.common.utils.graph -import io.novafoundation.nova.common.utils.MultiMap import io.novafoundation.nova.common.utils.MultiMapList import java.util.PriorityQueue @@ -11,141 +10,110 @@ interface Edge { val to: N } +interface WeightedEdge : Edge { + + // Smaller the better + val weight: Int +} + class Graph>( - val adjacencyList: Map> + val adjacencyList: MultiMapList ) { companion object; } -fun > Graph.Companion.create(vararg multiMaps: MultiMapList): Graph { - return create(multiMaps.toList()) -} +typealias Path = List -fun > Graph.Companion.create(vararg adjacencyPairs: Pair>): Graph { - return create(adjacencyPairs.toMap()) +fun Graph.vertices(): Set { + return adjacencyList.keys } -fun > Graph.Companion.create(multiMaps: List>): Graph { - return GraphBuilder().apply { - multiMaps.forEach(::addEdges) - }.build() +fun Graph<*, *>.numberOfEdges(): Int { + return adjacencyList.values.sumOf { it.size } } -typealias ConnectedComponent = List -typealias Path = List - -/** - * Find all connected components of the graph. - * Time Complexity is O(V+E) - * Space Complexity is O(V) - */ -fun > Graph.findConnectedComponents(): List> { - val visited = mutableSetOf() - val result = mutableListOf>() +fun interface EdgeVisitFilter> { - for (vertex in adjacencyList.keys) { - if (vertex in visited) continue - - val nextConnectedComponent = connectedComponentsDfs(vertex, adjacencyList, visited) - result.add(nextConnectedComponent) - } - - return result + suspend fun shouldVisit(edge: E, pathPredecessor: E?): Boolean } -fun > Graph.findConnectedComponentFor(vertex: N): ConnectedComponent { - return connectedComponentsDfs(vertex, adjacencyList, visited = mutableSetOf()) -} +/** + * Finds all nodes reachable from [origin] + * + * Works for both directed and undirected graphs + * + * Complexity: O(V + E) + */ +suspend fun > Graph.findAllPossibleDestinations( + origin: N, + nodeVisitFilter: EdgeVisitFilter? = null +): Set { + val actualNodeListFilter = nodeVisitFilter ?: EdgeVisitFilter { _, _ -> true } -fun > Graph.findAllPossibleDirections(): MultiMap { - val connectedComponents = findConnectedComponents() - return connectedComponents.findAllPossibleDirections() -} + val reachableNodes = reachabilityDfs(origin, adjacencyList, actualNodeListFilter, predecessor = null) + reachableNodes.removeAt(reachableNodes.indexOf(origin)) -fun > Graph.findAllPossibleDirectionsToList(): MultiMapList { - val connectedComponents = findConnectedComponents() - return connectedComponents.findAllPossibleDirectionsToList() + return reachableNodes.toSet() } -fun > Graph.findAllPossibleDirectionsFor(vertex: N): Set { - val connectedComponent = findConnectedComponentFor(vertex) - return connectedComponent.findAllPossibleDirectionsFor(vertex) +fun > Graph.hasOutcomingDirections(origin: N): Boolean { + val vertices = adjacencyList[origin] ?: return false + return vertices.isNotEmpty() } -fun List>.findAllPossibleDirections(): MultiMap { - val result = mutableMapOf>() +suspend fun > Graph.findDijkstraPathsBetween( + from: N, + to: N, + limit: Int, + nodeVisitFilter: EdgeVisitFilter? = null +): List> { + val actualNodeListFilter = nodeVisitFilter ?: EdgeVisitFilter { _, _ -> true } - forEach { connectedComponent -> - val asSet = connectedComponent.toSet() + data class QueueElement(val currentPath: Path, val score: Int) : Comparable { - asSet.forEach { node -> - // in the connected component every node is connected to every other except itself - result[node] = asSet - node + override fun compareTo(other: QueueElement): Int { + return score - other.score } - } - - return result -} - -fun ConnectedComponent.findAllPossibleDirectionsFor(node: N): Set { - val nodeSet = this.toSet() - - return if (nodeSet.contains(node)) { - nodeSet - node - } else { - emptySet() - } -} -fun List>.findAllPossibleDirectionsToList(): MultiMapList { - val result = mutableMapOf>() - - forEach { connectedComponent -> - connectedComponent.forEach { node -> - // in the connected component every node is connected to every other except itself - result[node] = connectedComponent - node + fun lastNode(): N { + return if (currentPath.isNotEmpty()) currentPath.last().to else from } - } - - return result -} -fun > Graph.findDijkstraPathsBetween(from: N, to: N, limit: Int): List> { - data class QueueElement(val currentPath: Path, val nodeList: List, val score: Int) : Comparable { - override fun compareTo(other: QueueElement): Int { - return score - other.score + operator fun contains(node: N): Boolean { + return currentPath.any { it.from == node || it.to == node } } } val paths = mutableListOf>() val count = mutableMapOf() - adjacencyList.keys.forEach { count[it] = 0 } val heap = PriorityQueue() - heap.add(QueueElement(currentPath = emptyList(), nodeList = listOf(from), score = 0)) + heap.add(QueueElement(currentPath = emptyList(), score = 0)) while (heap.isNotEmpty() && paths.size < limit) { val minimumQueueElement = heap.poll()!! - val lastNode = minimumQueueElement.nodeList.last() + val lastNode = minimumQueueElement.lastNode() - val newCount = count.getValue(lastNode) + 1 + val newCount = count.getOrElse(lastNode) { 0 } + 1 count[lastNode] = newCount + val predecessor = minimumQueueElement.currentPath.lastOrNull() + if (lastNode == to) { paths.add(minimumQueueElement.currentPath) continue } if (newCount <= limit) { - adjacencyList.getValue(lastNode).forEach { edge -> - if (edge.to in minimumQueueElement.nodeList) return@forEach + val edges = adjacencyList[lastNode].orEmpty() + edges.forEach { edge -> + if (edge.to in minimumQueueElement || !actualNodeListFilter.shouldVisit(edge, predecessor)) return@forEach val newElement = QueueElement( currentPath = minimumQueueElement.currentPath + edge, - nodeList = minimumQueueElement.nodeList + edge.to, - score = minimumQueueElement.score + 1 + score = minimumQueueElement.score + edge.weight ) heap.add(newElement) @@ -156,18 +124,22 @@ fun > Graph.findDijkstraPathsBetween(from: N, to: N, limit: return paths } -private fun > connectedComponentsDfs( +private suspend fun > reachabilityDfs( node: N, adjacencyList: Map>, - visited: MutableSet, + nodeVisitFilter: EdgeVisitFilter, + predecessor: E?, + visited: MutableSet = mutableSetOf(), connectedComponentState: MutableList = mutableListOf() -): ConnectedComponent { +): MutableList { visited.add(node) connectedComponentState.add(node) - for (edge in adjacencyList.getValue(node)) { - if (edge.to !in visited) { - connectedComponentsDfs(edge.to, adjacencyList, visited, connectedComponentState) + val edges = adjacencyList[node].orEmpty() + + for (edge in edges) { + if (edge.to !in visited && nodeVisitFilter.shouldVisit(edge, predecessor)) { + reachabilityDfs(edge.to, adjacencyList, nodeVisitFilter, predecessor = edge, visited, connectedComponentState) } } diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/graph/GraphBuilder.kt b/common/src/main/java/io/novafoundation/nova/common/utils/graph/GraphBuilder.kt index 9c7d68bf8a..2511bd61a7 100644 --- a/common/src/main/java/io/novafoundation/nova/common/utils/graph/GraphBuilder.kt +++ b/common/src/main/java/io/novafoundation/nova/common/utils/graph/GraphBuilder.kt @@ -6,19 +6,26 @@ class GraphBuilder> { private val adjacencyList: MutableMap> = mutableMapOf() - fun addEdge(from: N, to: E) { - val fromEdges = adjacencyList.getOrPut(from) { mutableListOf() } - fromEdges.add(to) + fun addEdge(edge: E) { + val fromEdges = adjacencyList.getOrPut(edge.from) { mutableListOf() } + initializeVertex(edge.to) + fromEdges.add(edge) } fun addEdges(from: N, to: List) { val fromEdges = adjacencyList.getOrPut(from) { mutableListOf() } + to.onEach { initializeVertex(it.to) } + fromEdges.addAll(to) } fun build(): Graph { return Graph(adjacencyList) } + + private fun initializeVertex(v: N) { + adjacencyList.computeIfAbsent(v) { mutableListOf() } + } } fun > GraphBuilder.addEdges(map: MultiMapList) { @@ -26,3 +33,34 @@ fun > GraphBuilder.addEdges(map: MultiMapList) { addEdges(fromNode, toNodes) } } + +fun > Graph.Companion.create(vararg multiMaps: MultiMapList): Graph { + return create(multiMaps.toList()) +} + +fun > Graph.Companion.create(vararg adjacencyPairs: Pair>): Graph { + return create(adjacencyPairs.toMap()) +} + +fun > Graph.Companion.create(multiMaps: List>): Graph { + return GraphBuilder().apply { + multiMaps.forEach(::addEdges) + }.build() +} + +@JvmName("createFromEdges") +fun > Graph.Companion.create(edges: List): Graph { + return build { + edges.forEach { + addEdge(it) + } + } +} + +inline fun > Graph.Companion.build(building: GraphBuilder.() -> Unit): Graph { + return GraphBuilder().apply(building).build() +} + +inline fun > Graph.Companion.buildAdjacencyList(building: GraphBuilder.() -> Unit): MultiMapList { + return build(building).adjacencyList +} diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/graph/SimpleGraph.kt b/common/src/main/java/io/novafoundation/nova/common/utils/graph/SimpleGraph.kt index a3960da08b..30d4b67fec 100644 --- a/common/src/main/java/io/novafoundation/nova/common/utils/graph/SimpleGraph.kt +++ b/common/src/main/java/io/novafoundation/nova/common/utils/graph/SimpleGraph.kt @@ -2,7 +2,10 @@ package io.novafoundation.nova.common.utils.graph import io.novafoundation.nova.common.utils.MultiMapList -data class SimpleEdge(override val from: N, override val to: N) : Edge +data class SimpleEdge(override val from: N, override val to: N) : WeightedEdge { + + override val weight: Int = 1 +} typealias SimpleGraph = Graph> diff --git a/common/src/main/java/io/novafoundation/nova/common/validation/FieldValidator.kt b/common/src/main/java/io/novafoundation/nova/common/validation/FieldValidator.kt index fa7ace2d3c..3c5f221a07 100644 --- a/common/src/main/java/io/novafoundation/nova/common/validation/FieldValidator.kt +++ b/common/src/main/java/io/novafoundation/nova/common/validation/FieldValidator.kt @@ -8,6 +8,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch interface FieldValidator { + fun observe(inputStream: Flow): Flow companion object @@ -37,9 +38,20 @@ class CompoundFieldValidator( } sealed class FieldValidationResult { + object Ok : FieldValidationResult() - class Error(val reason: String) : FieldValidationResult() + class Error( + /** + * User-friendly error message to be displayed in UI + */ + val reason: String, + + /** + * The optional tag that other components may use to determine which validator originated this error + */ + val tag: String? = null + ) : FieldValidationResult() } fun FieldValidationResult.getReasonOrNull(): String? { @@ -49,6 +61,10 @@ fun FieldValidationResult.getReasonOrNull(): String? { } } +fun FieldValidationResult.isErrorWithTag(tag: String): Boolean { + return this is FieldValidationResult.Error && this.tag == tag +} + fun ValidatableInputField.observeErrors( flow: Flow, scope: CoroutineScope, diff --git a/common/src/main/java/io/novafoundation/nova/common/validation/InputValidationMixin.kt b/common/src/main/java/io/novafoundation/nova/common/validation/InputValidationMixin.kt deleted file mode 100644 index 8b9e8756a4..0000000000 --- a/common/src/main/java/io/novafoundation/nova/common/validation/InputValidationMixin.kt +++ /dev/null @@ -1,26 +0,0 @@ -package io.novafoundation.nova.common.validation - -import kotlinx.coroutines.flow.Flow - -interface InputValidationMixin { - val fieldValidationResult: Flow - - interface Factory { - fun create(inputStream: Flow, validator: FieldValidator) - } -} - -class RealInputValidationMixinFactory { - - fun create(inputStream: Flow, validator: FieldValidator): InputValidationMixin { - return RealInputValidationMixin(inputStream, validator) - } -} - -class RealInputValidationMixin( - inputStream: Flow, - fieldValidator: FieldValidator -) : InputValidationMixin { - - override val fieldValidationResult: Flow = fieldValidator.observe(inputStream) -} diff --git a/common/src/main/java/io/novafoundation/nova/common/view/ExpandableView.kt b/common/src/main/java/io/novafoundation/nova/common/view/ExpandableView.kt index 73cc3fc8b1..4ce6e8c4fb 100644 --- a/common/src/main/java/io/novafoundation/nova/common/view/ExpandableView.kt +++ b/common/src/main/java/io/novafoundation/nova/common/view/ExpandableView.kt @@ -19,8 +19,8 @@ import io.novafoundation.nova.common.utils.makeVisible import kotlinx.android.synthetic.main.view_banner.view.bannerImage enum class ExpandableViewState { - COLLAPSE, - EXPAND + COLLAPSED, + EXPANDED } class ExpandableView @JvmOverloads constructor( @@ -64,11 +64,16 @@ class ExpandableView @JvmOverloads constructor( fun setState(state: ExpandableViewState) { when (state) { - ExpandableViewState.COLLAPSE -> collapse() - ExpandableViewState.EXPAND -> expand() + ExpandableViewState.COLLAPSED -> collapse() + ExpandableViewState.EXPANDED -> expand() } } + fun collapseImmediate() { + expandablePart?.makeGone() + chevron?.rotation = -180f + } + private fun applyAttributes(attrs: AttributeSet?) { attrs?.let { val typedArray = context.obtainStyledAttributes(attrs, R.styleable.ExpandableView) diff --git a/common/src/main/java/io/novafoundation/nova/common/view/Extensions.kt b/common/src/main/java/io/novafoundation/nova/common/view/Extensions.kt index 00c4df8ee7..cde47a4bb8 100644 --- a/common/src/main/java/io/novafoundation/nova/common/view/Extensions.kt +++ b/common/src/main/java/io/novafoundation/nova/common/view/Extensions.kt @@ -9,6 +9,7 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleCoroutineScope import io.novafoundation.nova.common.R import io.novafoundation.nova.common.utils.bindTo +import io.novafoundation.nova.common.utils.formatting.TimerValue import io.novafoundation.nova.common.utils.formatting.duration.CompoundDurationFormatter import io.novafoundation.nova.common.utils.formatting.duration.DayAndHourDurationFormatter import io.novafoundation.nova.common.utils.formatting.duration.DayDurationFormatter @@ -16,35 +17,34 @@ import io.novafoundation.nova.common.utils.formatting.duration.DurationFormatter import io.novafoundation.nova.common.utils.formatting.duration.HoursDurationFormatter import io.novafoundation.nova.common.utils.formatting.duration.RoundMinutesDurationFormatter import io.novafoundation.nova.common.utils.formatting.duration.TimeDurationFormatter -import io.novafoundation.nova.common.utils.formatting.TimerValue import io.novafoundation.nova.common.utils.formatting.duration.ZeroDurationFormatter import io.novafoundation.nova.common.utils.makeGone import io.novafoundation.nova.common.utils.onDestroy import kotlinx.coroutines.flow.MutableStateFlow import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.milliseconds -import kotlin.time.ExperimentalTime private val TIMER_TAG = R.string.common_time_left fun TextView.startTimer( value: TimerValue, + durationFormatter: DurationFormatter? = null, @StringRes customMessageFormat: Int? = null, lifecycle: Lifecycle? = null, onTick: ((view: TextView, millisUntilFinished: Long) -> Unit)? = null, onFinish: ((view: TextView) -> Unit)? = null -) = startTimer(value.millis, value.millisCalculatedAt, lifecycle, customMessageFormat, onTick, onFinish) +) = startTimer(value.millis, value.millisCalculatedAt, durationFormatter, lifecycle, customMessageFormat, onTick, onFinish) -@OptIn(ExperimentalTime::class) fun TextView.startTimer( millis: Long, millisCalculatedAt: Long? = null, + durationFormatter: DurationFormatter? = null, lifecycle: Lifecycle? = null, @StringRes customMessageFormat: Int? = null, onTick: ((view: TextView, millisUntilFinished: Long) -> Unit)? = null, onFinish: ((view: TextView) -> Unit)? = null ) { - val durationFormatter = getTimerDurationFormatter(context) + val actualDurationFormatter = durationFormatter ?: getTimerDurationFormatter(context) val timePassedSinceCalculation = if (millisCalculatedAt != null) System.currentTimeMillis() - millisCalculatedAt else 0L @@ -56,7 +56,7 @@ fun TextView.startTimer( val newTimer = object : CountDownTimer(millis - timePassedSinceCalculation, 1000) { override fun onTick(millisUntilFinished: Long) { - setNewValue(durationFormatter, millisUntilFinished, customMessageFormat) + setNewValue(actualDurationFormatter, millisUntilFinished, customMessageFormat) onTick?.invoke(this@startTimer, millisUntilFinished) } @@ -65,7 +65,7 @@ fun TextView.startTimer( if (onFinish != null) { onFinish(this@startTimer) } else { - this@startTimer.text = durationFormatter.format(0L.milliseconds) + this@startTimer.text = actualDurationFormatter.format(0L.milliseconds) } cancel() @@ -78,7 +78,7 @@ fun TextView.startTimer( newTimer.cancel() } - setNewValue(durationFormatter, millis - timePassedSinceCalculation, customMessageFormat) + setNewValue(actualDurationFormatter, millis - timePassedSinceCalculation, customMessageFormat) newTimer.start() setTag(TIMER_TAG, newTimer) diff --git a/common/src/main/java/io/novafoundation/nova/common/view/GenericTableCellView.kt b/common/src/main/java/io/novafoundation/nova/common/view/GenericTableCellView.kt new file mode 100644 index 0000000000..89a4509231 --- /dev/null +++ b/common/src/main/java/io/novafoundation/nova/common/view/GenericTableCellView.kt @@ -0,0 +1,119 @@ +package io.novafoundation.nova.common.view + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.view.children +import io.novafoundation.nova.common.R +import io.novafoundation.nova.common.utils.dp +import io.novafoundation.nova.common.utils.getResourceIdOrNull +import io.novafoundation.nova.common.utils.setDrawableEnd +import io.novafoundation.nova.common.utils.setVisible +import io.novafoundation.nova.common.utils.useAttributes +import kotlinx.android.synthetic.main.view_generic_table_cell.view.genericTableCellTitle +import kotlinx.android.synthetic.main.view_generic_table_cell.view.genericTableCellValueProgress + +open class GenericTableCellView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyle: Int = 0, + defStyleRes: Int = 0, +) : ConstraintLayout(context, attrs, defStyle, defStyleRes), TableItem { + + protected lateinit var valueView: V + + companion object { + + private val SELF_IDS = listOf(R.id.genericTableCellTitle, R.id.genericTableCellValueProgress) + } + + init { + minHeight = 44.dp + + View.inflate(context, R.layout.view_generic_table_cell, this) + + setBackgroundResource(R.drawable.bg_primary_list_item) + + attrs?.let(::applyAttributes) + } + + override fun onFinishInflate() { + super.onFinishInflate() + + findAndPositionValueView() + } + + @Suppress("UNCHECKED_CAST") + private fun findAndPositionValueView() { + children.forEach { + if (it.id !in SELF_IDS) { + valueView = it as V + valueView.layoutParams = createValueViewLayoutParams() + requestLayout() + } + } + } + + override fun addView(child: View) { + if (child.id in SELF_IDS) { + super.addView(child) + } else { + addValueView(child) + } + } + + fun showProgress(showProgress: Boolean) { + genericTableCellValueProgress.setVisible(showProgress) + valueView.setVisible(!showProgress) + } + + fun setTitle(title: String?) { + genericTableCellTitle.text = title + } + + fun setTitle(@StringRes titleRes: Int) { + genericTableCellTitle.setText(titleRes) + } + + fun setTitleIconEnd(@DrawableRes icon: Int?) { + genericTableCellTitle.setDrawableEnd(icon, widthInDp = 16, paddingInDp = 4) + } + + @JvmName("setValueContentView") + protected fun setValueView(view: V) { + addValueView(view) + } + + @Suppress("UNCHECKED_CAST") + private fun addValueView(child: View) { + valueView = child as V + super.addView(child, createValueViewLayoutParams()) + } + + private fun createValueViewLayoutParams() = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT).apply { + endToEnd = LayoutParams.PARENT_ID + startToEnd = R.id.genericTableCellTitle + marginStart = 16.dp + topToTop = LayoutParams.PARENT_ID + bottomToBottom = LayoutParams.PARENT_ID + horizontalBias = 1.0f + constrainedWidth = true + } + + private fun applyAttributes(attrs: AttributeSet) = context.useAttributes(attrs, R.styleable.GenericTableCellView) { typedArray -> + val titleText = typedArray.getString(R.styleable.GenericTableCellView_title) + setTitle(titleText) + + val titleIconEnd = typedArray.getResourceIdOrNull(R.styleable.GenericTableCellView_titleIcon) + titleIconEnd?.let(::setTitleIconEnd) + } + + override fun disableOwnDividers() {} + + override fun shouldDrawDivider(): Boolean { + return true + } +} diff --git a/common/src/main/java/io/novafoundation/nova/common/view/HasDivider.kt b/common/src/main/java/io/novafoundation/nova/common/view/HasDivider.kt index 2363dec53e..4262c4bca2 100644 --- a/common/src/main/java/io/novafoundation/nova/common/view/HasDivider.kt +++ b/common/src/main/java/io/novafoundation/nova/common/view/HasDivider.kt @@ -1,5 +1,6 @@ package io.novafoundation.nova.common.view interface HasDivider { + fun setDividerVisible(visible: Boolean) } diff --git a/common/src/main/java/io/novafoundation/nova/common/view/TableCellView.kt b/common/src/main/java/io/novafoundation/nova/common/view/TableCellView.kt index b87c1a8ff6..49183ef610 100644 --- a/common/src/main/java/io/novafoundation/nova/common/view/TableCellView.kt +++ b/common/src/main/java/io/novafoundation/nova/common/view/TableCellView.kt @@ -45,18 +45,21 @@ import kotlinx.android.synthetic.main.view_table_cell.view.tableCellValuePrimary import kotlinx.android.synthetic.main.view_table_cell.view.tableCellValueProgress import kotlinx.android.synthetic.main.view_table_cell.view.tableCellValueSecondary +private const val DRAW_DIVIDER_DEFAULT = true + open class TableCellView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyle: Int = 0, defStyleRes: Int = 0, -) : ConstraintLayout(context, attrs, defStyle, defStyleRes), HasDivider { +) : ConstraintLayout(context, attrs, defStyle, defStyleRes), TableItem { enum class FieldStyle { PRIMARY, SECONDARY, LINK, POSITIVE } companion object { + fun createTableCellView(context: Context): TableCellView { return TableCellView(context).apply { layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT) @@ -85,6 +88,8 @@ open class TableCellView @JvmOverloads constructor( private val contentGroup: Group get() = tableCellContent + private var shouldDrawDivider: Boolean = DRAW_DIVIDER_DEFAULT + val imageLoader: ImageLoader by lazy(LazyThreadSafetyMode.NONE) { FeatureUtils.getCommonApi(context).imageLoader() } @@ -98,6 +103,14 @@ open class TableCellView @JvmOverloads constructor( attrs?.let { applyAttributes(it) } } + override fun disableOwnDividers() { + setOwnDividerVisible(false) + } + + override fun shouldDrawDivider(): Boolean { + return shouldDrawDivider + } + fun setTitle(titleRes: Int) { tableCellTitle.setText(titleRes) } @@ -151,12 +164,20 @@ open class TableCellView @JvmOverloads constructor( } fun showProgress() { + makeVisible() + contentGroup.makeGone() valueProgress.makeVisible() } - override fun setDividerVisible(visible: Boolean) { - tableCellValueDivider.setVisible(visible) + @Deprecated( + """ + TableCellView's own divider is deprecated and will be removed in the future. + To show dividers between multiple TableCellViews put them into TableView + """ + ) + fun setOwnDividerVisible(visible: Boolean) { + tableCellValueDivider.setVisible(visible && shouldDrawDivider) } fun setPrimaryValueEndIcon(@DrawableRes icon: Int?, @ColorRes tint: Int? = null) { @@ -223,6 +244,10 @@ open class TableCellView @JvmOverloads constructor( constraintSet.applyTo(this) } + fun setShouldDrawDivider(shouldDrawDivider: Boolean) { + this.shouldDrawDivider = shouldDrawDivider + } + private fun applyAttributes(attrs: AttributeSet) = context.useAttributes(attrs, R.styleable.TableCellView) { typedArray -> val titleText = typedArray.getString(R.styleable.TableCellView_title) setTitle(titleText) @@ -231,7 +256,7 @@ open class TableCellView @JvmOverloads constructor( primaryValueText?.let { showValue(it) } val dividerVisible = typedArray.getBoolean(R.styleable.TableCellView_dividerVisible, true) - setDividerVisible(dividerVisible) + setOwnDividerVisible(dividerVisible) val primaryValueEndIcon = typedArray.getResourceIdOrNull(R.styleable.TableCellView_primaryValueEndIcon) primaryValueEndIcon?.let { @@ -275,6 +300,9 @@ open class TableCellView @JvmOverloads constructor( val titleEllipsisable = typedArray.getBoolean(R.styleable.TableCellView_titleEllipsisable, false) setTitleEllipsisable(titleEllipsisable) + + val shouldDrawDivider = typedArray.getBoolean(R.styleable.TableCellView_shouldDrawDivider, DRAW_DIVIDER_DEFAULT) + setShouldDrawDivider(shouldDrawDivider) } } @@ -297,14 +325,20 @@ fun TableCellView.setExtraInfoAvailable(available: Boolean) { } } -fun TableCellView.showLoadingState(state: ExtendedLoadingState, showData: (T) -> Unit) { +fun TableCellView.showLoadingState(state: ExtendedLoadingState, showData: (T) -> Unit) { when (state) { is ExtendedLoadingState.Error -> showValue(context.getString(R.string.common_error_general_title)) - is ExtendedLoadingState.Loaded -> showData(state.data) + + is ExtendedLoadingState.Loaded -> if (state.data != null) { + showData(state.data) + } else { + makeGone() + } + ExtendedLoadingState.Loading -> showProgress() } } -fun TableCellView.showLoadingValue(state: ExtendedLoadingState) { +fun TableCellView.showLoadingValue(state: ExtendedLoadingState) { showLoadingState(state, ::showValue) } diff --git a/common/src/main/java/io/novafoundation/nova/common/view/TableItem.kt b/common/src/main/java/io/novafoundation/nova/common/view/TableItem.kt new file mode 100644 index 0000000000..b436e3551c --- /dev/null +++ b/common/src/main/java/io/novafoundation/nova/common/view/TableItem.kt @@ -0,0 +1,9 @@ +package io.novafoundation.nova.common.view + +interface TableItem { + + fun disableOwnDividers() + + // TODO this is only needed until TableView has its own divider + fun shouldDrawDivider(): Boolean +} diff --git a/common/src/main/java/io/novafoundation/nova/common/view/TableView.kt b/common/src/main/java/io/novafoundation/nova/common/view/TableView.kt index 540a2d0770..2fd5b41ef2 100644 --- a/common/src/main/java/io/novafoundation/nova/common/view/TableView.kt +++ b/common/src/main/java/io/novafoundation/nova/common/view/TableView.kt @@ -78,13 +78,16 @@ open class TableView @JvmOverloads constructor( setupTableChildrenAppearance() dividerPath.reset() - children.toList() - .filter { it != titleView && it.isVisible } - .withoutLast() - .forEach { - dividerPath.moveTo(childHorizontalPadding, it.bottom.toFloat()) - dividerPath.lineTo(measuredWidth - childHorizontalPadding, it.bottom.toFloat()) + children.forEachIndexed { idx, child -> + val isVisible = child.isVisible + val allowsToDrawDividers = child is TableItem && child.shouldDrawDivider() + val hasNext = idx < childCount - 1 + + if (isVisible && allowsToDrawDividers && hasNext) { + dividerPath.moveTo(childHorizontalPadding, child.bottom.toFloat()) + dividerPath.lineTo(measuredWidth - childHorizontalPadding, child.bottom.toFloat()) } + } } fun setTitle(title: String?) { @@ -113,7 +116,7 @@ open class TableView @JvmOverloads constructor( } private fun setupTableChildrenAppearance() { - val tableChildren = children.filterNot { it == titleView } + val tableChildren = children.filter { it != titleView } .filter { it.isVisible } .toList() @@ -125,8 +128,8 @@ open class TableView @JvmOverloads constructor( } tableChildren.forEach { - if (it is HasDivider) { - it.setDividerVisible(false) + if (it is TableItem) { + it.disableOwnDividers() } it.updatePadding(start = 16.dp, end = 16.dp) diff --git a/common/src/main/res/anim/fade_scale_in.xml b/common/src/main/res/anim/fade_scale_in.xml new file mode 100644 index 0000000000..f604a7f640 --- /dev/null +++ b/common/src/main/res/anim/fade_scale_in.xml @@ -0,0 +1,18 @@ + + + + + + + \ No newline at end of file diff --git a/common/src/main/res/anim/fade_scale_out.xml b/common/src/main/res/anim/fade_scale_out.xml new file mode 100644 index 0000000000..e8ecbe7f28 --- /dev/null +++ b/common/src/main/res/anim/fade_scale_out.xml @@ -0,0 +1,18 @@ + + + + + + + \ No newline at end of file diff --git a/common/src/main/res/anim/fade_slide_bottom_in.xml b/common/src/main/res/anim/fade_slide_bottom_in.xml new file mode 100644 index 0000000000..1caa6ef063 --- /dev/null +++ b/common/src/main/res/anim/fade_slide_bottom_in.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/common/src/main/res/anim/fade_slide_bottom_out.xml b/common/src/main/res/anim/fade_slide_bottom_out.xml new file mode 100644 index 0000000000..6a5aee2202 --- /dev/null +++ b/common/src/main/res/anim/fade_slide_bottom_out.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/common/src/main/res/drawable-hdpi/ic_container_timer_animated.png b/common/src/main/res/drawable-hdpi/ic_container_timer_animated.png new file mode 100644 index 0000000000..ae2eafe226 Binary files /dev/null and b/common/src/main/res/drawable-hdpi/ic_container_timer_animated.png differ diff --git a/common/src/main/res/drawable-ldpi/ic_container_timer_animated.png b/common/src/main/res/drawable-ldpi/ic_container_timer_animated.png new file mode 100644 index 0000000000..caeb391a9b Binary files /dev/null and b/common/src/main/res/drawable-ldpi/ic_container_timer_animated.png differ diff --git a/common/src/main/res/drawable-mdpi/ic_container_timer_animated.png b/common/src/main/res/drawable-mdpi/ic_container_timer_animated.png new file mode 100644 index 0000000000..6f2267f0dd Binary files /dev/null and b/common/src/main/res/drawable-mdpi/ic_container_timer_animated.png differ diff --git a/common/src/main/res/drawable-xhdpi/ic_container_timer_animated.png b/common/src/main/res/drawable-xhdpi/ic_container_timer_animated.png new file mode 100644 index 0000000000..4b064e52fd Binary files /dev/null and b/common/src/main/res/drawable-xhdpi/ic_container_timer_animated.png differ diff --git a/common/src/main/res/drawable-xxhdpi/ic_container_timer_animated.png b/common/src/main/res/drawable-xxhdpi/ic_container_timer_animated.png new file mode 100644 index 0000000000..c62629474b Binary files /dev/null and b/common/src/main/res/drawable-xxhdpi/ic_container_timer_animated.png differ diff --git a/common/src/main/res/drawable-xxxhdpi/ic_container_timer_animated.png b/common/src/main/res/drawable-xxxhdpi/ic_container_timer_animated.png new file mode 100644 index 0000000000..ac7e967665 Binary files /dev/null and b/common/src/main/res/drawable-xxxhdpi/ic_container_timer_animated.png differ diff --git a/common/src/main/res/drawable/bg_container_with_border_circle.xml b/common/src/main/res/drawable/bg_container_with_border_circle.xml new file mode 100644 index 0000000000..2267d8a06f --- /dev/null +++ b/common/src/main/res/drawable/bg_container_with_border_circle.xml @@ -0,0 +1,5 @@ + + + + diff --git a/common/src/main/res/drawable/ic_execution_result_error.xml b/common/src/main/res/drawable/ic_execution_result_error.xml new file mode 100644 index 0000000000..3b03e0de67 --- /dev/null +++ b/common/src/main/res/drawable/ic_execution_result_error.xml @@ -0,0 +1,10 @@ + + + diff --git a/common/src/main/res/drawable/ic_execution_result_success.xml b/common/src/main/res/drawable/ic_execution_result_success.xml new file mode 100644 index 0000000000..747867f8f9 --- /dev/null +++ b/common/src/main/res/drawable/ic_execution_result_success.xml @@ -0,0 +1,10 @@ + + + diff --git a/common/src/main/res/layout/view_generic_table_cell.xml b/common/src/main/res/layout/view_generic_table_cell.xml new file mode 100644 index 0000000000..4b9554260b --- /dev/null +++ b/common/src/main/res/layout/view_generic_table_cell.xml @@ -0,0 +1,38 @@ + + + + + + + + \ No newline at end of file diff --git a/common/src/main/res/values-es/strings.xml b/common/src/main/res/values-es/strings.xml index 92e21215cb..4fe3f8fd36 100644 --- a/common/src/main/res/values-es/strings.xml +++ b/common/src/main/res/values-es/strings.xml @@ -356,6 +356,7 @@ Detalles Deshabilitado Desconectar + ¡No cierres la aplicación! Hecho No mostrar esto de nuevo Editar @@ -375,8 +376,10 @@ La operación eliminará la cuenta Expirado Explorar + Fallido La tarifa de red estimada %s es mucho mayor que la tarifa de red predeterminada (%s). Esto podría deberse a la congestión temporal de la red. Puedes actualizar para esperar una tarifa de red más baja. La tarifa de red es demasiado alta + Tarifa: %s Ordenar por: Filtros Descubrir más @@ -431,6 +434,7 @@ No disponible Lo sentimos, no tiene fondos suficientes para pagar la tarifa de red. Saldo insuficiente + No tienes suficiente saldo para pagar la tarifa de red de %s. El saldo actual es %s No ahora Apagado Aceptar @@ -470,6 +474,11 @@ Buscar Los resultados de la búsqueda se mostrarán aquí Resultados de la búsqueda: %d + seg + + %d segundo + %d segundos + Ruta de derivación secreta Configuraciones Compartir @@ -493,6 +502,7 @@ Abrir configuración Tu saldo es demasiado pequeño Total + Tarifa total ID de transacción Transacción enviada Intentar de nuevo @@ -1519,6 +1529,17 @@ Token para pagar la tasa de red La tasa de red se agrega al monto ingresado Este par no es compatible + Fallido en la operación #%s (%s) + Intercambio %s a %s en %s + Transferencia de %s de %s a %s + + %s operación + %s operaciones + + %s de %s operaciones + Intercambiando %s a %s en %s + Transfiriendo %s a %s + Tiempo de ejecución Deberías mantener al menos %s después de pagar %s de tasa de red ya que estás sosteniendo tokens insuficientes Debes mantener al menos %s para recibir el token %s Puedes intercambiar hasta %1$s ya que necesitas pagar %2$s por la tasa de red. @@ -1538,6 +1559,7 @@ Transferir %s desde otra red Recibir %s con QR o tu dirección Obtener %s usando + Durante la ejecución del intercambio, la cantidad intermedia recibida es %s, lo cual es menos que el saldo mínimo de %s. Intenta especificar una cantidad de intercambio mayor. El deslizamiento debe estar especificado entre %s y %s Deslizamiento inválido Selecciona un token para pagar @@ -1556,6 +1578,10 @@ Tasa antigua: %1$s ≈ %2$s.\nNueva tasa: %1$s ≈ %3$s La tasa de intercambio fue actualizada Repetir la operación + Ruta + La manera en que tu token pasará a través de diferentes redes para obtener el token deseado. + Intercambio + Transferencia Configuraciones de intercambio Deslizamiento El deslizamiento de intercambio es una ocurrencia común en el comercio descentralizado donde el precio final de una transacción de intercambio podría diferir ligeramente del precio esperado, debido a las cambiantes condiciones del mercado. diff --git a/common/src/main/res/values-fr-rFR/strings.xml b/common/src/main/res/values-fr-rFR/strings.xml index 1a2d369044..08f40424f4 100644 --- a/common/src/main/res/values-fr-rFR/strings.xml +++ b/common/src/main/res/values-fr-rFR/strings.xml @@ -356,6 +356,7 @@ Détails Désactivé Déconnecter + Ne fermez pas l\'application! Terminé Ne plus afficher ceci Modifier @@ -375,8 +376,10 @@ L\'opération supprimera le compte Expiré Explorer + Échoué Le coût estimé du réseau %s est beaucoup plus élevé que le coût par défaut (%s). Cela pourrait être dû à une congestion temporaire du réseau. Vous pouvez actualiser pour attendre un coût réseau plus bas. Le coût du réseau est trop élevé + Frais: %s Trier par : Filtres En savoir plus @@ -431,6 +434,7 @@ Indisponible Désolé, vous n’avez pas assez de fonds pour payer les frais de réseau. Solde insuffisant + Vous n\'avez pas assez de solde pour payer les frais de réseau de %s. Le solde actuel est de %s Pas maintenant Désactivé D\'ACCORD @@ -470,6 +474,11 @@ Rechercher Le résultat de la recherche sera affiché ici Résultats de la recherche : %d + sec + + %d seconde + %d secondes + Chemin de dérivation du secret Paramètres Partager @@ -493,6 +502,7 @@ Ouvrir les paramètres Votre solde est trop faible Total + Frais totaux ID de la transaction Envoi de la transaction Réessayez @@ -1519,6 +1529,17 @@ Token pour le paiement des frais de réseau Les frais de réseau sont ajoutés en plus du montant saisi Cette paire n\'est pas prise en charge + Échec de l\'opération #%s (%s) + Échange de %s à %s sur %s + Transfert de %s de %s à %s + + %s opération + %s opérations + + %s sur %s opérations + Échange de %s à %s sur %s + Transfert de %s à %s + Temps d\'exécution Vous devez conserver au moins %s après avoir payé %s de frais de réseau car vous détenez des tokens insuffisants Vous devez conserver au moins %s pour recevoir des tokens %s Vous pouvez échanger jusqu\'à %1$s car vous devez payer %2$s pour les frais de réseau. @@ -1538,6 +1559,7 @@ Transférer %s depuis un autre réseau Recevoir %s avec un QR ou votre adresse Obtenir %s en utilisant + Pendant l\'exécution de l\'échange, le montant intermédiaire reçu est de %s, ce qui est inférieur au solde minimum de %s. Essayez de spécifier un montant d\'échange plus important. La glissement doit être spécifiée entre %s et %s Glissement non valide Sélectionnez un token pour payer @@ -1556,6 +1578,10 @@ Ancien taux : %1$st∻%2$s.\nNouveau taux : %1$st∻%3$s Le taux d\'échange a été mis à jour Répétez l\'opération + Itinéraire + La voie que prendra votre jeton à travers différents réseaux pour obtenir le jeton souhaité. + Échange + Transfert Paramètres d\'échange Slippage Le slippage est un phénomène courant dans le trading décentralisé où le prix final d\'une transaction d\'échange peut légèrement différer du prix attendu, en raison des conditions changeantes du marché. diff --git a/common/src/main/res/values-in/strings.xml b/common/src/main/res/values-in/strings.xml index d6b692fefb..debfdd123c 100644 --- a/common/src/main/res/values-in/strings.xml +++ b/common/src/main/res/values-in/strings.xml @@ -356,6 +356,7 @@ Detail Nonaktif Putus + Jangan tutup aplikasinya! Selesai Jangan tampilkan ini lagi Edit @@ -375,8 +376,10 @@ Operasi akan menghapus akun Kedaluwarsa Jelajahi + Gagal Biaya jaringan yang diperkirakan %s jauh lebih tinggi dari biaya jaringan default (%s). Hal ini mungkin disebabkan oleh kemacetan jaringan sementara. Anda dapat menyegarkan untuk menunggu biaya jaringan yang lebih rendah. Biaya jaringan terlalu tinggi + Biaya: %s Urutkan berdasarkan: Penyaring Temukan lebih lanjut @@ -427,6 +430,7 @@ Tidak tersedia Maaf, Anda tidak memiliki cukup dana untuk membayar biaya jaringan. Saldo tidak mencukupi + Saldo Anda tidak cukup untuk membayar biaya jaringan sebesar %s. Saldo saat ini adalah %s Tidak sekarang Mati OK @@ -466,6 +470,10 @@ Cari Hasil pencarian akan ditampilkan di sini Hasil pencarian: %d + detik + + %d detik + Jalur derivasi rahasia Pengaturan Bagikan @@ -489,6 +497,7 @@ Buka Pengaturan Saldo Anda terlalu kecil Total + Biaya total ID Transaksi Transaksi dikirim Coba lagi @@ -1509,6 +1518,16 @@ Token untuk membayar biaya jaringan Biaya jaringan ditambahkan di atas jumlah yang dimasukkan Pasangan ini tidak didukung + Gagal dalam operasi #%s (%s) + %s ke %s swap pada %s + Transfer %s dari %s ke %s + + %s operasi + + %s dari %s operasi + Menukarkan %s ke %s pada %s + Mentransfer %s ke %s + Waktu eksekusi Anda harus menyisihkan setidaknya %s setelah membayar biaya jaringan %s karena Anda memegang token yang tidak mencukupi Anda harus menyisihkan setidaknya %s untuk menerima token %s Anda dapat menukar hingga %1$s karena Anda perlu membayar %2$s untuk biaya jaringan. @@ -1528,6 +1547,7 @@ Transfer %s dari jaringan lain Terima %s dengan QR atau alamat Anda Dapatkan %s menggunakan + Selama eksekusi swap, jumlah terima sementara adalah %s yang lebih rendah dari saldo minimum %s. Coba tentukan jumlah swap yang lebih besar. Prosentase pergeseran harus ditentukan antara %s dan %s Pergeseran tidak valid Pilih token untuk membayar @@ -1546,6 +1566,10 @@ Kurs lama: %1$s ≈ %2$s.\nKurs baru: %1$s ≈ %3$s Kurs pertukaran telah diperbarui Ulangi operasi + Rute + Jalur yang akan diambil token Anda melalui berbagai jaringan untuk mendapatkan token yang diinginkan. + Tukar + Transfer Pengaturan swap Slippage Slippage adalah kejadian umum dalam perdagangan terdesentralisasi di mana harga akhir dari transaksi swap mungkin sedikit berbeda dari harga yang diharapkan, karena kondisi pasar yang berubah. diff --git a/common/src/main/res/values-it/strings.xml b/common/src/main/res/values-it/strings.xml index 7ccfbe525a..2af74e3cb8 100644 --- a/common/src/main/res/values-it/strings.xml +++ b/common/src/main/res/values-it/strings.xml @@ -356,6 +356,7 @@ Dettagli Disabilitato Disconnetti + Non chiudere l\'app! Fatto Non mostrare più Modifica @@ -375,8 +376,10 @@ L\'operazione rimuoverà l\'account Scaduto Esplora + Fallito La commissione di rete stimata %s è molto più alta rispetto alla commissione predefinita (%s). Ciò potrebbe essere dovuto a congestione temporanea della rete. Puoi aggiornare per attendere una commissione di rete più bassa. Commissione di rete troppo alta + Tariffa: %s Ordina per: Filtri Scopri di più @@ -431,6 +434,7 @@ Non disponibile Spiacenti, non hai abbastanza fondi per pagare la tassa di rete. Saldo insufficiente + Non hai abbastanza saldo per pagare la commissione di rete di %s. Il saldo attuale è %s Non adesso Spento OK @@ -470,6 +474,11 @@ Cerca Qui verranno visualizzati i risultati della ricerca Risultati della ricerca: %d + sec + + %d secondo + %d secondi + Percorso di derivazione segreto Impostazioni Condividi @@ -493,6 +502,7 @@ Apri Impostazioni Il tuo saldo è troppo piccolo Totale + Commissione totale ID della transazione Transazione inviata Prova di nuovo @@ -1519,6 +1529,17 @@ Token per pagare la commissione di rete La commissione di rete viene aggiunta alla somma inserita Questa coppia non è supportata + Fallito nell\'operazione #%s (%s) + Scambio da %s a %s su %s + Trasferimento di %s da %s a %s + + %s operazione + %s operazioni + + %s di %s operazioni + Scambiando %s con %s su %s + Trasferimento di %s a %s + Tempo di esecuzione Dovresti mantenere almeno %s dopo aver pagato una commissione di rete di %s, in quanto detieni token non sufficienti Devi mantenere almeno %s per ricevere il token %s Puoi scambiare fino a %1$s poiché devi pagare %2$s per la commissione di rete. @@ -1538,6 +1559,7 @@ Trasferisci %s da un\'altra rete Ricevi %s con QR o il tuo indirizzo Ottieni %s usando + Durante l\'esecuzione dello swap, l\'importo intermediario ricevuto è %s, che è inferiore al saldo minimo di %s. Prova a specificare un importo di scambio maggiore. Lo slippage deve essere specificato tra %s e %s Slippage non valido Seleziona un token per pagare @@ -1556,6 +1578,10 @@ Il vecchio tasso: %1$s ≈ %2$s.\nNuovo tasso: %1$s ≈ %3$s Il tasso di cambio è stato aggiornato Ripeti l\'operazione + Percorso + Il percorso che il tuo token seguirà attraverso diverse reti per ottenere il token desiderato. + Scambio + Trasferimento Impostazioni di scambio Scivolamento Il cambio di scivolamento è un evento comune nel trading decentralizzato in cui il prezzo finale di una transazione di scambio potrebbe differire leggermente dal prezzo atteso, a causa delle mutevoli condizioni di mercato. diff --git a/common/src/main/res/values-ja/strings.xml b/common/src/main/res/values-ja/strings.xml index 2b31a4ecfd..70b079d07b 100644 --- a/common/src/main/res/values-ja/strings.xml +++ b/common/src/main/res/values-ja/strings.xml @@ -356,6 +356,7 @@ 詳細 無効 切断する + アプリを閉じないでください! 完了 これを再表示しない 編集 @@ -375,8 +376,10 @@ 操作によりアカウントが削除されます 期限切れ 探索する + 失敗しました 推定ネットワーク手数料%sがデフォルトのネットワーク手数料(%s)よりも大幅に高くなっています。これは一時的なネットワーク渋滞が原因かもしれません。ネットワーク手数料をリフレッシュして、より低い手数料を待つことができます。 ネットワーク手数料が高すぎます + 手数料: %s ソート順: フィルター 詳しく知る @@ -427,6 +430,7 @@ 利用できません 申し訳ありませんが、ネットワーク手数料を支払うための十分な資金がありません。 残高不足 + ネットワーク手数料%sを支払うには残高が不足しています。現在の残高は%sです 今はしません オフ OK @@ -466,6 +470,10 @@ 検索 検索結果がここに表示されます 検索結果:%d + + + %d秒 + シークレット導出パス 設定 共有 @@ -489,6 +497,7 @@ 設定を開く 残高が少なすぎます 合計 + 合計手数料 取引ID 取引が送信されました もう一度試してください @@ -1509,6 +1518,16 @@ ネットワーク手数料を支払うトークン 入力された金額の上にネットワーク手数料が追加されます このペアはサポートされていません + 操作 #%s (%s) が失敗しました + %sから%sへのスワップ on %s + %sから%sへの%s転送 + + %s操作 + + %s/%sの操作 + %sから%sへのスワップ on %s + %sを%sに転送 + 実行時間 十分なトークンを保持していないため、%sのネットワーク手数料を支払った後に少なくとも%sを保持する必要があります %sトークンを受け取るために少なくとも%sを保持する必要があります ネットワーク手数料として%2$sを支払うため、最大%1$sをスワップできます。 @@ -1528,6 +1547,7 @@ 他のネットワークから %s を転送 QRコードまたはあなたのアドレスで %s を受け取る %s を使って取得 + スワップ実行中に中間受取額が%sで、これは最小残高%sを下回っています。より大きなスワップ額を指定してみてください。 スリッページは %s から %s の間で指定する必要があります 無効なスリッページ 支払うトークンを選択 @@ -1546,6 +1566,10 @@ 旧レート: %1$s ≈ %2$s。\n新レート: %1$s ≈ %3$s 交換レートが更新されました 操作を繰り返す + ルート + 異なるネットワークを通じて希望するトークンを得るためにトークンが通る経路です。 + スワップ + 転送 スワップ設定 スリッページ スワップのスリッページは、分散型取引でよく見られる現象で、取引の最終価格が市場状況の変化により予想価格から少し異なる場合があります。 diff --git a/common/src/main/res/values-ko/strings.xml b/common/src/main/res/values-ko/strings.xml index 17b029a572..f53c55c062 100644 --- a/common/src/main/res/values-ko/strings.xml +++ b/common/src/main/res/values-ko/strings.xml @@ -356,6 +356,7 @@ 세부 사항 비활성화됨 연결 끊기 + 앱을 닫지 마세요! 완료 다시 표시하지 않기 편집 @@ -375,8 +376,10 @@ 작업이 계정을 제거합니다 만료됨 탐색 + 실패 예상 네트워크 수수료 %s가 기본 네트워크 수수료(%s)보다 훨씬 높습니다. 이는 일시적인 네트워크 혼잡으로 인해 발생할 수 있습니다. 더 낮은 네트워크 수수료를 기다리기 위해 새로 고칠 수 있습니다. 네트워크 수수료가 너무 높습니다 + 수수료: %s 정렬 기준: 필터 더 알아보기 @@ -427,6 +430,7 @@ 이용 불가 죄송합니다. 네트워크 수수료를 지불할 충분한 자금이 없습니다. 잔액 부족 + 네트워크 수수료 %s를 지불할 잔액이 부족합니다. 현재 잔액은 %s입니다 지금은 안 함 OK @@ -466,6 +470,10 @@ 검색 검색 결과가 여기에 표시됩니다 검색 결과: %d + + + %d초 + 비밀 경로 도출 설정 공유 @@ -489,6 +497,7 @@ 설정 열기 잔액이 너무 적습니다 총합 + 총 수수료 거래 ID 트랜잭션이 제출되었습니다 다시 시도하십시오 @@ -1509,6 +1518,16 @@ 네트워크 수수료를 지불할 토큰 입력한 금액 외에 네트워크 수수료가 추가됩니다 이 페어는 지원되지 않습니다 + 작업 #%s (%s) 실패 + %s에서 %s로의 스왑 %s + %s에서 %s로의 전송 + + %s 작업 + + %s of %s 작업 + %s에서 %s로 %s에서 스왑 중 + %s에서 %s로 전송 중 + 실행 시간 충분하지 않은 토큰을 보유하고 있으므로 %s의 네트워크 수수료를 지불한 후 최소 %s을(를) 유지해야 합니다 %s 토큰을 받기 위해 최소 %s을(를) 유지해야 합니다 네트워크 수수료로 %2$s를 지불해야 하므로 최대 %1$s까지 교환할 수 있습니다. @@ -1528,6 +1547,7 @@ 다른 네트워크에서 %s 전송 QR 또는 주소로 %s 받기 %s 받기 + 스왑 실행 중 중간 수령 금액이 %s로 최소 잔액 %s보다 적습니다. 더 큰 스왑 금액을 지정해보세요. 슬리피지는 %s에서 %s 사이로 지정해야 합니다 잘못된 슬리피지 지불할 토큰 선택 @@ -1546,6 +1566,10 @@ 이전 환율: %1$s ≈ %2$s.\n새 환율: %1$s ≈ %3$s 교환 비율이 업데이트되었습니다 작업 반복 + 경로 + 당신의 토큰이 원하는 토큰을 얻기 위해 다른 네트워크를 통해 이동하는 방식입니다. + 스왑 + 전송 교환 설정 슬리페이지 슬리페이지는 분산형 거래에서 흔히 발생하는 현상으로, 교환 거래의 최종 가격이 시장 상황 변화로 인해 예상 가격과 다를 수 있습니다. diff --git a/common/src/main/res/values-pl/strings.xml b/common/src/main/res/values-pl/strings.xml index 0721f8cab6..5ae357680d 100644 --- a/common/src/main/res/values-pl/strings.xml +++ b/common/src/main/res/values-pl/strings.xml @@ -356,6 +356,7 @@ Szczegóły Wyłączony Odłącz + Nie zamykaj aplikacji! Gotowe Nie pokazuj tego ponownie Edytuj @@ -375,8 +376,10 @@ Operacja usunie konto Wygasło Odkryj + Niepowodzenie Szacowana opłata sieciowa %s jest znacznie wyższa niż domyślna opłata sieciowa (%s). Może to być spowodowane przejściowym przeciążeniem sieci. Możesz odświeżyć stronę, aby poczekać na niższą opłatę. Opłata sieciowa jest zbyt wysoka + Opłata: %s Sortuj według: Filtry Dowiedz się więcej @@ -439,6 +442,7 @@ Niedostępne Przepraszamy, nie masz wystarczających środków na opłatę sieciową. Niewystarczające saldo + Nie masz wystarczającego salda, aby zapłacić opłatę sieciową w wysokości %s. Obecne saldo to %s Nie teraz Wył OK @@ -478,6 +482,13 @@ Szukaj Wyniki wyszukiwania będą wyświetlane tutaj Wyniki wyszukiwania: %d + sek + + %d sekunda + + + %d sekund + Ścieżka pochodzenia sekretu Ustawienia Udostępnij @@ -501,6 +512,7 @@ Otwórz Ustawienia Twój balans jest zbyt mały Razem + Łączna opłata ID transakcji Transakcja wysłana Spróbuj ponownie @@ -1539,6 +1551,19 @@ Token do opłacenia opłaty sieciowej Opłata sieciowa jest dodawana do wprowadzonej kwoty Ta para nie jest obsługiwana + Niepowodzenie w operacji #%s (%s) + Zamiana %s na %s na %s + Transfer %s z %s do %s + + %s operacja + + + %s operacji + + %s z %s operacji + Zamiana %s na %s na %s + Transfer %s do %s + Czas wykonania Powinieneś zachować co najmniej %s po opłaceniu %s opłaty sieciowej, ponieważ posiadasz niewystarczającą ilość tokenów Musisz zachować co najmniej %s, aby otrzymać token %s Możesz zamienić do %1$s, ponieważ musisz zapłacić %2$s jako opłatę sieciową. @@ -1558,6 +1583,7 @@ Przenieś %s z innej sieci Otrzymaj %s za pomocą QR lub swojego adresu Otrzymaj %s za pomocą + Podczas wykonywania swapu pośrednia ilość odbierana wynosi %s co jest mniej niż minimalne saldo %s. Spróbuj określić większą kwotę swapu. Poślizg musi być określony między %s a %s Niedozwolony poślizg Wybierz token do zapłaty @@ -1576,6 +1602,10 @@ Stary kurs: %1$s ≈ %2$s.\nNowy kurs: %1$s ≈ %3$s Kurs wymiany został zaktualizowany Powtórz operację + Trasa + Droga, którą twoje tokeny przejdą przez różne sieci, aby uzyskać żądany token. + Swap + Transfer Ustawienia wymiany Poślizg Poślizg wymiany jest powszechnym zjawiskiem w zdecentralizowanym handlu, gdzie ostateczna cena transakcji wymiany może nieznacznie różnić się od oczekiwanej ceny, z powodu zmieniających się warunków rynkowych. diff --git a/common/src/main/res/values-pt/strings.xml b/common/src/main/res/values-pt/strings.xml index a6aab8484e..709d602585 100644 --- a/common/src/main/res/values-pt/strings.xml +++ b/common/src/main/res/values-pt/strings.xml @@ -356,6 +356,7 @@ Detalhes Desativado Desconectar + Não feche o aplicativo! Feito Não mostrar isso novamente Editar @@ -375,8 +376,10 @@ A operação removerá a conta Expirado Explorar + Falhou A taxa de rede estimada %s é muito maior que a taxa de rede padrão (%s). Isso pode ser devido à congestão temporária da rede. Você pode atualizar para esperar por uma taxa de rede menor. Taxa de rede está muito alta + Taxa: %s Ordenar por: Filtros Descubra mais @@ -431,6 +434,7 @@ Não disponível Desculpe, você não tem fundos suficientes para pagar a taxa de rede. Saldo insuficiente + Você não tem saldo suficiente para pagar a taxa de rede de %s. Saldo atual é %s Agora não Desligado OK @@ -470,6 +474,11 @@ Pesquisar Os resultados da pesquisa serão exibidos aqui Resultados da pesquisa: %d + seg + + %d segundo + %d segundos + Sequência de derivação secreta Configurações Compartilhar @@ -493,6 +502,7 @@ Abrir Configurações Seu saldo é muito pequeno Total + Taxa total ID da transação Transação enviada Tente novamente @@ -1519,6 +1529,17 @@ Token para pagar a taxa de rede A taxa de rede é adicionada em cima do valor inserido Este par não é suportado + Falha na operação #%s (%s) + Troca de %s para %s em %s + Transferência de %s de %s para %s + + %s operação + %s operações + + %s de %s operações + Trocando %s para %s em %s + Transferindo %s para %s + Tempo de execução Você deve manter pelo menos %s após pagar a taxa de rede %s, pois você está com tokens insuficientes Você deve manter pelo menos %s para receber o token %s Você pode trocar até %1$s, pois você precisa pagar %2$s de taxa de rede. @@ -1538,6 +1559,7 @@ Transfira %s de outra rede Receba %s com QR ou seu endereço Obtenha %s usando + Durante a execução da troca, o valor intermediário recebido é %s, que é menor que o saldo mínimo de %s. Tente especificar um valor de troca maior. O deslizamento deve ser especificado entre %s e %s Deslizamento inválido Selecione um token para pagar @@ -1556,6 +1578,10 @@ Taxa antiga: %1$s ≈ %2$s.\nNova taxa: %1$s ≈ %3$s A taxa de troca foi atualizada Repetir a operação + Rota + O caminho que seu token percorrerá através de diferentes redes para obter o token desejado. + Troca + Transferência Configurações de Troca Deslizamento Deslizamento em trocas é um acontecimento comum em negociações descentralizadas onde o preço final de uma transação de troca pode diferir ligeiramente do preço esperado, devido às condições de mercado em mudança. diff --git a/common/src/main/res/values-ru/strings.xml b/common/src/main/res/values-ru/strings.xml index 33dc180a91..25da573bb4 100644 --- a/common/src/main/res/values-ru/strings.xml +++ b/common/src/main/res/values-ru/strings.xml @@ -356,6 +356,7 @@ Подробности Отключен Отключить + Не закрывайте приложение! Готово Больше не показывать это Редактировать @@ -375,8 +376,10 @@ Операция удалит аккаунт Истекла Исследуй + Не удалось Предполагаемая комиссия %s намного выше, чем комиссия по умолчанию (%s). Это может быть связано с временной перегрузкой сети. Вы можете обновить комиссию, чтобы дождаться более низкой суммы. Комиссия сети слишком высокая + Комиссия: %s Сортировать по: Фильтры Узнать больше @@ -439,6 +442,7 @@ Не доступно К сожалению, у вас недостаточно средств для оплаты сетевого сбора. Недостаточный баланс + У вас недостаточно средств для оплаты сетевой комиссии в размере %s. Текущий баланс: %s Не сейчас Выкл OK @@ -478,6 +482,13 @@ Поиск Результаты поиска будут отображаться здесь Результаты поиска: %d + сек + + %d секунда + + + %d секунд + Последовательность для вывода Настройки Поделиться @@ -501,6 +512,7 @@ Открыть Настройки Ваш баланс слишком мал Всего + Общая комиссия ID транзакции Транзакция отправлена Попробовать снова @@ -1539,6 +1551,19 @@ Токен для оплаты комиссии сети Комиссия сети добавится к введенной сумме Эта пара не поддерживается + Не удалось выполнить операцию #%s (%s) + Обмен %s на %s в %s + Перевод %s из %s в %s + + %s операция + + + %s операций + + %s из %s операций + Обмен %s на %s в %s + Перевод %s в %s + Время выполнения Вы должны сохранить как минимум %s после оплаты сетевой комиссии %s, поскольку вы храните не самодостаточные токены Вы должны сохранить как минимум %s, чтобы получить %s токены Вы можете обменять до %1$s так как вам нужно заплатить комиссию сети в размере %2$s. @@ -1558,6 +1583,7 @@ Перевести %s из другой сети Получить %s по QR или вашему адресу Получить %s с помощью + Во время выполнения обмена промежуточная полученная сумма равна %s, что меньше минимального баланса %s. Попробуйте указать большую сумму обмена. Проскальзывание должно быть указано в диапазоне от %s до %s Недопустимое проскальзывание Выберите токен для оплаты @@ -1576,6 +1602,10 @@ Старый курс: %1$s ≈ %2$s.\nНовый курс: %1$s ≈ %3$s Обменный курс изменился Повторить операцию + Маршрут + Путь, по которому ваш токен пройдет через разные сети, чтобы получить желаемый токен. + Обмен + Перевод Настройки обмена Проскальзывание Проскальзывание - распространенное явление в децентрализованной торговле, когда конечная цена сделки по обмену может незначительно отличаться от ожидаемой в связи с изменением рыночных условий. diff --git a/common/src/main/res/values-tr/strings.xml b/common/src/main/res/values-tr/strings.xml index 1775904096..6129687c0f 100644 --- a/common/src/main/res/values-tr/strings.xml +++ b/common/src/main/res/values-tr/strings.xml @@ -356,6 +356,7 @@ Detaylar Devre dışı Bağlantıyı kes + Uygulamayı kapatmayın! Tamam Bunu bir daha gösterme Düzenle @@ -375,8 +376,10 @@ Hesap kaldırılacak Süresi doldu Keşfet + Başarısız Tahmini ağ ücreti %s, varsayılan ağ ücretinden (%s) çok daha yüksek. Bu, geçici bir ağ yoğunluğundan kaynaklanıyor olabilir. Daha düşük bir ağ ücreti için yenileyebilirsiniz. Ağ ücreti çok yüksek + Ücret: %s Şuna göre sırala: Filtreler Daha fazla bilgi edinin @@ -431,6 +434,7 @@ Mevcut değil Üzgünüm, ağ ücretini ödeyecek yeterli bakiyeniz yok. Yetersiz bakiye + Ağ ücreti olan %s ödemesi için yeterli bakiyeniz yok. Mevcut bakiye %s Şimdi değil Kapalı Tamam @@ -470,6 +474,11 @@ Arama Arama sonuçları burada gösterilecek Arama sonuçları: %d + sn + + %d saniye + %d saniye + Gizli türetme yolu Ayarlar Paylaş @@ -493,6 +502,7 @@ Ayarları Aç Bakiyeniz çok küçük Toplam + Toplam Ücret İşlem Kimliği İşlem Gönderildi Tekrar Deneyin @@ -1519,6 +1529,17 @@ Ağ ücreti ödemek için token Ağ ücreti girilen miktarın üstüne eklenir Bu çift desteklenmiyor + #%s (%s) işlemi başarısız oldu + %s\'den %s\'e %s üzerinde swap + %s\'den %s\'e %s transferi + + %s işlem + %s işlem + + %s/%s işlem + %s\'den %s\'e %s üzerinde takas + %s\'den %s\'e transfer + Gerçekleşme süresi Ödeme yaptıktan sonra ağ ücreti olarak %s tutarında en azından %s tutmanız gerekmektedir çünkü yeterli olmayan tokenlara sahipsiniz %s token almak için en az %s tutmalısınız Ağ ücreti için %2$s ödemeniz gerektiği için en fazla %1$s tutarında takas yapabilirsiniz. @@ -1538,6 +1559,7 @@ Başka bir ağdan %s transfer et QR veya adresinizle %s alın %s kullanarak alın + Takas gerçekleştirilirken ara alınan miktar %s olarak minimum bakiye olan %s\'in altında. Daha büyük bir takas miktarı belirtmeye çalışın. Kayma %s ile %s arasında belirtilmeli Geçersiz kayma Ödeme yapmak için bir token seçin @@ -1556,6 +1578,10 @@ Eski oran: %1$s ≈ %2$s.\nYeni oran: %1$s ≈ %3$s Takas oranı güncellendi İşlemi tekrarla + Rota + Token\'ınızın farklı ağlardan geçerek istenen token\'a ulaşmak için alacağı yol. + Takas + Transfer Takas ayarları Kayma Takas kayması, değişken piyasa koşulları nedeniyle, bir takas işleminin nihai fiyatının beklenen fiyattan küçük bir miktar farklılık gösterebileceği merkeziyetsiz ticarette yaygın bir durumdur. diff --git a/common/src/main/res/values-vi/strings.xml b/common/src/main/res/values-vi/strings.xml index 7379a43bef..f98dab1519 100644 --- a/common/src/main/res/values-vi/strings.xml +++ b/common/src/main/res/values-vi/strings.xml @@ -356,6 +356,7 @@ Chi tiết Đã tắt Ngắt kết nối + Không đóng ứng dụng! Hoàn thành Không hiển thị lại điều này Chỉnh sửa @@ -375,8 +376,10 @@ Thao tác sẽ xóa tài khoản Hết hạn Khám phá + Thất bại Phí mạng ước tính %s cao hơn nhiều so với phí mạng mặc định (%s). Điều này có thể do tắc nghẽn mạng tạm thời. Bạn có thể làm mới để đợi phí mạng thấp hơn. Phí mạng quá cao + Phí: %s Sắp xếp theo: Bộ lọc Tìm hiểu thêm @@ -427,6 +430,7 @@ Không khả dụng Xin lỗi, bạn không có đủ tiền để trả phí mạng. Số dư không đủ + Bạn không có đủ số dư để trả phí mạng %s. Số dư hiện tại là %s Không phải bây giờ Tắt OK @@ -466,6 +470,10 @@ Tìm kiếm Kết quả tìm kiếm sẽ hiển thị ở đây Kết quả tìm kiếm: %d + giây + + %d giây + Đường dẫn dẫn xuất bí mật Cài đặt Chia sẻ @@ -489,6 +497,7 @@ Mở Cài đặt Số dư của bạn quá nhỏ Tổng cộng + Tổng phí ID Giao dịch Giao dịch đã được gửi Thử lại @@ -1509,6 +1518,16 @@ Token để trả phí mạng Phí mạng được thêm vào trên số tiền đã nhập Cặp này không được hỗ trợ + Thất bại trên hoạt động #%s (%s) + %s sang %s hoán đổi trên %s + Chuyển %s từ %s sang %s + + %s hoạt động + + %s của %s hoạt động + Đang hoán đổi %s sang %s trên %s + Đang chuyển %s sang %s + Thời gian thực hiện Bạn cần giữ ít nhất %s sau khi trả phí mạng %s vì bạn giữ các token không đủ Bạn cần giữ ít nhất %s để nhận được token %s Bạn có thể hoán đổi tối đa %1$s vì bạn cần trả %2$s cho phí mạng. @@ -1528,6 +1547,7 @@ Chuyển %s từ mạng khác Nhận %s bằng QR hoặc địa chỉ của bạn Nhận %s bằng cách sử dụng + Trong khi hoán đổi thực hiện, số tiền nhận trung gian là %s, thấp hơn số dư tối thiểu %s. Hãy thử chỉ định số tiền hoán đổi lớn hơn. Độ trượt giá phải được chỉ định trong khoảng %s đến %s Độ trượt giá không hợp lệ Chọn một token để thanh toán @@ -1546,6 +1566,10 @@ Tỷ giá cũ: %1$s %2$s.\nTỷ giá mới: %1$s %3$s Tỷ giá hoán đổi đã được cập nhật Lặp lại thao tác + Lộ trình + Cách mà token của bạn sẽ đi qua các mạng lưới khác nhau để có được token mong muốn. + Hoán đổi + Chuyển Cài đặt hoán đổi Trượt giá Trượt giá hoán đổi là một hiện tượng phổ biến trong giao dịch phi tập trung nơi giá cuối cùng của một giao dịch hoán đổi có thể hơi khác so với giá dự kiến do điều kiện thị trường thay đổi. diff --git a/common/src/main/res/values-zh-rCN/strings.xml b/common/src/main/res/values-zh-rCN/strings.xml index 8e720b42a4..4460344fb4 100644 --- a/common/src/main/res/values-zh-rCN/strings.xml +++ b/common/src/main/res/values-zh-rCN/strings.xml @@ -356,6 +356,7 @@ 详细信息 已禁用 断开 + 不要关闭应用程序! 完成 不再显示 编辑 @@ -375,8 +376,10 @@ 操作将移除账户 已过期 探索 + 失败 预估的网络费用%s远高于默认的网络费用(%s)。这可能是由于临时网络拥堵。您可以刷新以等待更低的网络费用。 网络费用过高 + 费用: %s 排序依据: 过滤器 了解更多 @@ -427,6 +430,7 @@ 不可用 抱歉,您没有足够的资金支付网络费用。 余额不足 + 你的余额不足以支付 %s 的网络费用。当前余额为 %s 暂不 关闭 确定 @@ -466,6 +470,10 @@ 搜索 搜索结果将在这里显示 搜索结果:%d + + + %d 秒 + 密钥派生路径 设置 分享 @@ -489,6 +497,7 @@ 打开设置 您的余额太小 总计 + 总费用 交易ID 交易已提交 再试一次 @@ -1509,6 +1518,16 @@ 用于支付网络费用的代币 网络费用将添加到输入的金额之上 不支持此对 + 操作 #%s (%s) 失败 + %s 到 %s 的交换在 %s 上 + %s 从 %s 转到 %s + + %s 操作 + + %s 共 %s 操作 + 正在进行 %s 到 %s 的交换在 %s 上 + 正在传输 %s 到 %s + 执行时间 您在支付%s网络费后至少应保留%s,因为您持有的代币不足 您必须至少保留%s,以接收%s代币 您最多可以交换%1$s,因为您需要支付%2$s的网络费 @@ -1528,6 +1547,7 @@ 从另一个网络转移%s 通过QR或您的地址接收%s 使用%s获取 + 在交换执行期间,中间接收金额为 %s,少于最低余额 %s。请尝试指定更大的交换金额。 滑点必须指定在%s到%s之间 无效滑点 选择一个代币来支付 @@ -1546,6 +1566,10 @@ 旧汇率:%1$s ≈ %2$s。\n新汇率:%1$s ≈ %3$s 兑换率已更新 重复操作 + 路线 + 你的代币通过不同网络到达所需代币的路径。 + 交换 + 转移 交换设置 滑点 在去中心化交易中,交换滑点是一种常见现象,由于市场条件的变化,交易的最终价格可能会与预期价格略有不同。 diff --git a/common/src/main/res/values/attrs.xml b/common/src/main/res/values/attrs.xml index c8898dffd1..61e744c6cf 100644 --- a/common/src/main/res/values/attrs.xml +++ b/common/src/main/res/values/attrs.xml @@ -106,6 +106,12 @@ + + + + + + diff --git a/common/src/main/res/values/colors.xml b/common/src/main/res/values/colors.xml index 409cd3792e..450bf10f81 100644 --- a/common/src/main/res/values/colors.xml +++ b/common/src/main/res/values/colors.xml @@ -4,6 +4,9 @@ #eeeeee #FFFFFF #101014 + + #373A49 + #3DFFFFFF #05081C diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml index b157bffc3d..6cfe0b4586 100644 --- a/common/src/main/res/values/strings.xml +++ b/common/src/main/res/values/strings.xml @@ -1,5 +1,44 @@ + During swap execution intermediate receive amount is %s which is less than minimum balance of %s. Try specifying larger swap amount. + + You don\'t hae enough balance to pay network fee of %s. Current balance is %s + + sec + + Do not close the app! + + Failed + + %s of %s operations + + + %s operation + %s operations + + + Swapping %s to %s on %s + Transferring %s to %s + + Failed on operation #%s (%s) + %s to %s swap on %s + %s transfer from %s to %s + + + %d second + %d seconds + + + Execution time + + Total fee + + Fee: %s + + Transfer + Swap + The way that your token will take through different networks to get the desired token. + Route Select network diff --git a/common/src/main/res/values/styles.xml b/common/src/main/res/values/styles.xml index 63d87457d5..612b87a879 100644 --- a/common/src/main/res/values/styles.xml +++ b/common/src/main/res/values/styles.xml @@ -64,6 +64,11 @@ 11sp + +