You've already forked RekomenciMobile
fix: fixing bugs with phone input field. feat: абсолютно готов экран profile
This commit is contained in:
@@ -19,6 +19,8 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||||
import com.prodhack.moscow2025.presentation.components.TBottomNavigation
|
import com.prodhack.moscow2025.presentation.components.TBottomNavigation
|
||||||
import com.prodhack.moscow2025.presentation.theme.MoscowHackatonTemplateTheme
|
import com.prodhack.moscow2025.presentation.theme.MoscowHackatonTemplateTheme
|
||||||
|
import com.prodhack.moscow2025.presentation.utils.ui.AppSnackbarVisuals
|
||||||
|
import com.prodhack.moscow2025.presentation.utils.ui.SnackbarStyle
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TTasksApp(
|
fun TTasksApp(
|
||||||
@@ -53,10 +55,19 @@ fun TTasksApp(
|
|||||||
SnackbarHost(
|
SnackbarHost(
|
||||||
hostState = snackbarHostState,
|
hostState = snackbarHostState,
|
||||||
snackbar = { data ->
|
snackbar = { data ->
|
||||||
|
val style = (data.visuals as? AppSnackbarVisuals)?.style ?: SnackbarStyle.Error
|
||||||
|
val containerColor = when (style) {
|
||||||
|
SnackbarStyle.Success -> MaterialTheme.colorScheme.tertiaryContainer
|
||||||
|
SnackbarStyle.Error -> MaterialTheme.colorScheme.errorContainer
|
||||||
|
}
|
||||||
|
val contentColor = when (style) {
|
||||||
|
SnackbarStyle.Success -> MaterialTheme.colorScheme.onTertiaryContainer
|
||||||
|
SnackbarStyle.Error -> MaterialTheme.colorScheme.onErrorContainer
|
||||||
|
}
|
||||||
Snackbar(
|
Snackbar(
|
||||||
snackbarData = data,
|
snackbarData = data,
|
||||||
containerColor = MaterialTheme.colorScheme.errorContainer,
|
containerColor = containerColor,
|
||||||
contentColor = MaterialTheme.colorScheme.onErrorContainer,
|
contentColor = contentColor,
|
||||||
shape = MaterialTheme.shapes.medium
|
shape = MaterialTheme.shapes.medium
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,7 +94,10 @@ fun TTasksNavHost(
|
|||||||
composable(AppDestination.Profile.route)
|
composable(AppDestination.Profile.route)
|
||||||
{
|
{
|
||||||
ProfileScreen(
|
ProfileScreen(
|
||||||
snackbarHostState = snackbarHostState
|
snackbarHostState = snackbarHostState,
|
||||||
|
navigateToLoginScreen = {
|
||||||
|
navController.navigate(AppDestination.Login.route)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -95,9 +95,11 @@ class FillProfileViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun onPhoneChange(value: String) {
|
fun onPhoneChange(value: String) {
|
||||||
|
val maxDigits = chosenPattern.value?.pattern?.count { it == '0' } ?: Int.MAX_VALUE
|
||||||
|
val digits = value.filter { it.isDigit() }.take(maxDigits)
|
||||||
_formStateFillProfile.update {
|
_formStateFillProfile.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
phone = value,
|
phone = digits,
|
||||||
errors = it.errors - AuthField.Phone
|
errors = it.errors - AuthField.Phone
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+146
-22
@@ -1,20 +1,36 @@
|
|||||||
package com.prodhack.moscow2025.presentation.screens.profile
|
package com.prodhack.moscow2025.presentation.screens.profile
|
||||||
|
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.imePadding
|
import androidx.compose.foundation.layout.imePadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.systemBarsPadding
|
import androidx.compose.foundation.layout.systemBarsPadding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ButtonColors
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme.colorScheme
|
||||||
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
import androidx.compose.material3.SnackbarDuration
|
import androidx.compose.material3.SnackbarDuration
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
@@ -24,27 +40,38 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.prodhack.moscow2025.R
|
import com.prodhack.moscow2025.R
|
||||||
import com.prodhack.moscow2025.domain.usecase.auth.AuthField
|
import com.prodhack.moscow2025.domain.usecase.auth.AuthField
|
||||||
import com.prodhack.moscow2025.presentation.components.standart.BigButton
|
import com.prodhack.moscow2025.presentation.components.standart.BigButton
|
||||||
|
import com.prodhack.moscow2025.presentation.components.standart.FieldWrapper
|
||||||
import com.prodhack.moscow2025.presentation.components.standart.TTTextField
|
import com.prodhack.moscow2025.presentation.components.standart.TTTextField
|
||||||
import com.prodhack.moscow2025.presentation.theme.Paddings
|
import com.prodhack.moscow2025.presentation.theme.Paddings
|
||||||
|
import com.prodhack.moscow2025.presentation.theme.Shapes
|
||||||
import com.prodhack.moscow2025.presentation.utils.ErrorCollectorScope
|
import com.prodhack.moscow2025.presentation.utils.ErrorCollectorScope
|
||||||
|
import com.prodhack.moscow2025.presentation.utils.PhoneVisualTransformation
|
||||||
import com.prodhack.moscow2025.presentation.utils.UIState
|
import com.prodhack.moscow2025.presentation.utils.UIState
|
||||||
|
import com.prodhack.moscow2025.presentation.utils.ui.SnackbarStyle
|
||||||
|
import com.prodhack.moscow2025.presentation.utils.ui.showSnackbar
|
||||||
import org.koin.androidx.compose.koinViewModel
|
import org.koin.androidx.compose.koinViewModel
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ErrorCollectorScope.ProfileScreen(
|
fun ErrorCollectorScope.ProfileScreen(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
snackbarHostState: SnackbarHostState,
|
snackbarHostState: SnackbarHostState,
|
||||||
|
navigateToLoginScreen: () -> Unit,
|
||||||
viewModel: ProfileScreenViewModel = koinViewModel()
|
viewModel: ProfileScreenViewModel = koinViewModel()
|
||||||
) {
|
) {
|
||||||
val typography = androidx.compose.material3.MaterialTheme.typography
|
val typography = androidx.compose.material3.MaterialTheme.typography
|
||||||
|
val sheetState = rememberModalBottomSheetState()
|
||||||
|
val isSheetOpen = remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val formState by viewModel.formStateFillProfile.collectAsState()
|
val formState by viewModel.formStateFillProfile.collectAsState()
|
||||||
|
|
||||||
@@ -71,6 +98,7 @@ fun ErrorCollectorScope.ProfileScreen(
|
|||||||
if (profileState is UIState.Success) {
|
if (profileState is UIState.Success) {
|
||||||
snackbarHostState.showSnackbar(
|
snackbarHostState.showSnackbar(
|
||||||
message = "Данные профиля обновлены",
|
message = "Данные профиля обновлены",
|
||||||
|
style = SnackbarStyle.Success,
|
||||||
duration = SnackbarDuration.Short
|
duration = SnackbarDuration.Short
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -85,26 +113,11 @@ fun ErrorCollectorScope.ProfileScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.imePadding()
|
|
||||||
.systemBarsPadding(),
|
|
||||||
contentAlignment = Alignment.BottomStart
|
|
||||||
) {
|
|
||||||
Image(
|
|
||||||
painter = painterResource(R.drawable.lottie),
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.BottomStart)
|
|
||||||
.padding(start = 16.dp)
|
|
||||||
.fillMaxWidth(0.35f),
|
|
||||||
contentScale = ContentScale.FillWidth
|
|
||||||
)
|
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
.imePadding()
|
||||||
|
.systemBarsPadding()
|
||||||
.padding(horizontal = 30.dp),
|
.padding(horizontal = 30.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Top
|
verticalArrangement = Arrangement.Top
|
||||||
@@ -113,7 +126,7 @@ fun ErrorCollectorScope.ProfileScreen(
|
|||||||
Text(
|
Text(
|
||||||
text = "Профиль",
|
text = "Профиль",
|
||||||
style = typography.titleLarge,
|
style = typography.titleLarge,
|
||||||
fontSize = 32.sp
|
fontSize = 40.sp
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(20.dp))
|
Spacer(Modifier.height(20.dp))
|
||||||
TTTextField(
|
TTTextField(
|
||||||
@@ -138,13 +151,65 @@ fun ErrorCollectorScope.ProfileScreen(
|
|||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email)
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email)
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(12.dp))
|
Spacer(Modifier.height(12.dp))
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.height(IntrinsicSize.Min),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(Paddings.medium)
|
||||||
|
) {
|
||||||
|
FieldWrapper(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(IntrinsicSize.Min)
|
||||||
|
.fillMaxHeight()
|
||||||
|
) {
|
||||||
|
BasicTextField(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(bottom = 16.dp)
|
||||||
|
.clip(Shapes.smallRoundedBox)
|
||||||
|
.background(
|
||||||
|
color = colorScheme.primary,
|
||||||
|
shape = Shapes.smallRoundedBox
|
||||||
|
),
|
||||||
|
value = viewModel.chosenPattern.value?.prefix ?: "",
|
||||||
|
onValueChange = {},
|
||||||
|
readOnly = true,
|
||||||
|
textStyle = TextStyle(
|
||||||
|
color = colorScheme.onPrimary
|
||||||
|
),
|
||||||
|
decorationBox = { innerTextField ->
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable { isSheetOpen.value = true }
|
||||||
|
.padding(8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Box(modifier = Modifier.weight(1f)) {
|
||||||
|
innerTextField()
|
||||||
|
}
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.size(15.dp),
|
||||||
|
painter = painterResource(R.drawable.ic_arr_dropdown),
|
||||||
|
tint = colorScheme.onPrimary,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
TTTextField(
|
TTTextField(
|
||||||
value = formState.phone,
|
value = formState.phone,
|
||||||
onValueChange = viewModel::onPhoneChange,
|
onValueChange = viewModel::onPhoneChange,
|
||||||
label = "Телефон",
|
label = "Телефон",
|
||||||
error = formState.errors[AuthField.Phone],
|
keyboardOptions = KeyboardOptions(
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone)
|
keyboardType = KeyboardType.Phone
|
||||||
|
),
|
||||||
|
visualTransformation = viewModel.chosenPattern.value?.pattern?.let {
|
||||||
|
PhoneVisualTransformation(it, '0')
|
||||||
|
} ?: VisualTransformation.None,
|
||||||
|
error = formState.errors[AuthField.Phone]
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
BigButton(
|
BigButton(
|
||||||
@@ -153,7 +218,66 @@ fun ErrorCollectorScope.ProfileScreen(
|
|||||||
buttonText = "Сохранить",
|
buttonText = "Сохранить",
|
||||||
isLoading = profileState is UIState.Loading
|
isLoading = profileState is UIState.Loading
|
||||||
)
|
)
|
||||||
|
Spacer(Modifier.height(15.dp))
|
||||||
|
Button(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(60.dp),
|
||||||
|
shape = Shapes.smallRoundedBox,
|
||||||
|
onClick = {
|
||||||
|
viewModel.logout()
|
||||||
|
navigateToLoginScreen()
|
||||||
|
},
|
||||||
|
colors = ButtonColors(
|
||||||
|
containerColor = colorScheme.errorContainer,
|
||||||
|
contentColor = colorScheme.onErrorContainer,
|
||||||
|
disabledContainerColor = colorScheme.errorContainer,
|
||||||
|
disabledContentColor = colorScheme.onErrorContainer
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Text(
|
||||||
|
text = "Выйти из аккаунта",
|
||||||
|
style = typography.titleMedium,
|
||||||
|
fontSize = 24.sp
|
||||||
|
)
|
||||||
|
Spacer(Modifier.width(Paddings.small))
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.logout_icon),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Spacer(modifier = Modifier.height(48.dp))
|
Spacer(modifier = Modifier.height(48.dp))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isSheetOpen.value) {
|
||||||
|
ModalBottomSheet(
|
||||||
|
sheetState = sheetState,
|
||||||
|
onDismissRequest = { isSheetOpen.value = false },
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
||||||
|
items(viewModel.phoneNumberPatterns) { pattern ->
|
||||||
|
Text(
|
||||||
|
text = pattern.name,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(10.dp)
|
||||||
|
.clickable {
|
||||||
|
viewModel.chosenPattern.value = pattern
|
||||||
|
viewModel.onPhoneChange(formState.phone)
|
||||||
|
isSheetOpen.value = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+75
-5
@@ -6,18 +6,26 @@ import android.graphics.Bitmap
|
|||||||
import android.graphics.drawable.BitmapDrawable
|
import android.graphics.drawable.BitmapDrawable
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.paging.map
|
import androidx.paging.map
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
|
import com.prodhack.moscow2025.data.data_providers.PhoneNumberPatternsProvider
|
||||||
import com.prodhack.moscow2025.domain.interfaces.GalleryRepository
|
import com.prodhack.moscow2025.domain.interfaces.GalleryRepository
|
||||||
import com.prodhack.moscow2025.domain.models.UpdateUserData
|
import com.prodhack.moscow2025.domain.models.UpdateUserData
|
||||||
import com.prodhack.moscow2025.domain.usecase.auth.AuthField
|
import com.prodhack.moscow2025.domain.usecase.auth.AuthField
|
||||||
import com.prodhack.moscow2025.domain.usecase.auth.GetUserUseCase
|
import com.prodhack.moscow2025.domain.usecase.auth.GetUserUseCase
|
||||||
|
import com.prodhack.moscow2025.domain.usecase.auth.LogOutUseCase
|
||||||
import com.prodhack.moscow2025.domain.usecase.auth.UpdateUserUseCase
|
import com.prodhack.moscow2025.domain.usecase.auth.UpdateUserUseCase
|
||||||
import com.prodhack.moscow2025.domain.usecase.auth.ValidateAuthFieldsUseCase
|
import com.prodhack.moscow2025.domain.usecase.auth.ValidateAuthFieldsUseCase
|
||||||
|
import com.prodhack.moscow2025.domain.usecase.GetDefaultPhoneNumberPatternUseCase
|
||||||
|
import com.prodhack.moscow2025.presentation.screens.fillProfile.UIPhoneNumberPattern
|
||||||
|
import com.prodhack.moscow2025.presentation.screens.fillProfile.mapToUI
|
||||||
import com.prodhack.moscow2025.presentation.utils.UIState
|
import com.prodhack.moscow2025.presentation.utils.UIState
|
||||||
import com.prodhack.moscow2025.presentation.utils.base.BaseViewModel
|
import com.prodhack.moscow2025.presentation.utils.base.BaseViewModel
|
||||||
|
import com.prodhack.moscow2025.presentation.utils.convertNumberToPattern
|
||||||
import com.prodhack.moscow2025.presentation.utils.toByteArray
|
import com.prodhack.moscow2025.presentation.utils.toByteArray
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -68,6 +76,8 @@ class ProfileScreenViewModel(
|
|||||||
private val getUserUseCase: GetUserUseCase,
|
private val getUserUseCase: GetUserUseCase,
|
||||||
private val updateUserUseCase: UpdateUserUseCase,
|
private val updateUserUseCase: UpdateUserUseCase,
|
||||||
private val validateAuthFieldsUseCase: ValidateAuthFieldsUseCase,
|
private val validateAuthFieldsUseCase: ValidateAuthFieldsUseCase,
|
||||||
|
private val logOutUseCase: LogOutUseCase,
|
||||||
|
private val getDefaultPhoneNumberPatternUseCase: GetDefaultPhoneNumberPatternUseCase,
|
||||||
private val galleryRepository: GalleryRepository
|
private val galleryRepository: GalleryRepository
|
||||||
): BaseViewModel() {
|
): BaseViewModel() {
|
||||||
private val _formStateProfile = MutableStateFlow(ProfileState())
|
private val _formStateProfile = MutableStateFlow(ProfileState())
|
||||||
@@ -76,6 +86,9 @@ class ProfileScreenViewModel(
|
|||||||
private val _profileState = MutableUIStateFlow<String>()
|
private val _profileState = MutableUIStateFlow<String>()
|
||||||
val profileState: StateFlow<UIState<String>> = _profileState
|
val profileState: StateFlow<UIState<String>> = _profileState
|
||||||
|
|
||||||
|
val chosenPattern = mutableStateOf<UIPhoneNumberPattern?>(null)
|
||||||
|
val phoneNumberPatterns = mutableStateListOf<UIPhoneNumberPattern>()
|
||||||
|
|
||||||
fun onEmailChange(value: String) {
|
fun onEmailChange(value: String) {
|
||||||
_formStateProfile.update {
|
_formStateProfile.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
@@ -104,9 +117,11 @@ class ProfileScreenViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun onPhoneChange(value: String) {
|
fun onPhoneChange(value: String) {
|
||||||
|
val maxDigits = chosenPattern.value?.pattern?.count { it == '0' } ?: Int.MAX_VALUE
|
||||||
|
val digits = value.filter { it.isDigit() }.take(maxDigits)
|
||||||
_formStateProfile.update {
|
_formStateProfile.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
phone = value,
|
phone = digits,
|
||||||
errors = it.errors - AuthField.Phone
|
errors = it.errors - AuthField.Phone
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -167,38 +182,93 @@ class ProfileScreenViewModel(
|
|||||||
phone = _formStateProfile.value.phone
|
phone = _formStateProfile.value.phone
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!validation.isValid) {
|
val errors = validation.errors.toMutableMap()
|
||||||
_formStateProfile.update { it.copy(errors = validation.errors) }
|
|
||||||
|
val pattern = chosenPattern.value
|
||||||
|
if (pattern == null) {
|
||||||
|
errors[AuthField.Phone] = "Выберите код страны"
|
||||||
|
} else {
|
||||||
|
val expectedDigits = pattern.pattern.count { it == '0' }
|
||||||
|
if (_formStateProfile.value.phone.length != expectedDigits) {
|
||||||
|
errors[AuthField.Phone] = "Номер должен содержать $expectedDigits цифр"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.isNotEmpty()) {
|
||||||
|
_formStateProfile.update { it.copy(errors = errors) }
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
_profileState.emit(UIState.Loading())
|
_profileState.emit(UIState.Loading())
|
||||||
|
|
||||||
|
val formattedPhone = pattern?.mapToDomain()?.let { phonePattern ->
|
||||||
|
convertNumberToPattern(phonePattern, _formStateProfile.value.phone)
|
||||||
|
} ?: _formStateProfile.value.phone
|
||||||
|
|
||||||
val result = updateUserUseCase(
|
val result = updateUserUseCase(
|
||||||
UpdateUserData(
|
UpdateUserData(
|
||||||
firstName = _formStateProfile.value.firstName,
|
firstName = _formStateProfile.value.firstName,
|
||||||
lastName = _formStateProfile.value.lastName,
|
lastName = _formStateProfile.value.lastName,
|
||||||
email = _formStateProfile.value.email,
|
email = _formStateProfile.value.email,
|
||||||
phone = _formStateProfile.value.phone
|
phone = formattedPhone
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
result.map { it.id }.collectRequest(_profileState)
|
result.map { it.id }.collectRequest(_profileState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun logout() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
logOutUseCase()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
loadPhonePatterns()
|
||||||
|
|
||||||
val user = getUserUseCase().getOrNull()
|
val user = getUserUseCase().getOrNull()
|
||||||
if (user != null) {
|
if (user != null) {
|
||||||
|
val digits = user.phone.orEmpty().filter { it.isDigit() }
|
||||||
|
val selectedPattern = phoneNumberPatterns.firstOrNull { pattern ->
|
||||||
|
val codeDigits = pattern.countryCode.filter { it.isDigit() }
|
||||||
|
digits.startsWith(codeDigits) && digits.length >= codeDigits.length
|
||||||
|
} ?: phoneNumberPatterns.firstOrNull {
|
||||||
|
it.countryCodeISO.equals(
|
||||||
|
getDefaultPhoneNumberPatternUseCase.execute()?.countryCodeISO,
|
||||||
|
ignoreCase = true
|
||||||
|
)
|
||||||
|
} ?: phoneNumberPatterns.firstOrNull()
|
||||||
|
|
||||||
|
selectedPattern?.let { chosenPattern.value = it }
|
||||||
|
|
||||||
|
val digitsWithoutCode = selectedPattern?.let {
|
||||||
|
val codeDigits = it.countryCode.filter { d -> d.isDigit() }
|
||||||
|
if (digits.startsWith(codeDigits)) digits.drop(codeDigits.length) else digits
|
||||||
|
} ?: digits
|
||||||
|
|
||||||
|
val maxDigits = selectedPattern?.pattern?.count { it == '0' } ?: Int.MAX_VALUE
|
||||||
|
|
||||||
_formStateProfile.update {
|
_formStateProfile.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
firstName = user.firstName.orEmpty(),
|
firstName = user.firstName.orEmpty(),
|
||||||
lastName = user.lastName.orEmpty(),
|
lastName = user.lastName.orEmpty(),
|
||||||
email = user.email,
|
email = user.email,
|
||||||
phone = user.phone.orEmpty()
|
phone = digitsWithoutCode.take(maxDigits)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun loadPhonePatterns() {
|
||||||
|
phoneNumberPatterns.clear()
|
||||||
|
phoneNumberPatterns.addAll(
|
||||||
|
PhoneNumberPatternsProvider.phoneNumberPatterns.map { it.mapToUI() }
|
||||||
|
)
|
||||||
|
if (chosenPattern.value == null) {
|
||||||
|
val defaultPattern = getDefaultPhoneNumberPatternUseCase.execute()?.mapToUI()
|
||||||
|
chosenPattern.value = defaultPattern ?: phoneNumberPatterns.firstOrNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+31
-8
@@ -29,7 +29,19 @@ class PhoneVisualTransformation(val mask: String, val maskNumber: Char) : Visual
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return TransformedText(annotatedString, PhoneOffsetMapper(mask, maskNumber))
|
if (annotatedString.isEmpty()) {
|
||||||
|
return TransformedText(annotatedString, OffsetMapping.Identity)
|
||||||
|
}
|
||||||
|
|
||||||
|
return TransformedText(
|
||||||
|
annotatedString,
|
||||||
|
PhoneOffsetMapper(
|
||||||
|
mask = mask,
|
||||||
|
numberChar = maskNumber,
|
||||||
|
transformedLength = annotatedString.length,
|
||||||
|
maxDigits = trimmed.length
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
@@ -45,19 +57,30 @@ class PhoneVisualTransformation(val mask: String, val maskNumber: Char) : Visual
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class PhoneOffsetMapper(val mask: String, val numberChar: Char) : OffsetMapping {
|
private class PhoneOffsetMapper(
|
||||||
|
val mask: String,
|
||||||
|
val numberChar: Char,
|
||||||
|
private val transformedLength: Int,
|
||||||
|
private val maxDigits: Int
|
||||||
|
) : OffsetMapping {
|
||||||
|
|
||||||
override fun originalToTransformed(offset: Int): Int {
|
override fun originalToTransformed(offset: Int): Int {
|
||||||
var noneDigitCount = 0
|
if (offset <= 0) return 0
|
||||||
var i = 0
|
var digitsSeen = 0
|
||||||
while (i < offset + noneDigitCount) {
|
var index = 0
|
||||||
if (mask[i++] != numberChar) noneDigitCount++
|
val targetDigits = offset.coerceAtMost(maxDigits)
|
||||||
|
|
||||||
|
while (index < mask.length && digitsSeen < targetDigits) {
|
||||||
|
if (mask[index] == numberChar) {
|
||||||
|
digitsSeen++
|
||||||
}
|
}
|
||||||
return offset + noneDigitCount
|
index++
|
||||||
|
}
|
||||||
|
return index.coerceAtMost(transformedLength)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun transformedToOriginal(offset: Int): Int =
|
override fun transformedToOriginal(offset: Int): Int =
|
||||||
offset - mask.take(offset).count { it != numberChar }
|
mask.take(offset.coerceAtMost(transformedLength)).count { it == numberChar }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun convertNumberToPattern(pattern: PhoneNumberPattern, number: String): String {
|
fun convertNumberToPattern(pattern: PhoneNumberPattern, number: String): String {
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package com.prodhack.moscow2025.presentation.utils.ui
|
||||||
|
|
||||||
|
import androidx.compose.material3.SnackbarDuration
|
||||||
|
import androidx.compose.material3.SnackbarHostState
|
||||||
|
import androidx.compose.material3.SnackbarVisuals
|
||||||
|
|
||||||
|
enum class SnackbarStyle {
|
||||||
|
Success,
|
||||||
|
Error,
|
||||||
|
}
|
||||||
|
|
||||||
|
data class AppSnackbarVisuals(
|
||||||
|
override val message: String,
|
||||||
|
override val actionLabel: String? = null,
|
||||||
|
override val withDismissAction: Boolean = false,
|
||||||
|
override val duration: SnackbarDuration = SnackbarDuration.Short,
|
||||||
|
val style: SnackbarStyle = SnackbarStyle.Error
|
||||||
|
) : SnackbarVisuals
|
||||||
|
|
||||||
|
suspend fun SnackbarHostState.showSnackbar(
|
||||||
|
message: String,
|
||||||
|
style: SnackbarStyle,
|
||||||
|
actionLabel: String? = null,
|
||||||
|
withDismissAction: Boolean = false,
|
||||||
|
duration: SnackbarDuration = SnackbarDuration.Short
|
||||||
|
) = showSnackbar(
|
||||||
|
AppSnackbarVisuals(
|
||||||
|
message = message,
|
||||||
|
actionLabel = actionLabel,
|
||||||
|
withDismissAction = withDismissAction,
|
||||||
|
duration = duration,
|
||||||
|
style = style
|
||||||
|
)
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user