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 com.prodhack.moscow2025.presentation.components.TBottomNavigation
|
||||
import com.prodhack.moscow2025.presentation.theme.MoscowHackatonTemplateTheme
|
||||
import com.prodhack.moscow2025.presentation.utils.ui.AppSnackbarVisuals
|
||||
import com.prodhack.moscow2025.presentation.utils.ui.SnackbarStyle
|
||||
|
||||
@Composable
|
||||
fun TTasksApp(
|
||||
@@ -53,10 +55,19 @@ fun TTasksApp(
|
||||
SnackbarHost(
|
||||
hostState = snackbarHostState,
|
||||
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(
|
||||
snackbarData = data,
|
||||
containerColor = MaterialTheme.colorScheme.errorContainer,
|
||||
contentColor = MaterialTheme.colorScheme.onErrorContainer,
|
||||
containerColor = containerColor,
|
||||
contentColor = contentColor,
|
||||
shape = MaterialTheme.shapes.medium
|
||||
)
|
||||
}
|
||||
|
||||
@@ -94,7 +94,10 @@ fun TTasksNavHost(
|
||||
composable(AppDestination.Profile.route)
|
||||
{
|
||||
ProfileScreen(
|
||||
snackbarHostState = snackbarHostState
|
||||
snackbarHostState = snackbarHostState,
|
||||
navigateToLoginScreen = {
|
||||
navController.navigate(AppDestination.Login.route)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -95,9 +95,11 @@ class FillProfileViewModel(
|
||||
}
|
||||
|
||||
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 {
|
||||
it.copy(
|
||||
phone = value,
|
||||
phone = digits,
|
||||
errors = it.errors - AuthField.Phone
|
||||
)
|
||||
}
|
||||
|
||||
+146
-22
@@ -1,20 +1,36 @@
|
||||
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.Box
|
||||
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.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
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.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.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
@@ -24,27 +40,38 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
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.text.TextStyle
|
||||
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.sp
|
||||
import com.prodhack.moscow2025.R
|
||||
import com.prodhack.moscow2025.domain.usecase.auth.AuthField
|
||||
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.theme.Paddings
|
||||
import com.prodhack.moscow2025.presentation.theme.Shapes
|
||||
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.ui.SnackbarStyle
|
||||
import com.prodhack.moscow2025.presentation.utils.ui.showSnackbar
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ErrorCollectorScope.ProfileScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
navigateToLoginScreen: () -> Unit,
|
||||
viewModel: ProfileScreenViewModel = koinViewModel()
|
||||
) {
|
||||
val typography = androidx.compose.material3.MaterialTheme.typography
|
||||
val sheetState = rememberModalBottomSheetState()
|
||||
val isSheetOpen = remember { mutableStateOf(false) }
|
||||
|
||||
val formState by viewModel.formStateFillProfile.collectAsState()
|
||||
|
||||
@@ -71,6 +98,7 @@ fun ErrorCollectorScope.ProfileScreen(
|
||||
if (profileState is UIState.Success) {
|
||||
snackbarHostState.showSnackbar(
|
||||
message = "Данные профиля обновлены",
|
||||
style = SnackbarStyle.Success,
|
||||
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(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.imePadding()
|
||||
.systemBarsPadding()
|
||||
.padding(horizontal = 30.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Top
|
||||
@@ -113,7 +126,7 @@ fun ErrorCollectorScope.ProfileScreen(
|
||||
Text(
|
||||
text = "Профиль",
|
||||
style = typography.titleLarge,
|
||||
fontSize = 32.sp
|
||||
fontSize = 40.sp
|
||||
)
|
||||
Spacer(Modifier.height(20.dp))
|
||||
TTTextField(
|
||||
@@ -138,13 +151,65 @@ fun ErrorCollectorScope.ProfileScreen(
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email)
|
||||
)
|
||||
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(
|
||||
value = formState.phone,
|
||||
onValueChange = viewModel::onPhoneChange,
|
||||
label = "Телефон",
|
||||
error = formState.errors[AuthField.Phone],
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone)
|
||||
keyboardOptions = KeyboardOptions(
|
||||
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))
|
||||
BigButton(
|
||||
@@ -153,7 +218,66 @@ fun ErrorCollectorScope.ProfileScreen(
|
||||
buttonText = "Сохранить",
|
||||
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))
|
||||
}
|
||||
|
||||
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.net.Uri
|
||||
import android.provider.MediaStore
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.map
|
||||
import coil.ImageLoader
|
||||
import coil.request.ImageRequest
|
||||
import com.prodhack.moscow2025.data.data_providers.PhoneNumberPatternsProvider
|
||||
import com.prodhack.moscow2025.domain.interfaces.GalleryRepository
|
||||
import com.prodhack.moscow2025.domain.models.UpdateUserData
|
||||
import com.prodhack.moscow2025.domain.usecase.auth.AuthField
|
||||
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.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.base.BaseViewModel
|
||||
import com.prodhack.moscow2025.presentation.utils.convertNumberToPattern
|
||||
import com.prodhack.moscow2025.presentation.utils.toByteArray
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@@ -68,6 +76,8 @@ class ProfileScreenViewModel(
|
||||
private val getUserUseCase: GetUserUseCase,
|
||||
private val updateUserUseCase: UpdateUserUseCase,
|
||||
private val validateAuthFieldsUseCase: ValidateAuthFieldsUseCase,
|
||||
private val logOutUseCase: LogOutUseCase,
|
||||
private val getDefaultPhoneNumberPatternUseCase: GetDefaultPhoneNumberPatternUseCase,
|
||||
private val galleryRepository: GalleryRepository
|
||||
): BaseViewModel() {
|
||||
private val _formStateProfile = MutableStateFlow(ProfileState())
|
||||
@@ -76,6 +86,9 @@ class ProfileScreenViewModel(
|
||||
private val _profileState = MutableUIStateFlow<String>()
|
||||
val profileState: StateFlow<UIState<String>> = _profileState
|
||||
|
||||
val chosenPattern = mutableStateOf<UIPhoneNumberPattern?>(null)
|
||||
val phoneNumberPatterns = mutableStateListOf<UIPhoneNumberPattern>()
|
||||
|
||||
fun onEmailChange(value: String) {
|
||||
_formStateProfile.update {
|
||||
it.copy(
|
||||
@@ -104,9 +117,11 @@ class ProfileScreenViewModel(
|
||||
}
|
||||
|
||||
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 {
|
||||
it.copy(
|
||||
phone = value,
|
||||
phone = digits,
|
||||
errors = it.errors - AuthField.Phone
|
||||
)
|
||||
}
|
||||
@@ -167,38 +182,93 @@ class ProfileScreenViewModel(
|
||||
phone = _formStateProfile.value.phone
|
||||
)
|
||||
|
||||
if (!validation.isValid) {
|
||||
_formStateProfile.update { it.copy(errors = validation.errors) }
|
||||
val errors = validation.errors.toMutableMap()
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
_profileState.emit(UIState.Loading())
|
||||
|
||||
val formattedPhone = pattern?.mapToDomain()?.let { phonePattern ->
|
||||
convertNumberToPattern(phonePattern, _formStateProfile.value.phone)
|
||||
} ?: _formStateProfile.value.phone
|
||||
|
||||
val result = updateUserUseCase(
|
||||
UpdateUserData(
|
||||
firstName = _formStateProfile.value.firstName,
|
||||
lastName = _formStateProfile.value.lastName,
|
||||
email = _formStateProfile.value.email,
|
||||
phone = _formStateProfile.value.phone
|
||||
phone = formattedPhone
|
||||
)
|
||||
)
|
||||
result.map { it.id }.collectRequest(_profileState)
|
||||
}
|
||||
}
|
||||
|
||||
fun logout() {
|
||||
viewModelScope.launch {
|
||||
logOutUseCase()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
loadPhonePatterns()
|
||||
|
||||
val user = getUserUseCase().getOrNull()
|
||||
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 {
|
||||
it.copy(
|
||||
firstName = user.firstName.orEmpty(),
|
||||
lastName = user.lastName.orEmpty(),
|
||||
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 {
|
||||
@@ -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 {
|
||||
var noneDigitCount = 0
|
||||
var i = 0
|
||||
while (i < offset + noneDigitCount) {
|
||||
if (mask[i++] != numberChar) noneDigitCount++
|
||||
if (offset <= 0) return 0
|
||||
var digitsSeen = 0
|
||||
var index = 0
|
||||
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 =
|
||||
offset - mask.take(offset).count { it != numberChar }
|
||||
mask.take(offset.coerceAtMost(transformedLength)).count { it == numberChar }
|
||||
}
|
||||
|
||||
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