registration + auto login
BIN
app/src/main/ic_launcher-playstore.png
Normal file
|
After Width: | Height: | Size: 68 KiB |
@@ -7,12 +7,27 @@ import com.google.gson.annotations.SerializedName
|
|||||||
*/
|
*/
|
||||||
data class UserProfile(
|
data class UserProfile(
|
||||||
val id: Long = 0,
|
val id: Long = 0,
|
||||||
val username: String,
|
val username: String = "",
|
||||||
val email: String,
|
val email: String = "",
|
||||||
@SerializedName("first_name") val firstName: String,
|
@SerializedName("first_name") val firstName: String = "",
|
||||||
@SerializedName("last_name") val lastName: String,
|
@SerializedName("last_name") val lastName: String = "",
|
||||||
val phone: String,
|
val phone: String = "",
|
||||||
@SerializedName("user_id") val userId: String = "",
|
@SerializedName("user_id") val userId: String = "",
|
||||||
@SerializedName("created_at") val createdAt: String = "",
|
@SerializedName("created_at") val createdAt: String = "",
|
||||||
@SerializedName("is_verified") val isVerified: Boolean = false
|
@SerializedName("is_verified") val isVerified: Boolean = false,
|
||||||
|
|
||||||
|
// Дополнительные поля для экрана редактирования профиля
|
||||||
|
val bio: String? = null,
|
||||||
|
@SerializedName("date_of_birth") val date_of_birth: String? = null,
|
||||||
|
|
||||||
|
// Экстренные контакты
|
||||||
|
@SerializedName("emergency_contact_1_name") val emergency_contact_1_name: String? = null,
|
||||||
|
@SerializedName("emergency_contact_1_phone") val emergency_contact_1_phone: String? = null,
|
||||||
|
@SerializedName("emergency_contact_2_name") val emergency_contact_2_name: String? = null,
|
||||||
|
@SerializedName("emergency_contact_2_phone") val emergency_contact_2_phone: String? = null,
|
||||||
|
|
||||||
|
// Настройки уведомлений и доступа
|
||||||
|
@SerializedName("emergency_notifications_enabled") val emergency_notifications_enabled: Boolean? = false,
|
||||||
|
@SerializedName("location_sharing_enabled") val location_sharing_enabled: Boolean? = false,
|
||||||
|
@SerializedName("push_notifications_enabled") val push_notifications_enabled: Boolean? = false
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,91 +2,89 @@ package kr.smartsoltech.wellshe.ui.auth.compose
|
|||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Visibility
|
||||||
|
import androidx.compose.material.icons.filled.VisibilityOff
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
|
||||||
import kr.smartsoltech.wellshe.ui.auth.AuthViewModel
|
import kr.smartsoltech.wellshe.ui.auth.AuthViewModel
|
||||||
import kr.smartsoltech.wellshe.util.isValidEmail
|
|
||||||
import kr.smartsoltech.wellshe.util.isValidPassword
|
|
||||||
import kr.smartsoltech.wellshe.util.isValidPhone
|
|
||||||
|
|
||||||
|
@OptIn(ExperimentalComposeUiApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun RegisterScreen(
|
fun RegisterScreen(
|
||||||
onNavigateBack: () -> Unit,
|
onNavigateBack: () -> Unit,
|
||||||
onRegisterSuccess: () -> Unit,
|
onRegisterSuccess: (email: String, password: String) -> Unit,
|
||||||
viewModel: AuthViewModel = hiltViewModel()
|
viewModel: AuthViewModel = hiltViewModel()
|
||||||
) {
|
) {
|
||||||
val authState by viewModel.authState.observeAsState()
|
val authState by viewModel.authState.observeAsState()
|
||||||
val isLoading by viewModel.isLoading.observeAsState()
|
val isLoading by viewModel.isLoading.observeAsState()
|
||||||
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
|
|
||||||
var email by remember { mutableStateOf("") }
|
var email by remember { mutableStateOf("") }
|
||||||
var username by remember { mutableStateOf("") }
|
var username by remember { mutableStateOf("") }
|
||||||
var password by remember { mutableStateOf("") }
|
var password by remember { mutableStateOf("") }
|
||||||
var confirmPassword by remember { mutableStateOf("") }
|
var confirmPassword by remember { mutableStateOf("") }
|
||||||
var firstName by remember { mutableStateOf("") }
|
var fullName by remember { mutableStateOf("") }
|
||||||
var lastName by remember { mutableStateOf("") }
|
|
||||||
var phone by remember { mutableStateOf("") }
|
var phone by remember { mutableStateOf("") }
|
||||||
|
|
||||||
var emailError by remember { mutableStateOf<String?>(null) }
|
var passwordVisible by remember { mutableStateOf(false) }
|
||||||
var usernameError by remember { mutableStateOf<String?>(null) }
|
var confirmPasswordVisible by remember { mutableStateOf(false) }
|
||||||
var passwordError by remember { mutableStateOf<String?>(null) }
|
|
||||||
var confirmPasswordError by remember { mutableStateOf<String?>(null) }
|
|
||||||
var firstNameError by remember { mutableStateOf<String?>(null) }
|
|
||||||
var lastNameError by remember { mutableStateOf<String?>(null) }
|
|
||||||
var phoneError by remember { mutableStateOf<String?>(null) }
|
|
||||||
|
|
||||||
var isFormValid by remember { mutableStateOf(false) }
|
var isFormValid by remember { mutableStateOf(false) }
|
||||||
|
var passwordsMatch by remember { mutableStateOf(true) }
|
||||||
|
|
||||||
fun validateForm() {
|
// FocusRequesters для полей формы
|
||||||
// Проверка email
|
val usernameFocusRequester = remember { FocusRequester() }
|
||||||
emailError = if (!email.isValidEmail()) "Введите корректный email" else null
|
val passwordFocusRequester = remember { FocusRequester() }
|
||||||
|
val confirmPasswordFocusRequester = remember { FocusRequester() }
|
||||||
|
val fullNameFocusRequester = remember { FocusRequester() }
|
||||||
|
val phoneFocusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
// Проверка имени пользователя
|
// Валидация формы
|
||||||
usernameError = if (username.length < 3) "Имя пользователя должно быть не менее 3 символов" else null
|
LaunchedEffect(email, username, password, confirmPassword, fullName, phone) {
|
||||||
|
passwordsMatch = password == confirmPassword
|
||||||
// Проверка пароля
|
isFormValid = email.isNotBlank() && username.isNotBlank() &&
|
||||||
passwordError = if (!password.isValidPassword())
|
password.isNotBlank() && confirmPassword.isNotBlank() &&
|
||||||
"Пароль должен содержать не менее 8 символов, включая цифру, заглавную букву и специальный символ"
|
fullName.isNotBlank() && phone.isNotBlank() &&
|
||||||
else null
|
passwordsMatch &&
|
||||||
|
password.length >= 8 // Минимальная длина пароля
|
||||||
// Проверка совпадения паролей
|
|
||||||
confirmPasswordError = if (password != confirmPassword) "Пароли не совпадают" else null
|
|
||||||
|
|
||||||
// Проверка имени и фамилии
|
|
||||||
firstNameError = if (firstName.isEmpty()) "Введите имя" else null
|
|
||||||
lastNameError = if (lastName.isEmpty()) "Введите фамилию" else null
|
|
||||||
|
|
||||||
// Проверка телефона
|
|
||||||
phoneError = if (!phone.isValidPhone()) "Введите корректный номер телефона" else null
|
|
||||||
|
|
||||||
// Проверка заполнения всех полей
|
|
||||||
isFormValid = email.isNotEmpty() && username.isNotEmpty() &&
|
|
||||||
password.isNotEmpty() && confirmPassword.isNotEmpty() &&
|
|
||||||
firstName.isNotEmpty() && lastName.isNotEmpty() && phone.isNotEmpty() &&
|
|
||||||
emailError == null && usernameError == null && passwordError == null &&
|
|
||||||
confirmPasswordError == null && firstNameError == null &&
|
|
||||||
lastNameError == null && phoneError == null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверяем валидность формы при изменении любого поля
|
// Обработка состояния авторизации
|
||||||
LaunchedEffect(email, username, password, confirmPassword, firstName, lastName, phone) {
|
|
||||||
validateForm()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Обработка успешной регистрации
|
|
||||||
LaunchedEffect(authState) {
|
LaunchedEffect(authState) {
|
||||||
if (authState is AuthViewModel.AuthState.RegistrationSuccess) {
|
if (authState is AuthViewModel.AuthState.RegistrationSuccess) {
|
||||||
onRegisterSuccess()
|
// При успешной регистрации сразу выполняем вход с теми же данными
|
||||||
|
onRegisterSuccess(email, password)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция для выполнения регистрации
|
||||||
|
val performRegister = {
|
||||||
|
if (isFormValid && isLoading != true) {
|
||||||
|
keyboardController?.hide()
|
||||||
|
// Разделяем полное имя на имя и фамилию для сервера
|
||||||
|
val nameParts = fullName.trim().split(" ", limit = 2)
|
||||||
|
val firstName = nameParts[0]
|
||||||
|
val lastName = if (nameParts.size > 1) nameParts[1] else ""
|
||||||
|
|
||||||
|
viewModel.register(email, username, password, firstName, lastName, phone)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,96 +96,164 @@ fun RegisterScreen(
|
|||||||
.padding(16.dp)
|
.padding(16.dp)
|
||||||
.verticalScroll(scrollState),
|
.verticalScroll(scrollState),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Регистрация",
|
text = "Регистрация",
|
||||||
style = MaterialTheme.typography.headlineMedium,
|
style = MaterialTheme.typography.headlineLarge
|
||||||
modifier = Modifier.padding(bottom = 16.dp)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Поле Email
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = email,
|
value = email,
|
||||||
onValueChange = { email = it },
|
onValueChange = { email = it },
|
||||||
label = { Text("Email") },
|
label = { Text("Email") },
|
||||||
isError = emailError != null,
|
|
||||||
supportingText = { emailError?.let { Text(it) } },
|
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email)
|
keyboardOptions = KeyboardOptions(
|
||||||
|
keyboardType = KeyboardType.Email,
|
||||||
|
imeAction = ImeAction.Next
|
||||||
|
),
|
||||||
|
keyboardActions = KeyboardActions(
|
||||||
|
onNext = { usernameFocusRequester.requestFocus() }
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Поле Username
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = username,
|
value = username,
|
||||||
onValueChange = { username = it },
|
onValueChange = { username = it },
|
||||||
label = { Text("Имя пользователя") },
|
label = { Text("Имя пользователя") },
|
||||||
isError = usernameError != null,
|
|
||||||
supportingText = { usernameError?.let { Text(it) } },
|
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.focusRequester(usernameFocusRequester),
|
||||||
|
keyboardOptions = KeyboardOptions(
|
||||||
|
keyboardType = KeyboardType.Text,
|
||||||
|
imeAction = ImeAction.Next
|
||||||
|
),
|
||||||
|
keyboardActions = KeyboardActions(
|
||||||
|
onNext = { passwordFocusRequester.requestFocus() }
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Поле Password
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = password,
|
value = password,
|
||||||
onValueChange = { password = it },
|
onValueChange = { password = it },
|
||||||
label = { Text("Пароль") },
|
label = { Text("Пароль") },
|
||||||
isError = passwordError != null,
|
|
||||||
supportingText = { passwordError?.let { Text(it) } },
|
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
visualTransformation = PasswordVisualTransformation(),
|
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password)
|
.fillMaxWidth()
|
||||||
|
.focusRequester(passwordFocusRequester),
|
||||||
|
keyboardOptions = KeyboardOptions(
|
||||||
|
keyboardType = KeyboardType.Password,
|
||||||
|
imeAction = ImeAction.Next
|
||||||
|
),
|
||||||
|
keyboardActions = KeyboardActions(
|
||||||
|
onNext = { confirmPasswordFocusRequester.requestFocus() }
|
||||||
|
),
|
||||||
|
trailingIcon = {
|
||||||
|
IconButton(onClick = { passwordVisible = !passwordVisible }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = if (passwordVisible) Icons.Filled.Visibility else Icons.Filled.VisibilityOff,
|
||||||
|
contentDescription = if (passwordVisible) "Скрыть пароль" else "Показать пароль"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isError = password.isNotEmpty() && password.length < 8
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (password.isNotEmpty() && password.length < 8) {
|
||||||
|
Text(
|
||||||
|
text = "Пароль должен содержать минимум 8 символов",
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
modifier = Modifier.align(Alignment.Start)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Поле для подтверждения пароля
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = confirmPassword,
|
value = confirmPassword,
|
||||||
onValueChange = { confirmPassword = it },
|
onValueChange = { confirmPassword = it },
|
||||||
label = { Text("Подтвердите пароль") },
|
label = { Text("Подтвердите пароль") },
|
||||||
isError = confirmPasswordError != null,
|
|
||||||
supportingText = { confirmPasswordError?.let { Text(it) } },
|
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
visualTransformation = PasswordVisualTransformation(),
|
visualTransformation = if (confirmPasswordVisible) VisualTransformation.None else PasswordVisualTransformation(),
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password)
|
.fillMaxWidth()
|
||||||
|
.focusRequester(confirmPasswordFocusRequester),
|
||||||
|
keyboardOptions = KeyboardOptions(
|
||||||
|
keyboardType = KeyboardType.Password,
|
||||||
|
imeAction = ImeAction.Next
|
||||||
|
),
|
||||||
|
keyboardActions = KeyboardActions(
|
||||||
|
onNext = { fullNameFocusRequester.requestFocus() }
|
||||||
|
),
|
||||||
|
trailingIcon = {
|
||||||
|
IconButton(onClick = { confirmPasswordVisible = !confirmPasswordVisible }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = if (confirmPasswordVisible) Icons.Filled.Visibility else Icons.Filled.VisibilityOff,
|
||||||
|
contentDescription = if (confirmPasswordVisible) "Скрыть пароль" else "Показать пароль"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isError = confirmPassword.isNotEmpty() && !passwordsMatch
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (confirmPassword.isNotEmpty() && !passwordsMatch) {
|
||||||
|
Text(
|
||||||
|
text = "Пароли не совпадают",
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
modifier = Modifier.align(Alignment.Start)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Поле для полного имени
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = firstName,
|
value = fullName,
|
||||||
onValueChange = { firstName = it },
|
onValueChange = { fullName = it },
|
||||||
label = { Text("Имя") },
|
label = { Text("Полное имя") },
|
||||||
isError = firstNameError != null,
|
|
||||||
supportingText = { firstNameError?.let { Text(it) } },
|
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier
|
||||||
)
|
.fillMaxWidth()
|
||||||
|
.focusRequester(fullNameFocusRequester),
|
||||||
OutlinedTextField(
|
keyboardOptions = KeyboardOptions(
|
||||||
value = lastName,
|
keyboardType = KeyboardType.Text,
|
||||||
onValueChange = { lastName = it },
|
imeAction = ImeAction.Next
|
||||||
label = { Text("Фамилия") },
|
),
|
||||||
isError = lastNameError != null,
|
keyboardActions = KeyboardActions(
|
||||||
supportingText = { lastNameError?.let { Text(it) } },
|
onNext = { phoneFocusRequester.requestFocus() }
|
||||||
singleLine = true,
|
)
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Поле для телефона
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = phone,
|
value = phone,
|
||||||
onValueChange = { phone = it },
|
onValueChange = { phone = it },
|
||||||
label = { Text("Телефон") },
|
label = { Text("Номер телефона") },
|
||||||
isError = phoneError != null,
|
|
||||||
supportingText = { phoneError?.let { Text(it) } },
|
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone)
|
.fillMaxWidth()
|
||||||
|
.focusRequester(phoneFocusRequester),
|
||||||
|
keyboardOptions = KeyboardOptions(
|
||||||
|
keyboardType = KeyboardType.Phone,
|
||||||
|
imeAction = ImeAction.Done
|
||||||
|
),
|
||||||
|
keyboardActions = KeyboardActions(
|
||||||
|
onDone = {
|
||||||
|
performRegister()
|
||||||
|
}
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
// Кнопка регистрации
|
||||||
Button(
|
Button(
|
||||||
onClick = {
|
onClick = { performRegister() },
|
||||||
viewModel.register(email, username, password, firstName, lastName, phone)
|
|
||||||
},
|
|
||||||
enabled = isFormValid && isLoading != true,
|
enabled = isFormValid && isLoading != true,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -203,18 +269,16 @@ fun RegisterScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TextButton(
|
// Кнопка возврата к экрану входа
|
||||||
onClick = onNavigateBack,
|
TextButton(onClick = onNavigateBack) {
|
||||||
modifier = Modifier.fillMaxWidth()
|
Text("Уже есть аккаунт? Войти")
|
||||||
) {
|
|
||||||
Text("Вернуться к входу")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Отображение ошибки
|
||||||
if (authState is AuthViewModel.AuthState.RegistrationError) {
|
if (authState is AuthViewModel.AuthState.RegistrationError) {
|
||||||
Text(
|
Text(
|
||||||
text = (authState as AuthViewModel.AuthState.RegistrationError).message,
|
text = (authState as AuthViewModel.AuthState.RegistrationError).message,
|
||||||
color = MaterialTheme.colorScheme.error,
|
color = MaterialTheme.colorScheme.error
|
||||||
modifier = Modifier.padding(top = 8.dp)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package kr.smartsoltech.wellshe.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PermissionRequestDialog(
|
||||||
|
permissionText: String,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onConfirm: () -> Unit
|
||||||
|
) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
title = { Text("Требуется разрешение") },
|
||||||
|
text = { Text(permissionText) },
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = onConfirm) {
|
||||||
|
Text("Настройки")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = onDismiss) {
|
||||||
|
Text("Отмена")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -46,8 +46,13 @@ fun AppNavGraph(
|
|||||||
onNavigateBack = {
|
onNavigateBack = {
|
||||||
navController.popBackStack()
|
navController.popBackStack()
|
||||||
},
|
},
|
||||||
onRegisterSuccess = {
|
onRegisterSuccess = { email, password ->
|
||||||
navController.popBackStack()
|
// После успешной регистрации автоматически входим в систему с новыми учетными данными
|
||||||
|
authViewModel.login(email, password, true)
|
||||||
|
// Переходим на главный экран
|
||||||
|
navController.navigate(BottomNavItem.Cycle.route) {
|
||||||
|
popUpTo("login") { inclusive = true }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,427 @@
|
|||||||
|
package kr.smartsoltech.wellshe.ui.profile
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.net.Uri
|
||||||
|
import android.provider.ContactsContract
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.*
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import java.time.LocalDate
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
import kr.smartsoltech.wellshe.model.auth.UserProfile
|
||||||
|
import kr.smartsoltech.wellshe.ui.components.PermissionRequestDialog
|
||||||
|
import kr.smartsoltech.wellshe.ui.profile.viewmodel.ProfileViewModel
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun ProfileEditScreen(
|
||||||
|
onNavigateBack: () -> Unit,
|
||||||
|
viewModel: ProfileViewModel = hiltViewModel()
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val scrollState = rememberScrollState()
|
||||||
|
|
||||||
|
// Состояние для профиля пользователя
|
||||||
|
var userProfile by remember { mutableStateOf(viewModel.userProfile.value ?: UserProfile()) }
|
||||||
|
|
||||||
|
// Состояния для показа диалогов выбора даты и разрешений
|
||||||
|
var showDatePicker by remember { mutableStateOf(false) }
|
||||||
|
var showContactPermissionDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
// Состояние для обработки запроса разрешений на контакты
|
||||||
|
var contactType by remember { mutableStateOf(ContactType.NONE) }
|
||||||
|
|
||||||
|
// Launcher для выбора контакта
|
||||||
|
val contactPickerLauncher = rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.StartActivityForResult()
|
||||||
|
) { result ->
|
||||||
|
if (result.resultCode == Activity.RESULT_OK) {
|
||||||
|
result.data?.data?.let { contactUri ->
|
||||||
|
// Обрабатываем выбранный контакт
|
||||||
|
val projection = arrayOf(
|
||||||
|
ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,
|
||||||
|
ContactsContract.CommonDataKinds.Phone.NUMBER
|
||||||
|
)
|
||||||
|
|
||||||
|
context.contentResolver.query(contactUri, projection, null, null, null)?.use { cursor ->
|
||||||
|
if (cursor.moveToFirst()) {
|
||||||
|
val nameIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME)
|
||||||
|
val numberIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER)
|
||||||
|
|
||||||
|
val name = cursor.getString(nameIndex)
|
||||||
|
val phone = cursor.getString(numberIndex)
|
||||||
|
|
||||||
|
// Обновляем профиль в зависимости от типа контакта
|
||||||
|
when (contactType) {
|
||||||
|
ContactType.EMERGENCY_1 -> {
|
||||||
|
userProfile = userProfile.copy(
|
||||||
|
emergency_contact_1_name = name,
|
||||||
|
emergency_contact_1_phone = phone
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ContactType.EMERGENCY_2 -> {
|
||||||
|
userProfile = userProfile.copy(
|
||||||
|
emergency_contact_2_name = name,
|
||||||
|
emergency_contact_2_phone = phone
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запрос разрешения на доступ к контактам
|
||||||
|
val contactPermissionLauncher = rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.RequestPermission()
|
||||||
|
) { isGranted ->
|
||||||
|
if (isGranted) {
|
||||||
|
// Если разрешение получено, запускаем выбор контакта
|
||||||
|
val intent = Intent(Intent.ACTION_PICK).apply {
|
||||||
|
setType(ContactsContract.CommonDataKinds.Phone.CONTENT_TYPE)
|
||||||
|
}
|
||||||
|
contactPickerLauncher.launch(intent)
|
||||||
|
} else {
|
||||||
|
// Если разрешение не получено, показываем диалог
|
||||||
|
showContactPermissionDialog = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверка и запрос разрешения на доступ к контактам
|
||||||
|
fun checkAndRequestContactPermission(type: ContactType) {
|
||||||
|
contactType = type
|
||||||
|
|
||||||
|
when (PackageManager.PERMISSION_GRANTED) {
|
||||||
|
ContextCompat.checkSelfPermission(
|
||||||
|
context,
|
||||||
|
Manifest.permission.READ_CONTACTS
|
||||||
|
) -> {
|
||||||
|
// Разрешение уже есть, запускаем выбор контакта
|
||||||
|
val intent = Intent(Intent.ACTION_PICK).apply {
|
||||||
|
setType(ContactsContract.CommonDataKinds.Phone.CONTENT_TYPE)
|
||||||
|
}
|
||||||
|
contactPickerLauncher.launch(intent)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
// Запрашиваем разрешение
|
||||||
|
contactPermissionLauncher.launch(Manifest.permission.READ_CONTACTS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Диалог выбора даты рождения
|
||||||
|
if (showDatePicker) {
|
||||||
|
val currentDate = userProfile.date_of_birth?.let {
|
||||||
|
try {
|
||||||
|
LocalDate.parse(it)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LocalDate.now().minusYears(20)
|
||||||
|
}
|
||||||
|
} ?: LocalDate.now().minusYears(20)
|
||||||
|
|
||||||
|
DatePickerDialog(
|
||||||
|
onDismissRequest = { showDatePicker = false },
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
val selectedDate = LocalDate.of(
|
||||||
|
viewModel.datePickerState.selectedDateMillis?.let { millis ->
|
||||||
|
java.time.Instant.ofEpochMilli(millis).atZone(java.time.ZoneId.systemDefault()).toLocalDate().year
|
||||||
|
} ?: currentDate.year,
|
||||||
|
viewModel.datePickerState.selectedDateMillis?.let { millis ->
|
||||||
|
java.time.Instant.ofEpochMilli(millis).atZone(java.time.ZoneId.systemDefault()).toLocalDate().monthValue
|
||||||
|
} ?: currentDate.monthValue,
|
||||||
|
viewModel.datePickerState.selectedDateMillis?.let { millis ->
|
||||||
|
java.time.Instant.ofEpochMilli(millis).atZone(java.time.ZoneId.systemDefault()).toLocalDate().dayOfMonth
|
||||||
|
} ?: currentDate.dayOfMonth
|
||||||
|
)
|
||||||
|
|
||||||
|
userProfile = userProfile.copy(
|
||||||
|
date_of_birth = selectedDate.format(DateTimeFormatter.ISO_LOCAL_DATE)
|
||||||
|
)
|
||||||
|
showDatePicker = false
|
||||||
|
}) {
|
||||||
|
Text("OK")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = { showDatePicker = false }) {
|
||||||
|
Text("Отмена")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
DatePicker(state = viewModel.datePickerState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Диалог запроса разрешения на контакты
|
||||||
|
if (showContactPermissionDialog) {
|
||||||
|
PermissionRequestDialog(
|
||||||
|
permissionText = "Для выбора экстренного контакта необходимо разрешение на доступ к контактам.",
|
||||||
|
onDismiss = { showContactPermissionDialog = false },
|
||||||
|
onConfirm = {
|
||||||
|
val intent = Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
||||||
|
data = Uri.fromParts("package", context.packageName, null)
|
||||||
|
}
|
||||||
|
context.startActivity(intent)
|
||||||
|
showContactPermissionDialog = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = { Text("Редактирование профиля") },
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = onNavigateBack) {
|
||||||
|
Icon(Icons.Filled.ArrowBack, contentDescription = "Назад")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
IconButton(onClick = {
|
||||||
|
viewModel.updateUserProfile(userProfile)
|
||||||
|
onNavigateBack()
|
||||||
|
}) {
|
||||||
|
Icon(Icons.Filled.Save, contentDescription = "Сохранить")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { paddingValues ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(paddingValues)
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.verticalScroll(scrollState),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
// Раздел с основной информацией
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surface
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Личная информация",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = userProfile.bio ?: "",
|
||||||
|
onValueChange = { userProfile = userProfile.copy(bio = it) },
|
||||||
|
label = { Text("О себе") },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
minLines = 3,
|
||||||
|
maxLines = 5
|
||||||
|
)
|
||||||
|
|
||||||
|
// Дата рождения
|
||||||
|
OutlinedTextField(
|
||||||
|
value = userProfile.date_of_birth ?: "",
|
||||||
|
onValueChange = {},
|
||||||
|
label = { Text("Дата рождения") },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
readOnly = true,
|
||||||
|
trailingIcon = {
|
||||||
|
IconButton(onClick = { showDatePicker = true }) {
|
||||||
|
Icon(Icons.Filled.DateRange, contentDescription = "Выбрать дату")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Раздел с экстренными контактами
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surface
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Экстренные контакты",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
|
||||||
|
// Первый экстренный контакт
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Первый контакт",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = if (userProfile.emergency_contact_1_name.isNullOrBlank()) "Не выбрано"
|
||||||
|
else "${userProfile.emergency_contact_1_name}\n${userProfile.emergency_contact_1_phone}",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = { checkAndRequestContactPermission(ContactType.EMERGENCY_1) },
|
||||||
|
modifier = Modifier.padding(start = 8.dp)
|
||||||
|
) {
|
||||||
|
Icon(Icons.Filled.Contacts, contentDescription = "Выбрать контакт")
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text("Выбрать")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Второй экстренный контакт
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Второй контакт",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = if (userProfile.emergency_contact_2_name.isNullOrBlank()) "Не выбрано"
|
||||||
|
else "${userProfile.emergency_contact_2_name}\n${userProfile.emergency_contact_2_phone}",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = { checkAndRequestContactPermission(ContactType.EMERGENCY_2) },
|
||||||
|
modifier = Modifier.padding(start = 8.dp)
|
||||||
|
) {
|
||||||
|
Icon(Icons.Filled.Contacts, contentDescription = "Выбрать контакт")
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text("Выбрать")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Раздел с настройками уведомлений и доступа
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surface
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Настройки и разрешения",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
|
||||||
|
// Включение/выключение экстренных уведомлений
|
||||||
|
SwitchRow(
|
||||||
|
title = "Экстренные уведомления",
|
||||||
|
checked = userProfile.emergency_notifications_enabled ?: false,
|
||||||
|
onCheckedChange = {
|
||||||
|
userProfile = userProfile.copy(emergency_notifications_enabled = it)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Включение/выключение обмена местоположением
|
||||||
|
SwitchRow(
|
||||||
|
title = "Обмен местоположением",
|
||||||
|
checked = userProfile.location_sharing_enabled ?: false,
|
||||||
|
onCheckedChange = {
|
||||||
|
userProfile = userProfile.copy(location_sharing_enabled = it)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Включение/выключение push-уведомлений
|
||||||
|
SwitchRow(
|
||||||
|
title = "Push-уведомления",
|
||||||
|
checked = userProfile.push_notifications_enabled ?: false,
|
||||||
|
onCheckedChange = {
|
||||||
|
userProfile = userProfile.copy(push_notifications_enabled = it)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SwitchRow(
|
||||||
|
title: String,
|
||||||
|
checked: Boolean,
|
||||||
|
onCheckedChange: (Boolean) -> Unit
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
|
||||||
|
Switch(
|
||||||
|
checked = checked,
|
||||||
|
onCheckedChange = onCheckedChange
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Перечисление типов контактов
|
||||||
|
enum class ContactType {
|
||||||
|
NONE,
|
||||||
|
EMERGENCY_1,
|
||||||
|
EMERGENCY_2
|
||||||
|
}
|
||||||
@@ -60,9 +60,9 @@ fun ProfileScreen(
|
|||||||
var integrations by remember {
|
var integrations by remember {
|
||||||
mutableStateOf(
|
mutableStateOf(
|
||||||
listOf(
|
listOf(
|
||||||
Integration("Google Fit", false),
|
Integration("Google Fit", true),
|
||||||
Integration("FatSecret Proxy", true),
|
Integration("FatSecret Proxy", false),
|
||||||
Integration("Wear OS", false)
|
Integration("Wear OS", true)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -106,7 +106,7 @@ fun ProfileScreen(
|
|||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Цель воды (мл)",
|
text = "Цель воды в сутки (мл)",
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package kr.smartsoltech.wellshe.ui.profile.viewmodel
|
||||||
|
|
||||||
|
import androidx.compose.material3.DatePickerState
|
||||||
|
import androidx.compose.material3.DisplayMode
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kr.smartsoltech.wellshe.model.auth.UserProfile
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.LocalDate
|
||||||
|
import java.time.ZoneId
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class ProfileViewModel @Inject constructor() : ViewModel() {
|
||||||
|
|
||||||
|
private val _userProfile = MutableStateFlow<UserProfile?>(null)
|
||||||
|
val userProfile: StateFlow<UserProfile?> = _userProfile.asStateFlow()
|
||||||
|
|
||||||
|
private val _isLoading = MutableStateFlow(false)
|
||||||
|
val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()
|
||||||
|
|
||||||
|
private val _error = MutableStateFlow<String?>(null)
|
||||||
|
val error: StateFlow<String?> = _error.asStateFlow()
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
val datePickerState = DatePickerState(
|
||||||
|
initialSelectedDateMillis = LocalDate.now().minusYears(20).atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli(),
|
||||||
|
initialDisplayedMonthMillis = LocalDate.now().minusYears(20).atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli(),
|
||||||
|
yearRange = IntRange(1900, LocalDate.now().year),
|
||||||
|
initialDisplayMode = DisplayMode.Picker,
|
||||||
|
locale = java.util.Locale.getDefault()
|
||||||
|
)
|
||||||
|
|
||||||
|
init {
|
||||||
|
loadUserProfile()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadUserProfile() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_isLoading.value = true
|
||||||
|
try {
|
||||||
|
// Здесь должен быть запрос к API или базе данных для получения профиля пользователя
|
||||||
|
// Пока используем заполнитель с фиктивными данными
|
||||||
|
_userProfile.value = UserProfile(
|
||||||
|
username = "user123",
|
||||||
|
email = "user@example.com",
|
||||||
|
firstName = "Иван",
|
||||||
|
lastName = "Иванов",
|
||||||
|
phone = "+7 (999) 123-45-67"
|
||||||
|
)
|
||||||
|
_error.value = null
|
||||||
|
} catch (e: Exception) {
|
||||||
|
_error.value = "Не удалось загрузить профиль: ${e.message}"
|
||||||
|
} finally {
|
||||||
|
_isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateUserProfile(profile: UserProfile) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_isLoading.value = true
|
||||||
|
try {
|
||||||
|
// Здесь должен быть запрос к API или базе данных для обновления профиля
|
||||||
|
// Пока просто обновляем локальное состояние
|
||||||
|
_userProfile.value = profile
|
||||||
|
_error.value = null
|
||||||
|
} catch (e: Exception) {
|
||||||
|
_error.value = "Не удалось обновить профиль: ${e.message}"
|
||||||
|
} finally {
|
||||||
|
_isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearError() {
|
||||||
|
_error.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,170 +1,74 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector
|
||||||
android:width="108dp"
|
|
||||||
android:height="108dp"
|
android:height="108dp"
|
||||||
|
android:width="108dp"
|
||||||
|
android:viewportHeight="108"
|
||||||
android:viewportWidth="108"
|
android:viewportWidth="108"
|
||||||
android:viewportHeight="108">
|
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<path
|
<path android:fillColor="#3DDC84"
|
||||||
android:fillColor="#3DDC84"
|
android:pathData="M0,0h108v108h-108z"/>
|
||||||
android:pathData="M0,0h108v108h-108z" />
|
<path android:fillColor="#00000000" android:pathData="M9,0L9,108"
|
||||||
<path
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:fillColor="#00000000"
|
<path android:fillColor="#00000000" android:pathData="M19,0L19,108"
|
||||||
android:pathData="M9,0L9,108"
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:strokeWidth="0.8"
|
<path android:fillColor="#00000000" android:pathData="M29,0L29,108"
|
||||||
android:strokeColor="#33FFFFFF" />
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
<path
|
<path android:fillColor="#00000000" android:pathData="M39,0L39,108"
|
||||||
android:fillColor="#00000000"
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:pathData="M19,0L19,108"
|
<path android:fillColor="#00000000" android:pathData="M49,0L49,108"
|
||||||
android:strokeWidth="0.8"
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:strokeColor="#33FFFFFF" />
|
<path android:fillColor="#00000000" android:pathData="M59,0L59,108"
|
||||||
<path
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:fillColor="#00000000"
|
<path android:fillColor="#00000000" android:pathData="M69,0L69,108"
|
||||||
android:pathData="M29,0L29,108"
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:strokeWidth="0.8"
|
<path android:fillColor="#00000000" android:pathData="M79,0L79,108"
|
||||||
android:strokeColor="#33FFFFFF" />
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
<path
|
<path android:fillColor="#00000000" android:pathData="M89,0L89,108"
|
||||||
android:fillColor="#00000000"
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:pathData="M39,0L39,108"
|
<path android:fillColor="#00000000" android:pathData="M99,0L99,108"
|
||||||
android:strokeWidth="0.8"
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:strokeColor="#33FFFFFF" />
|
<path android:fillColor="#00000000" android:pathData="M0,9L108,9"
|
||||||
<path
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:fillColor="#00000000"
|
<path android:fillColor="#00000000" android:pathData="M0,19L108,19"
|
||||||
android:pathData="M49,0L49,108"
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:strokeWidth="0.8"
|
<path android:fillColor="#00000000" android:pathData="M0,29L108,29"
|
||||||
android:strokeColor="#33FFFFFF" />
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
<path
|
<path android:fillColor="#00000000" android:pathData="M0,39L108,39"
|
||||||
android:fillColor="#00000000"
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:pathData="M59,0L59,108"
|
<path android:fillColor="#00000000" android:pathData="M0,49L108,49"
|
||||||
android:strokeWidth="0.8"
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:strokeColor="#33FFFFFF" />
|
<path android:fillColor="#00000000" android:pathData="M0,59L108,59"
|
||||||
<path
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:fillColor="#00000000"
|
<path android:fillColor="#00000000" android:pathData="M0,69L108,69"
|
||||||
android:pathData="M69,0L69,108"
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:strokeWidth="0.8"
|
<path android:fillColor="#00000000" android:pathData="M0,79L108,79"
|
||||||
android:strokeColor="#33FFFFFF" />
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
<path
|
<path android:fillColor="#00000000" android:pathData="M0,89L108,89"
|
||||||
android:fillColor="#00000000"
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:pathData="M79,0L79,108"
|
<path android:fillColor="#00000000" android:pathData="M0,99L108,99"
|
||||||
android:strokeWidth="0.8"
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:strokeColor="#33FFFFFF" />
|
<path android:fillColor="#00000000" android:pathData="M19,29L89,29"
|
||||||
<path
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:fillColor="#00000000"
|
<path android:fillColor="#00000000" android:pathData="M19,39L89,39"
|
||||||
android:pathData="M89,0L89,108"
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:strokeWidth="0.8"
|
<path android:fillColor="#00000000" android:pathData="M19,49L89,49"
|
||||||
android:strokeColor="#33FFFFFF" />
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
<path
|
<path android:fillColor="#00000000" android:pathData="M19,59L89,59"
|
||||||
android:fillColor="#00000000"
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:pathData="M99,0L99,108"
|
<path android:fillColor="#00000000" android:pathData="M19,69L89,69"
|
||||||
android:strokeWidth="0.8"
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:strokeColor="#33FFFFFF" />
|
<path android:fillColor="#00000000" android:pathData="M19,79L89,79"
|
||||||
<path
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:fillColor="#00000000"
|
<path android:fillColor="#00000000" android:pathData="M29,19L29,89"
|
||||||
android:pathData="M0,9L108,9"
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:strokeWidth="0.8"
|
<path android:fillColor="#00000000" android:pathData="M39,19L39,89"
|
||||||
android:strokeColor="#33FFFFFF" />
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
<path
|
<path android:fillColor="#00000000" android:pathData="M49,19L49,89"
|
||||||
android:fillColor="#00000000"
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:pathData="M0,19L108,19"
|
<path android:fillColor="#00000000" android:pathData="M59,19L59,89"
|
||||||
android:strokeWidth="0.8"
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:strokeColor="#33FFFFFF" />
|
<path android:fillColor="#00000000" android:pathData="M69,19L69,89"
|
||||||
<path
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:fillColor="#00000000"
|
<path android:fillColor="#00000000" android:pathData="M79,19L79,89"
|
||||||
android:pathData="M0,29L108,29"
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,39L108,39"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,49L108,49"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,59L108,59"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,69L108,69"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,79L108,79"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,89L108,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,99L108,99"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,29L89,29"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,39L89,39"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,49L89,49"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,59L89,59"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,69L89,69"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,79L89,79"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M29,19L29,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M39,19L39,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M49,19L49,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M59,19L59,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M69,19L69,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M79,19L79,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@drawable/ic_launcher_background" />
|
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@drawable/ic_launcher_background" />
|
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 3.3 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 982 B After Width: | Height: | Size: 2.4 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 5.1 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 7.4 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 10 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 16 KiB |