From ac620ed4336002ed0edf96b0a76a0d759efa5404 Mon Sep 17 00:00:00 2001 From: kukabi Date: Wed, 14 Feb 2024 16:39:51 +0300 Subject: [PATCH] Validator details progress: add/remove validator + state feedback. --- subvt/build.gradle.kts | 5 +- .../io/helikon/subvt/data/extension/Gson.kt | 6 + .../data/repository/AppServiceRepository.kt | 35 - .../java/io/helikon/subvt/di/AppModule.kt | 6 - .../subvt/ui/component/ActionButton.kt | 3 +- .../io/helikon/subvt/ui/component/Snackbar.kt | 25 +- .../introduction/IntroductionViewModel.kt | 14 +- .../selection/NetworkSelectionScreen.kt | 2 +- .../selection/NetworkSelectionViewModel.kt | 14 +- .../details/ValidatorDetailsScreen.kt | 771 ++++++++++-------- .../details/ValidatorDetailsViewModel.kt | 99 ++- .../validator/list/ValidatorListScreen.kt | 2 +- subvt/src/main/res/drawable-hdpi/check.png | Bin 0 -> 3036 bytes subvt/src/main/res/drawable-mdpi/check.png | Bin 0 -> 1590 bytes subvt/src/main/res/drawable-xhdpi/check.png | Bin 0 -> 4641 bytes subvt/src/main/res/drawable-xxhdpi/check.png | Bin 0 -> 9160 bytes subvt/src/main/res/drawable-xxxhdpi/check.png | Bin 0 -> 14825 bytes subvt/src/main/res/values/dimens.xml | 3 +- subvt/src/main/res/values/duration.xml | 1 + subvt/src/main/res/values/strings.xml | 1 + 20 files changed, 601 insertions(+), 386 deletions(-) create mode 100644 subvt/src/main/java/io/helikon/subvt/data/extension/Gson.kt delete mode 100644 subvt/src/main/java/io/helikon/subvt/data/repository/AppServiceRepository.kt create mode 100644 subvt/src/main/res/drawable-hdpi/check.png create mode 100644 subvt/src/main/res/drawable-mdpi/check.png create mode 100644 subvt/src/main/res/drawable-xhdpi/check.png create mode 100644 subvt/src/main/res/drawable-xxhdpi/check.png create mode 100644 subvt/src/main/res/drawable-xxxhdpi/check.png diff --git a/subvt/build.gradle.kts b/subvt/build.gradle.kts index 07e9b46..04ffdfa 100644 --- a/subvt/build.gradle.kts +++ b/subvt/build.gradle.kts @@ -79,12 +79,15 @@ dependencies { implementation("androidx.compose.ui:ui-graphics") implementation("androidx.compose.ui:ui-tooling-preview") implementation("androidx.compose.material3:material3:1.2.0") - implementation("androidx.navigation:navigation-compose:2.7.6") + implementation("androidx.navigation:navigation-compose:2.7.7") implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0") implementation("androidx.lifecycle:lifecycle-runtime-compose:2.7.0") implementation("androidx.datastore:datastore-preferences:1.0.0") implementation("androidx.core:core-splashscreen:1.0.1") + // de/serialization + implementation("com.google.code.gson:gson:2.10.1") + // firebase implementation(platform("com.google.firebase:firebase-bom:32.7.1")) implementation("com.google.firebase:firebase-messaging-ktx") diff --git a/subvt/src/main/java/io/helikon/subvt/data/extension/Gson.kt b/subvt/src/main/java/io/helikon/subvt/data/extension/Gson.kt new file mode 100644 index 0000000..dac1368 --- /dev/null +++ b/subvt/src/main/java/io/helikon/subvt/data/extension/Gson.kt @@ -0,0 +1,6 @@ +package io.helikon.subvt.data.extension + +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken + +internal inline fun Gson.fromJson(json: String) = fromJson(json, object : TypeToken() {}.type) diff --git a/subvt/src/main/java/io/helikon/subvt/data/repository/AppServiceRepository.kt b/subvt/src/main/java/io/helikon/subvt/data/repository/AppServiceRepository.kt deleted file mode 100644 index a40fd45..0000000 --- a/subvt/src/main/java/io/helikon/subvt/data/repository/AppServiceRepository.kt +++ /dev/null @@ -1,35 +0,0 @@ -package io.helikon.subvt.data.repository - -import android.content.Context -import io.helikon.subvt.BuildConfig -import io.helikon.subvt.data.model.app.Network -import io.helikon.subvt.data.model.app.User -import io.helikon.subvt.data.service.AppService -import kotlinx.collections.immutable.ImmutableList -import javax.inject.Inject - -class AppServiceRepository - @Inject - constructor(private val context: Context) { - private lateinit var appService: AppService - - private fun init() { - if (!::appService.isInitialized) { - appService = - AppService( - context, - "https://${BuildConfig.API_HOST}:${BuildConfig.APP_SERVICE_PORT}/", - ) - } - } - - suspend fun createUser(): Result { - init() - return appService.createUser() - } - - suspend fun getNetworks(): Result?> { - init() - return appService.getNetworks() - } - } diff --git a/subvt/src/main/java/io/helikon/subvt/di/AppModule.kt b/subvt/src/main/java/io/helikon/subvt/di/AppModule.kt index 3b51137..6c12e24 100644 --- a/subvt/src/main/java/io/helikon/subvt/di/AppModule.kt +++ b/subvt/src/main/java/io/helikon/subvt/di/AppModule.kt @@ -7,7 +7,6 @@ import dagger.hilt.InstallIn import dagger.hilt.android.components.ViewModelComponent import dagger.hilt.android.qualifiers.ApplicationContext import io.helikon.subvt.data.db.SubVTDatabase -import io.helikon.subvt.data.repository.AppServiceRepository import io.helikon.subvt.data.repository.NetworkRepository import io.helikon.subvt.data.repository.UserPreferencesRepository @@ -19,11 +18,6 @@ class AppModule { @ApplicationContext context: Context, ): UserPreferencesRepository = UserPreferencesRepository(context) - @Provides - fun provideAppServiceRepository( - @ApplicationContext context: Context, - ): AppServiceRepository = AppServiceRepository(context) - @Provides fun provideNetworkRepository( @ApplicationContext context: Context, diff --git a/subvt/src/main/java/io/helikon/subvt/ui/component/ActionButton.kt b/subvt/src/main/java/io/helikon/subvt/ui/component/ActionButton.kt index a2bb233..47c982c 100644 --- a/subvt/src/main/java/io/helikon/subvt/ui/component/ActionButton.kt +++ b/subvt/src/main/java/io/helikon/subvt/ui/component/ActionButton.kt @@ -3,6 +3,7 @@ package io.helikon.subvt.ui.component import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Button @@ -68,7 +69,7 @@ fun ActionButton( } if (isLoading) { CircularProgressIndicator( - modifier = Modifier.width(dimensionResource(id = R.dimen.action_button_progress_width)), + modifier = Modifier.size(dimensionResource(id = R.dimen.action_button_progress_width)), color = Color.actionButtonText(), trackColor = Color.transparent(), ) diff --git a/subvt/src/main/java/io/helikon/subvt/ui/component/Snackbar.kt b/subvt/src/main/java/io/helikon/subvt/ui/component/Snackbar.kt index 8bce9b8..c4f027e 100644 --- a/subvt/src/main/java/io/helikon/subvt/ui/component/Snackbar.kt +++ b/subvt/src/main/java/io/helikon/subvt/ui/component/Snackbar.kt @@ -9,8 +9,11 @@ import androidx.compose.foundation.background import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentSize @@ -30,6 +33,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.compose.ui.zIndex import io.helikon.subvt.R import io.helikon.subvt.ui.modifier.NoRippleInteractionSource import io.helikon.subvt.ui.modifier.noRippleClickable @@ -51,16 +55,21 @@ fun SnackbarScaffold( modifier.fillMaxSize(), ) { content() - Snackbar( - text = snackbarText, + Column( modifier = Modifier - .align(Alignment.BottomCenter), - isDark = isDark, - isVisible = snackbarIsVisible, - onSnackbarClick, - onSnackbarRetry, - ) + .align(Alignment.BottomCenter) + .zIndex(15.0f), + ) { + Snackbar( + text = snackbarText, + isDark = isDark, + isVisible = snackbarIsVisible, + onClick = onSnackbarClick, + onRetry = onSnackbarRetry, + ) + Spacer(modifier = Modifier.navigationBarsPadding()) + } } } diff --git a/subvt/src/main/java/io/helikon/subvt/ui/screen/introduction/IntroductionViewModel.kt b/subvt/src/main/java/io/helikon/subvt/ui/screen/introduction/IntroductionViewModel.kt index 533782c..e8cc115 100644 --- a/subvt/src/main/java/io/helikon/subvt/ui/screen/introduction/IntroductionViewModel.kt +++ b/subvt/src/main/java/io/helikon/subvt/ui/screen/introduction/IntroductionViewModel.kt @@ -7,6 +7,8 @@ import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import io.helikon.subvt.BuildConfig import io.helikon.subvt.data.DataRequestState import io.helikon.subvt.data.DataRequestState.Error import io.helikon.subvt.data.DataRequestState.Idle @@ -14,8 +16,8 @@ import io.helikon.subvt.data.DataRequestState.Loading import io.helikon.subvt.data.DataRequestState.Success import io.helikon.subvt.data.SubVTData import io.helikon.subvt.data.model.app.User -import io.helikon.subvt.data.repository.AppServiceRepository import io.helikon.subvt.data.repository.UserPreferencesRepository +import io.helikon.subvt.data.service.AppService import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import javax.inject.Inject @@ -24,11 +26,16 @@ import javax.inject.Inject class IntroductionViewModel @Inject constructor( + @ApplicationContext context: Context, private val userPreferencesRepository: UserPreferencesRepository, - private val appServiceRepository: AppServiceRepository, ) : ViewModel() { var createUserState by mutableStateOf>(Idle) private set + private val appService = + AppService( + context, + "https://${BuildConfig.API_HOST}:${BuildConfig.APP_SERVICE_PORT}/", + ) fun createUser(context: Context) { createUserState = Loading @@ -36,12 +43,11 @@ class IntroductionViewModel val response = try { SubVTData.reset(context) - appServiceRepository.createUser() + appService.createUser() } catch (error: Throwable) { createUserState = Error(error) return@launch } - appServiceRepository.createUser() if (response.isSuccess) { response.getOrNull().let { createUserState = diff --git a/subvt/src/main/java/io/helikon/subvt/ui/screen/network/selection/NetworkSelectionScreen.kt b/subvt/src/main/java/io/helikon/subvt/ui/screen/network/selection/NetworkSelectionScreen.kt index 455846f..0e5e1fa 100644 --- a/subvt/src/main/java/io/helikon/subvt/ui/screen/network/selection/NetworkSelectionScreen.kt +++ b/subvt/src/main/java/io/helikon/subvt/ui/screen/network/selection/NetworkSelectionScreen.kt @@ -117,7 +117,7 @@ private fun NetworkSelectionScreenContent( CircularProgressIndicator( modifier = Modifier - .width(dimensionResource(id = R.dimen.common_progress_width)) + .size(dimensionResource(id = R.dimen.common_progress_size)) .align(Alignment.Center), color = Color.lightGray(), trackColor = Color.transparent(), diff --git a/subvt/src/main/java/io/helikon/subvt/ui/screen/network/selection/NetworkSelectionViewModel.kt b/subvt/src/main/java/io/helikon/subvt/ui/screen/network/selection/NetworkSelectionViewModel.kt index 330fcb4..14b6e46 100644 --- a/subvt/src/main/java/io/helikon/subvt/ui/screen/network/selection/NetworkSelectionViewModel.kt +++ b/subvt/src/main/java/io/helikon/subvt/ui/screen/network/selection/NetworkSelectionViewModel.kt @@ -1,16 +1,19 @@ package io.helikon.subvt.ui.screen.network.selection +import android.content.Context import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import io.helikon.subvt.BuildConfig import io.helikon.subvt.data.DataRequestState import io.helikon.subvt.data.model.Network -import io.helikon.subvt.data.repository.AppServiceRepository import io.helikon.subvt.data.repository.NetworkRepository import io.helikon.subvt.data.repository.UserPreferencesRepository +import io.helikon.subvt.data.service.AppService import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking @@ -20,13 +23,18 @@ import javax.inject.Inject class NetworkSelectionViewModel @Inject constructor( + @ApplicationContext context: Context, private val userPreferencesRepository: UserPreferencesRepository, - private val appServiceRepository: AppServiceRepository, private val networkRepository: NetworkRepository, ) : ViewModel() { var getNetworksState by mutableStateOf>>(DataRequestState.Idle) private set val networks = networkRepository.allNetworks + private val appService = + AppService( + context, + "https://${BuildConfig.API_HOST}:${BuildConfig.APP_SERVICE_PORT}/", + ) fun getNetworks() { val networks = @@ -44,7 +52,7 @@ class NetworkSelectionViewModel viewModelScope.launch(Dispatchers.IO) { val response = try { - appServiceRepository.getNetworks() + appService.getNetworks() } catch (error: Throwable) { getNetworksState = DataRequestState.Error(error) return@launch diff --git a/subvt/src/main/java/io/helikon/subvt/ui/screen/validator/details/ValidatorDetailsScreen.kt b/subvt/src/main/java/io/helikon/subvt/ui/screen/validator/details/ValidatorDetailsScreen.kt index e48a908..cca41e2 100644 --- a/subvt/src/main/java/io/helikon/subvt/ui/screen/validator/details/ValidatorDetailsScreen.kt +++ b/subvt/src/main/java/io/helikon/subvt/ui/screen/validator/details/ValidatorDetailsScreen.kt @@ -1,5 +1,10 @@ package io.helikon.subvt.ui.screen.validator.details +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.isSystemInDarkTheme @@ -19,14 +24,21 @@ import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clipToBounds +import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Brush import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLifecycleOwner @@ -42,6 +54,7 @@ import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.helikon.subvt.R +import io.helikon.subvt.data.DataRequestState import io.helikon.subvt.data.extension.display import io.helikon.subvt.data.extension.inactiveNominationTotal import io.helikon.subvt.data.extension.inactiveNominations @@ -51,6 +64,7 @@ import io.helikon.subvt.data.model.app.ValidatorDetails import io.helikon.subvt.data.preview.PreviewData import io.helikon.subvt.data.service.RPCSubscriptionServiceStatus import io.helikon.subvt.ui.component.AnimatedBackground +import io.helikon.subvt.ui.component.SnackbarScaffold import io.helikon.subvt.ui.modifier.noRippleClickable import io.helikon.subvt.ui.modifier.scrollHeader import io.helikon.subvt.ui.screen.network.status.view.NetworkSelectorButton @@ -69,9 +83,13 @@ import io.helikon.subvt.ui.util.ThemePreviews import io.helikon.subvt.util.formatDecimal data class ValidatorDetailsScreenState( - val serviceStatus: RPCSubscriptionServiceStatus, + val validatorDetailsServiceStatus: RPCSubscriptionServiceStatus, + val appServiceStatus: DataRequestState, val network: Network?, val validator: ValidatorDetails?, + val isMyValidator: Boolean, + val feedbackIsValidatorAdded: Boolean?, + val snackbarIsVisible: Boolean, ) @Composable @@ -82,7 +100,11 @@ fun ValidatorDetailsScreen( lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, onBack: () -> Unit, ) { - val serviceStatus by viewModel.serviceStatus.collectAsStateWithLifecycle() + val serviceStatus by viewModel.validatorDetailsServiceStatus.collectAsStateWithLifecycle() + val snackbarIsVisible by remember { mutableStateOf(false) } + val onSnackbarRetry = { + viewModel.getMyValidators() + } DisposableEffect(lifecycleOwner) { // Create an observer that triggers our remembered callbacks // for sending analytics events @@ -95,20 +117,36 @@ fun ValidatorDetailsScreen( } } lifecycleOwner.lifecycle.addObserver(observer) + // viewModel.isMyValidator.value = false onDispose { lifecycleOwner.lifecycle.removeObserver(observer) } } + LaunchedEffect(Unit) { + viewModel.getMyValidators() + } ValidatorDetailsScreenContent( modifier = modifier, isDark = isDark, state = ValidatorDetailsScreenState( - serviceStatus = serviceStatus, + validatorDetailsServiceStatus = serviceStatus, + appServiceStatus = viewModel.appServiceStatus.value, network = viewModel.network, validator = viewModel.validator, + isMyValidator = viewModel.isMyValidator.value, + feedbackIsValidatorAdded = viewModel.feedbackIsValidatorAdded.value, + snackbarIsVisible = snackbarIsVisible, ), + onAddValidator = { viewModel.addValidator() }, + onRemoveValidator = { viewModel.removeValidator() }, + onSnackbarRetry = + if (viewModel.gotMyValidators.value) { + onSnackbarRetry + } else { + null + }, onBack = onBack, ) } @@ -118,6 +156,9 @@ fun ValidatorDetailsScreenContent( modifier: Modifier = Modifier, isDark: Boolean = isSystemInDarkTheme(), state: ValidatorDetailsScreenState, + onAddValidator: () -> Unit, + onRemoveValidator: () -> Unit, + onSnackbarRetry: (() -> Unit)?, onBack: () -> Unit, ) { val scrollState = rememberScrollState() @@ -127,369 +168,456 @@ fun ValidatorDetailsScreenContent( } else { scrollState.value.toFloat() / scrollState.maxValue.toFloat() * 4.0f } - Box( - modifier = - modifier - .clipToBounds() - .fillMaxSize(), + SnackbarScaffold( + snackbarText = stringResource(id = R.string.validator_details_my_validators_fetch_error), + snackbarIsVisible = state.snackbarIsVisible, + onSnackbarClick = null, + onSnackbarRetry = onSnackbarRetry, ) { - AnimatedBackground( - modifier = - Modifier - .fillMaxSize() - .zIndex(0.0f), - ) - Column( + Box( modifier = - Modifier - .scrollHeader( - isDark = isDark, - scrolledRatio = scrolledRatio, - ), + modifier + .clipToBounds() + .fillMaxSize(), ) { - Spacer( + AnimatedBackground( modifier = Modifier - .padding(0.dp, dimensionResource(id = R.dimen.common_content_margin_top)) - .statusBarsPadding(), + .fillMaxSize() + .zIndex(0.0f), ) - Row( - horizontalArrangement = Arrangement.spacedBy(dimensionResource(id = R.dimen.common_panel_padding)), - verticalAlignment = Alignment.CenterVertically, + Column( modifier = Modifier - .height(dimensionResource(id = R.dimen.network_selector_button_height)) - .padding(dimensionResource(id = R.dimen.common_padding), 0.dp), + .scrollHeader( + isDark = isDark, + scrolledRatio = scrolledRatio, + ) + .zIndex(20.0f), ) { - Box( + Spacer( modifier = Modifier - .size(dimensionResource(id = R.dimen.network_selector_button_height)) - .noRippleClickable { - onBack() - }, - contentAlignment = Alignment.CenterStart, - ) { - Image( - painterResource(id = R.drawable.arrow_back), - contentDescription = stringResource(id = R.string.description_back), - ) - } - Spacer(modifier = Modifier.weight(1.0f)) - state.network?.let { network -> - NetworkSelectorButton( - network = network, - isClickable = false, - ) - } - Box( + .padding(0.dp, dimensionResource(id = R.dimen.common_content_margin_top)) + .statusBarsPadding(), + ) + Row( + horizontalArrangement = Arrangement.spacedBy(dimensionResource(id = R.dimen.common_panel_padding)), + verticalAlignment = Alignment.CenterVertically, modifier = Modifier - .noRippleClickable { - // no-op - } - .size(dimensionResource(id = R.dimen.top_small_button_size)) - .background( - color = Color.panelBg(isDark), - shape = RoundedCornerShape(12.dp), - ), - contentAlignment = Alignment.Center, + .height(dimensionResource(id = R.dimen.network_selector_button_height)) + .padding(dimensionResource(id = R.dimen.common_padding), 0.dp), ) { - Image( - modifier = Modifier.size(dimensionResource(id = R.dimen.top_small_button_image_size)), - painter = painterResource(id = R.drawable.add_validator_icon), - contentDescription = "", - ) + Box( + modifier = + Modifier + .size(dimensionResource(id = R.dimen.network_selector_button_height)) + .noRippleClickable { + onBack() + }, + contentAlignment = Alignment.CenterStart, + ) { + Image( + painterResource(id = R.drawable.arrow_back), + contentDescription = stringResource(id = R.string.description_back), + ) + } + Spacer(modifier = Modifier.weight(1.0f)) + state.network?.let { network -> + NetworkSelectorButton( + network = network, + isClickable = false, + ) + } + Box( + modifier = + Modifier + .noRippleClickable { + if (state.appServiceStatus is DataRequestState.Idle) { + if (state.isMyValidator) { + onRemoveValidator() + } else { + onAddValidator() + } + } + } + .size(dimensionResource(id = R.dimen.top_small_button_size)) + .background( + color = Color.panelBg(isDark), + shape = RoundedCornerShape(12.dp), + ), + contentAlignment = Alignment.Center, + ) { + if (state.appServiceStatus is DataRequestState.Loading) { + CircularProgressIndicator( + modifier = + Modifier + .size(dimensionResource(id = R.dimen.common_small_progress_size)) + .align(Alignment.Center), + strokeWidth = 1.dp, + color = Color.text(isDark).copy(alpha = 0.85f), + trackColor = Color.transparent(), + ) + } else { + Image( + modifier = Modifier.size(dimensionResource(id = R.dimen.top_small_button_image_size)), + painter = + painterResource( + id = + if (state.isMyValidator) { + R.drawable.remove_validator_icon + } else { + R.drawable.add_validator_icon + }, + ), + contentDescription = "", + ) + } + } + Box( + modifier = + Modifier + .noRippleClickable { + // no-op + } + .size(dimensionResource(id = R.dimen.top_small_button_size)) + .background( + color = Color.panelBg(isDark), + shape = RoundedCornerShape(12.dp), + ), + contentAlignment = Alignment.Center, + ) { + Text( + text = stringResource(id = R.string.validator_details_rewards_icon), + style = Font.light(16.sp), + color = Color.text(isDark), + ) + } + Box( + modifier = + Modifier + .noRippleClickable { + // no-op + } + .size(dimensionResource(id = R.dimen.top_small_button_size)) + .background( + color = Color.panelBg(isDark), + shape = RoundedCornerShape(12.dp), + ), + contentAlignment = Alignment.Center, + ) { + Text( + text = stringResource(id = R.string.validator_details_para_validation_icon), + style = Font.light(14.sp), + color = Color.text(isDark), + ) + } + Box( + modifier = + Modifier + .noRippleClickable { + // no-op + } + .size(dimensionResource(id = R.dimen.top_small_button_size)) + .background( + color = Color.panelBg(isDark), + shape = RoundedCornerShape(12.dp), + ), + contentAlignment = Alignment.Center, + ) { + Image( + modifier = Modifier.size(dimensionResource(id = R.dimen.top_small_button_image_size)), + painter = painterResource(id = R.drawable.validator_reports_icon), + contentDescription = "", + ) + } } - Box( + Spacer(modifier = Modifier.height(dimensionResource(id = R.dimen.common_padding))) + } + + Column( + modifier = + Modifier + .fillMaxSize() + .padding( + dimensionResource(id = R.dimen.common_padding), + 0.dp, + dimensionResource(id = R.dimen.common_padding), + dimensionResource(id = R.dimen.common_padding), + ) + .zIndex(5f) + .verticalScroll(scrollState), + Arrangement.spacedBy( + dimensionResource(id = R.dimen.common_panel_padding), + ), + ) { + Spacer( modifier = Modifier - .noRippleClickable { - // no-op - } - .size(dimensionResource(id = R.dimen.top_small_button_size)) - .background( - color = Color.panelBg(isDark), - shape = RoundedCornerShape(12.dp), - ), - contentAlignment = Alignment.Center, - ) { - Image( - modifier = Modifier.size(dimensionResource(id = R.dimen.top_small_button_image_size)), - painter = painterResource(id = R.drawable.remove_validator_icon), - contentDescription = "", + .padding( + PaddingValues( + 0.dp, + dimensionResource(id = R.dimen.validator_details_content_padding_top), + 0.dp, + 0.dp, + ), + ) + .statusBarsPadding(), + ) + // content begins here + state.validator?.let { validator -> + IdenticonView( + modifier = + Modifier + .fillMaxWidth() + .height(dimensionResource(id = R.dimen.validator_details_identicon_height)), + accountId = validator.account.id, ) } - Box( - modifier = - Modifier - .noRippleClickable { - // no-op - } - .size(dimensionResource(id = R.dimen.top_small_button_size)) - .background( - color = Color.panelBg(isDark), - shape = RoundedCornerShape(12.dp), - ), - contentAlignment = Alignment.Center, + IdentityView(validator = state.validator) + // Spacer(modifier = Modifier.height(dimensionResource(id = R.dimen.common_panel_padding) / 2)) + BalanceView( + titleResourceId = R.string.validator_details_nomination_total, + network = state.network, + balance = state.validator?.nominationTotal(), + ) + BalanceView( + titleResourceId = R.string.validator_details_self_stake, + network = state.network, + balance = state.validator?.selfStake?.activeAmount, + ) + state.validator?.let { validator -> + validator.validatorStake?.let { validatorStake -> + NominatorListView( + titleResourceId = R.string.validator_details_active_stake, + network = state.network, + count = validatorStake.nominators.size, + total = validatorStake.totalStake, + nominations = + validatorStake.nominators.map { + Triple(it.account.address, false, it.stake) + }.sortedByDescending { + it.third + }, + ) + } + } + NominatorListView( + titleResourceId = R.string.validator_details_inactive_nominations, + network = state.network, + count = state.validator?.inactiveNominations()?.size, + total = state.validator?.inactiveNominationTotal(), + nominations = + state.validator?.inactiveNominations()?.map { + Triple(it.stashAccount.address, false, it.stake.activeAmount) + }?.sortedByDescending { + it.third + }, + ) + state.validator?.account?.discoveredAt?.let { + AccountAgeView(discoveredAt = it) + } + HorizontalDataView( + titleResourceId = R.string.validator_details_offline_faults, + text = state.validator?.offlineOffenceCount?.toString() ?: "-", + displayExclamation = (state.validator?.offlineOffenceCount ?: 0) > 0, + ) + VerticalDataView( + modifier = Modifier.fillMaxWidth(), + titleResourceId = R.string.validator_details_reward_destination, + text = + state.validator?.rewardDestination?.display( + context = LocalContext.current, + prefix = state.network?.ss58Prefix?.toShort() ?: 0, + ) ?: "-", + ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(dimensionResource(id = R.dimen.common_panel_padding)), ) { - Text( - text = stringResource(id = R.string.validator_details_rewards_icon), - style = Font.light(16.sp), - color = Color.text(isDark), + VerticalDataView( + modifier = Modifier.weight(1.0f), + titleResourceId = R.string.validator_details_commission, + text = + String.format( + stringResource(id = R.string.percentage), + formatDecimal( + number = + ( + state.validator?.preferences?.commissionPerBillion + ?: 0 + ).toBigInteger(), + tokenDecimalCount = 7, + formatDecimalCount = 2, + ), + ), ) - } - Box( - modifier = - Modifier - .noRippleClickable { - // no-op - } - .size(dimensionResource(id = R.dimen.top_small_button_size)) - .background( - color = Color.panelBg(isDark), - shape = RoundedCornerShape(12.dp), + VerticalDataView( + modifier = Modifier.weight(1.0f), + titleResourceId = R.string.validator_details_apr, + text = + String.format( + stringResource(id = R.string.percentage), + formatDecimal( + number = + ( + state.validator?.returnRatePerBillion + ?: 0 + ).toBigInteger(), + tokenDecimalCount = 7, + formatDecimalCount = 2, + ), ), - contentAlignment = Alignment.Center, - ) { - Text( - text = stringResource(id = R.string.validator_details_para_validation_icon), - style = Font.light(14.sp), - color = Color.text(isDark), ) } - Box( - modifier = - Modifier - .noRippleClickable { - // no-op - } - .size(dimensionResource(id = R.dimen.top_small_button_size)) - .background( - color = Color.panelBg(isDark), - shape = RoundedCornerShape(12.dp), - ), - contentAlignment = Alignment.Center, + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(dimensionResource(id = R.dimen.common_panel_padding)), ) { - Image( - modifier = Modifier.size(dimensionResource(id = R.dimen.top_small_button_image_size)), - painter = painterResource(id = R.drawable.validator_reports_icon), - contentDescription = "", + VerticalDataView( + modifier = Modifier.weight(1.0f), + titleResourceId = R.string.validator_details_era_blocks, + text = state.validator?.blocksAuthored?.toString() ?: "-", ) - } - } - Spacer(modifier = Modifier.height(dimensionResource(id = R.dimen.common_padding))) - } - - Column( - modifier = - Modifier - .fillMaxSize() - .padding( - dimensionResource(id = R.dimen.common_padding), - 0.dp, - dimensionResource(id = R.dimen.common_padding), - dimensionResource(id = R.dimen.common_padding), + VerticalDataView( + modifier = Modifier.weight(1.0f), + titleResourceId = R.string.validator_details_era_points, + text = state.validator?.rewardPoints?.toString() ?: "-", ) - .zIndex(5f) - .verticalScroll(scrollState), - Arrangement.spacedBy( - dimensionResource(id = R.dimen.common_panel_padding), - ), - ) { - Spacer( - modifier = - Modifier - .padding( - PaddingValues( + } + state.validator?.let { validator -> + if (validator.onekvCandidateRecordId != null) { + OneKVDetailsView( + modifier = Modifier.fillMaxWidth(), + validator = validator, + ) + } + } + Spacer( + modifier = + Modifier + .navigationBarsPadding() + .padding( 0.dp, - dimensionResource(id = R.dimen.validator_details_content_padding_top), 0.dp, 0.dp, + dimensionResource(id = R.dimen.common_scrollable_content_margin_bottom), ), - ) - .statusBarsPadding(), - ) - // content begins here - state.validator?.let { validator -> - IdenticonView( - modifier = - Modifier - .fillMaxWidth() - .height(dimensionResource(id = R.dimen.validator_details_identicon_height)), - accountId = validator.account.id, ) } - IdentityView(validator = state.validator) - // Spacer(modifier = Modifier.height(dimensionResource(id = R.dimen.common_panel_padding) / 2)) - BalanceView( - titleResourceId = R.string.validator_details_nomination_total, - network = state.network, - balance = state.validator?.nominationTotal(), - ) - BalanceView( - titleResourceId = R.string.validator_details_self_stake, - network = state.network, - balance = state.validator?.selfStake?.activeAmount, - ) - state.validator?.let { validator -> - validator.validatorStake?.let { validatorStake -> - NominatorListView( - titleResourceId = R.string.validator_details_active_stake, - network = state.network, - count = validatorStake.nominators.size, - total = validatorStake.totalStake, - nominations = - validatorStake.nominators.map { - Triple(it.account.address, false, it.stake) - }.sortedByDescending { - it.third - }, - ) - } - } - NominatorListView( - titleResourceId = R.string.validator_details_inactive_nominations, - network = state.network, - count = state.validator?.inactiveNominations()?.size, - total = state.validator?.inactiveNominationTotal(), - nominations = - state.validator?.inactiveNominations()?.map { - Triple(it.stashAccount.address, false, it.stake.activeAmount) - }?.sortedByDescending { - it.third - }, - ) - state.validator?.account?.discoveredAt?.let { - AccountAgeView(discoveredAt = it) - } - HorizontalDataView( - titleResourceId = R.string.validator_details_offline_faults, - text = state.validator?.offlineOffenceCount?.toString() ?: "-", - displayExclamation = (state.validator?.offlineOffenceCount ?: 0) > 0, - ) - VerticalDataView( - modifier = Modifier.fillMaxWidth(), - titleResourceId = R.string.validator_details_reward_destination, - text = - state.validator?.rewardDestination?.display( - context = LocalContext.current, - prefix = state.network?.ss58Prefix?.toShort() ?: 0, - ) ?: "-", - ) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(dimensionResource(id = R.dimen.common_panel_padding)), - ) { - VerticalDataView( - modifier = Modifier.weight(1.0f), - titleResourceId = R.string.validator_details_commission, - text = - String.format( - stringResource(id = R.string.percentage), - formatDecimal( - number = - ( - state.validator?.preferences?.commissionPerBillion - ?: 0 - ).toBigInteger(), - tokenDecimalCount = 7, - formatDecimalCount = 2, - ), - ), - ) - VerticalDataView( - modifier = Modifier.weight(1.0f), - titleResourceId = R.string.validator_details_apr, - text = - String.format( - stringResource(id = R.string.percentage), - formatDecimal( - number = (state.validator?.returnRatePerBillion ?: 0).toBigInteger(), - tokenDecimalCount = 7, - formatDecimalCount = 2, - ), - ), + state.validator?.let { + IconsView( + Modifier + .padding( + 0.dp, + 0.dp, + 0.dp, + dimensionResource(id = R.dimen.common_padding), + ) + .navigationBarsPadding() + .fillMaxWidth() + .zIndex(10.0f) + .align(Alignment.BottomCenter), + validator = it, ) } - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(dimensionResource(id = R.dimen.common_panel_padding)), + AnimatedVisibility( + modifier = + Modifier + .align(alignment = Alignment.BottomCenter) + .zIndex(12.0f), + visible = state.feedbackIsValidatorAdded != null, + enter = + slideInVertically( + initialOffsetY = { + it + }, + ) + fadeIn(), + exit = + slideOutVertically( + targetOffsetY = { + it + }, + ) + fadeOut(), ) { - VerticalDataView( - modifier = Modifier.weight(1.0f), - titleResourceId = R.string.validator_details_era_blocks, - text = state.validator?.blocksAuthored?.toString() ?: "-", - ) - VerticalDataView( - modifier = Modifier.weight(1.0f), - titleResourceId = R.string.validator_details_era_points, - text = state.validator?.rewardPoints?.toString() ?: "-", - ) - } - state.validator?.let { validator -> - if (validator.onekvCandidateRecordId != null) { - OneKVDetailsView( - modifier = Modifier.fillMaxWidth(), - validator = validator, + Column( + horizontalAlignment = Alignment.CenterHorizontally, + ) { + val roundedCorner = + RoundedCornerShape(dimensionResource(id = R.dimen.common_panel_border_radius)) + Row( + modifier = + Modifier + .shadow( + elevation = 12.dp, + shape = roundedCorner, + spotColor = Color.green().copy(alpha = 0.75f), + ) + .background(color = Color.bg(isDark)) + .clip(shape = roundedCorner) + .padding(dimensionResource(id = R.dimen.common_padding)), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(dimensionResource(id = R.dimen.common_padding)), + ) { + Image( + modifier = Modifier.size(24.dp), + painter = painterResource(id = R.drawable.check), + contentDescription = "", + ) + Text( + text = + stringResource( + id = + if (state.feedbackIsValidatorAdded == true) { + R.string.validator_details_validator_added + } else { + R.string.validator_details_validator_removed + }, + ), + style = Font.semiBold(16.sp), + color = Color.text(isDark), + ) + } + Spacer( + modifier = + Modifier.padding( + 0.dp, + 0.dp, + 0.dp, + dimensionResource(id = R.dimen.common_padding), + ).navigationBarsPadding(), ) } } - Spacer( + Box( modifier = Modifier - .navigationBarsPadding() - .padding( - 0.dp, - 0.dp, - 0.dp, - dimensionResource(id = R.dimen.common_scrollable_content_margin_bottom), + .fillMaxWidth() + .height(dimensionResource(id = R.dimen.common_bottom_gradient_height)) + .zIndex(7.0f) + .align(Alignment.BottomCenter) + .background( + brush = + Brush.verticalGradient( + colors = + listOf( + Color.transparent(), + Color + .bg(isDark) + .copy(alpha = 0.85f), + Color.bg(isDark), + ), + ), ), ) } - state.validator?.let { - IconsView( - Modifier - .padding( - 0.dp, - 0.dp, - 0.dp, - dimensionResource(id = R.dimen.common_padding), - ) - .navigationBarsPadding() - .fillMaxWidth() - .zIndex(8.0f) - .align(Alignment.BottomCenter), - validator = it, - ) - } - Box( - modifier = - Modifier - .fillMaxWidth() - .height(dimensionResource(id = R.dimen.common_bottom_gradient_height)) - .zIndex(7.0f) - .align(Alignment.BottomCenter) - .background( - brush = - Brush.verticalGradient( - colors = - listOf( - Color.transparent(), - Color - .bg(isDark) - .copy(alpha = 0.85f), - Color.bg(isDark), - ), - ), - ), - ) } } @ThemePreviews @Composable -fun ValidatorCountPanelPreview(isDark: Boolean = isSystemInDarkTheme()) { +fun ValidatorDetailsScreenPreview(isDark: Boolean = isSystemInDarkTheme()) { Surface( color = Color.bg(isDark), ) { @@ -498,10 +626,17 @@ fun ValidatorCountPanelPreview(isDark: Boolean = isSystemInDarkTheme()) { isDark, state = ValidatorDetailsScreenState( - serviceStatus = RPCSubscriptionServiceStatus.Connecting, + validatorDetailsServiceStatus = RPCSubscriptionServiceStatus.Connecting, + appServiceStatus = DataRequestState.Idle, network = PreviewData.networks[0], validator = null, + isMyValidator = false, + feedbackIsValidatorAdded = true, + snackbarIsVisible = true, ), + onAddValidator = {}, + onRemoveValidator = {}, + onSnackbarRetry = {}, onBack = {}, ) } diff --git a/subvt/src/main/java/io/helikon/subvt/ui/screen/validator/details/ValidatorDetailsViewModel.kt b/subvt/src/main/java/io/helikon/subvt/ui/screen/validator/details/ValidatorDetailsViewModel.kt index 9eff99b..10175d1 100644 --- a/subvt/src/main/java/io/helikon/subvt/ui/screen/validator/details/ValidatorDetailsViewModel.kt +++ b/subvt/src/main/java/io/helikon/subvt/ui/screen/validator/details/ValidatorDetailsViewModel.kt @@ -1,5 +1,7 @@ package io.helikon.subvt.ui.screen.validator.details +import android.content.Context +import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -7,17 +9,24 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import io.helikon.subvt.BuildConfig +import io.helikon.subvt.R +import io.helikon.subvt.data.DataRequestState import io.helikon.subvt.data.model.Network +import io.helikon.subvt.data.model.app.NewUserValidator +import io.helikon.subvt.data.model.app.UserValidator import io.helikon.subvt.data.model.app.ValidatorDetails import io.helikon.subvt.data.model.app.ValidatorDetailsDiff import io.helikon.subvt.data.repository.NetworkRepository +import io.helikon.subvt.data.service.AppService import io.helikon.subvt.data.service.RPCSubscriptionListener import io.helikon.subvt.data.service.RPCSubscriptionService import io.helikon.subvt.data.service.ValidatorDetailsService import io.helikon.subvt.ui.navigation.NavigationItem import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import timber.log.Timber import javax.inject.Inject @HiltViewModel @@ -25,14 +34,33 @@ class ValidatorDetailsViewModel @Inject constructor( savedStateHandle: SavedStateHandle, + @ApplicationContext context: Context, private val networkRepository: NetworkRepository, ) : ViewModel(), RPCSubscriptionListener { - private val service = ValidatorDetailsService(this) - private var subscriptionId = -1L - val serviceStatus = service.status private val networkId = NavigationItem.ValidatorDetails.getNetworkId(savedStateHandle) private val accountId = NavigationItem.ValidatorDetails.getAccountId(savedStateHandle) + private val validatorDetailsService = ValidatorDetailsService(this) + private var subscriptionId = -1L + val validatorDetailsServiceStatus = validatorDetailsService.status + + private val appService = + AppService( + context, + "https://${BuildConfig.API_HOST}:${BuildConfig.APP_SERVICE_PORT}/", + ) + private var myValidators = mutableListOf() + private val _gotMyValidators = mutableStateOf(false) + val gotMyValidators: State = _gotMyValidators + private val _isMyValidator = mutableStateOf(false) + val isMyValidator: State = _isMyValidator + private val _appServiceStatus = mutableStateOf>(DataRequestState.Idle) + val appServiceStatus: State> = _appServiceStatus + + private val feedbackDuration = context.resources.getInteger(R.integer.snackbar_short_display_duration_ms) + private val _feedbackIsValidatorAdded = mutableStateOf(null) + val feedbackIsValidatorAdded: State = _feedbackIsValidatorAdded + var network by mutableStateOf(null) private set var validator by mutableStateOf(null) @@ -44,7 +72,7 @@ class ValidatorDetailsViewModel this@ValidatorDetailsViewModel.network = network network.validatorDetailsServiceHost?.let { host -> network.validatorDetailsServicePort?.let { port -> - service.subscribe( + validatorDetailsService.subscribe( host, port, listOf(accountId.toString()), @@ -68,7 +96,7 @@ class ValidatorDetailsViewModel fun unsubscribe() { viewModelScope.launch(Dispatchers.IO) { - service.unsubscribe() + validatorDetailsService.unsubscribe() } } @@ -89,9 +117,66 @@ class ValidatorDetailsViewModel finalizedBlockNumber: Long?, update: ValidatorDetailsDiff?, ) { - Timber.d("7") update?.let { validator = validator?.apply(it) } } + + fun getMyValidators() { + myValidators.clear() + _appServiceStatus.value = DataRequestState.Loading + viewModelScope.launch(Dispatchers.IO) { + val result = appService.getUserValidators() + if (result.isSuccess) { + myValidators.addAll(result.getOrNull() ?: listOf()) + _isMyValidator.value = myValidators.count { it.validatorAccountId == accountId } > 0 + _appServiceStatus.value = DataRequestState.Idle + _gotMyValidators.value = true + } else { + _appServiceStatus.value = DataRequestState.Error(result.exceptionOrNull()) + } + } + } + + fun addValidator() { + _appServiceStatus.value = DataRequestState.Loading + viewModelScope.launch(Dispatchers.IO) { + val request = NewUserValidator(networkId, accountId) + val result = appService.createUserValidator(request) + if (result.isSuccess) { + val userValidator = result.getOrNull() + if (userValidator != null) { + myValidators.add(userValidator) + _isMyValidator.value = true + _appServiceStatus.value = DataRequestState.Idle + _feedbackIsValidatorAdded.value = true + delay(feedbackDuration.toLong()) + _feedbackIsValidatorAdded.value = null + } else { + _appServiceStatus.value = DataRequestState.Error(result.exceptionOrNull()) + } + } else { + _appServiceStatus.value = DataRequestState.Error(result.exceptionOrNull()) + } + } + } + + fun removeValidator() { + _appServiceStatus.value = DataRequestState.Loading + viewModelScope.launch(Dispatchers.IO) { + myValidators.firstOrNull { it.validatorAccountId == accountId }?.let { + val result = appService.deleteUserValidator(it.id) + if (result.isSuccess) { + myValidators.removeAll { myValidator -> myValidator.validatorAccountId == accountId } + _isMyValidator.value = false + _appServiceStatus.value = DataRequestState.Idle + _feedbackIsValidatorAdded.value = false + delay(feedbackDuration.toLong()) + _feedbackIsValidatorAdded.value = null + } else { + _appServiceStatus.value = DataRequestState.Error(result.exceptionOrNull()) + } + } + } + } } diff --git a/subvt/src/main/java/io/helikon/subvt/ui/screen/validator/list/ValidatorListScreen.kt b/subvt/src/main/java/io/helikon/subvt/ui/screen/validator/list/ValidatorListScreen.kt index 266b9d3..379b8bf 100644 --- a/subvt/src/main/java/io/helikon/subvt/ui/screen/validator/list/ValidatorListScreen.kt +++ b/subvt/src/main/java/io/helikon/subvt/ui/screen/validator/list/ValidatorListScreen.kt @@ -197,7 +197,7 @@ fun ValidatorListScreenContent( CircularProgressIndicator( modifier = Modifier - .width(dimensionResource(id = R.dimen.common_progress_width)) + .size(dimensionResource(id = R.dimen.common_progress_size)) .align(Alignment.Center), color = Color.lightGray(), trackColor = Color.transparent(), diff --git a/subvt/src/main/res/drawable-hdpi/check.png b/subvt/src/main/res/drawable-hdpi/check.png new file mode 100644 index 0000000000000000000000000000000000000000..4aee58376200a2db191514de59819a729faa2ebd GIT binary patch literal 3036 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 XPEK~#7F?R{;G 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 literal 0 HcmV?d00001 diff --git a/subvt/src/main/res/drawable-xxxhdpi/check.png b/subvt/src/main/res/drawable-xxxhdpi/check.png new file mode 100644 index 0000000000000000000000000000000000000000..488c7569aa49510fcb9e1f2728fb99cd2cefc502 GIT binary patch literal 14825 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 P00000NkvXXu0mjfmSx$t literal 0 HcmV?d00001 diff --git a/subvt/src/main/res/values/dimens.xml b/subvt/src/main/res/values/dimens.xml index 8f38a22..89827ad 100644 --- a/subvt/src/main/res/values/dimens.xml +++ b/subvt/src/main/res/values/dimens.xml @@ -3,7 +3,8 @@ 16dp 8dp 16dp - 36dp + 36dp + 14dp 24dp 12dp 104dp diff --git a/subvt/src/main/res/values/duration.xml b/subvt/src/main/res/values/duration.xml index e9f77e2..46f15ab 100644 --- a/subvt/src/main/res/values/duration.xml +++ b/subvt/src/main/res/values/duration.xml @@ -4,6 +4,7 @@ 500 2500 + 1000 250 diff --git a/subvt/src/main/res/values/strings.xml b/subvt/src/main/res/values/strings.xml index 6cc33e7..fbba941 100644 --- a/subvt/src/main/res/values/strings.xml +++ b/subvt/src/main/res/values/strings.xml @@ -108,4 +108,5 @@ Slashed Added Removed + Data error. Please check your connection. \ No newline at end of file