diff --git a/app/src/main/java/com/novayaplaneta/MainActivity.kt b/app/src/main/java/com/novayaplaneta/MainActivity.kt index ea29ae5..8ffbcc5 100644 --- a/app/src/main/java/com/novayaplaneta/MainActivity.kt +++ b/app/src/main/java/com/novayaplaneta/MainActivity.kt @@ -28,39 +28,14 @@ class MainActivity : ComponentActivity() { val navBackStackEntry by navController.currentBackStackEntryAsState() val currentRoute = navBackStackEntry?.destination?.route - // Нижняя панель не показывается на экранах входа, регистрации и восстановления пароля - val showBottomBar = currentRoute != null && - currentRoute != "login" && - currentRoute != "registration" && - currentRoute != "forgot_password" - + // Нижняя панель скрыта везде (используется левая панель навигации) Scaffold( - modifier = Modifier.fillMaxSize(), - bottomBar = { - if (showBottomBar) { - BottomNavigationBar( - currentRoute = currentRoute, - onNavigate = { route -> - navController.navigate(route) { - popUpTo(navController.graph.startDestinationId) { - saveState = true - } - launchSingleTop = true - restoreState = true - } - } - ) - } - } + modifier = Modifier.fillMaxSize() ) { innerPadding -> NewPlanetNavigation( navController = navController, modifier = Modifier .fillMaxSize() - .padding( - // Убираем нижний отступ на экране входа - bottom = if (showBottomBar) innerPadding.calculateBottomPadding() else 0.dp - ) ) } } diff --git a/app/src/main/java/com/novayaplaneta/ui/navigation/NewPlanetNavigation.kt b/app/src/main/java/com/novayaplaneta/ui/navigation/NewPlanetNavigation.kt index b950128..d368e22 100644 --- a/app/src/main/java/com/novayaplaneta/ui/navigation/NewPlanetNavigation.kt +++ b/app/src/main/java/com/novayaplaneta/ui/navigation/NewPlanetNavigation.kt @@ -36,7 +36,7 @@ fun NewPlanetNavigation( ForgotPasswordScreen(navController = navController) } composable("schedule") { - ScheduleScreen() + ScheduleScreen(navController = navController) } composable("tasks") { TaskScreen() @@ -45,13 +45,13 @@ fun NewPlanetNavigation( TimerScreen() } composable("rewards") { - RewardsScreen() + RewardsScreen(navController = navController) } composable("ai") { AIScreen() } composable("settings") { - SettingsScreen() + SettingsScreen(navController = navController) } } } diff --git a/app/src/main/java/com/novayaplaneta/ui/screens/auth/EmailValidator.kt b/app/src/main/java/com/novayaplaneta/ui/screens/auth/EmailValidator.kt new file mode 100644 index 0000000..c73a3f4 --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/ui/screens/auth/EmailValidator.kt @@ -0,0 +1,12 @@ +package com.novayaplaneta.ui.screens.auth + +object EmailValidator { + // Стандартный паттерн для валидации email + private val EMAIL_PATTERN = android.util.Patterns.EMAIL_ADDRESS + + fun isValid(email: String): Boolean { + return email.isNotBlank() && EMAIL_PATTERN.matcher(email).matches() + } +} + + diff --git a/app/src/main/java/com/novayaplaneta/ui/screens/auth/ForgotPasswordScreen.kt b/app/src/main/java/com/novayaplaneta/ui/screens/auth/ForgotPasswordScreen.kt new file mode 100644 index 0000000..0ec783a --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/ui/screens/auth/ForgotPasswordScreen.kt @@ -0,0 +1,379 @@ +package com.novayaplaneta.ui.screens.auth + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavController +import com.novayaplaneta.ui.components.NovayaPlanetaLogo +import com.novayaplaneta.ui.theme.* +import kotlin.math.min + +@Composable +fun ForgotPasswordScreen( + navController: NavController, + viewModel: ForgotPasswordViewModel = hiltViewModel(), + modifier: Modifier = Modifier +) { + val uiState by viewModel.uiState.collectAsState() + val snackbarHostState = remember { SnackbarHostState() } + + LaunchedEffect(uiState.errorMessage) { + val errorMsg = uiState.errorMessage + if (errorMsg != null) { + snackbarHostState.showSnackbar( + message = errorMsg, + actionLabel = null + ) + } + } + + Scaffold( + snackbarHost = { + SnackbarHost( + hostState = snackbarHostState, + snackbar = { snackbarData -> + Snackbar( + snackbarData = snackbarData, + containerColor = MaterialTheme.colorScheme.error, + contentColor = Color.White + ) + } + ) + }, + containerColor = LoginBackgroundTurquoise + ) { paddingValues -> + Box( + modifier = modifier + .fillMaxSize() + .padding(paddingValues) + .background(color = LoginBackgroundTurquoise) + ) { + val configuration = LocalConfiguration.current + val screenWidthDp: Int = configuration.screenWidthDp + val screenHeightDp: Int = configuration.screenHeightDp + val isLandscape = screenWidthDp > screenHeightDp + + // Адаптивные отступы + val horizontalPadding = (screenWidthDp * 0.04f).toInt().coerceIn(24, 48).dp + val verticalPadding = (screenHeightDp * 0.03f).toInt().coerceIn(16, 32).dp + + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = horizontalPadding, vertical = verticalPadding), + horizontalAlignment = Alignment.CenterHorizontally + ) { + // Логотип вверху слева + val logoSizeByHeight = (screenHeightDp * 0.15f).toInt() + val logoSizeByWidth = screenWidthDp / 5 + val logoSize = min(logoSizeByHeight, logoSizeByWidth).coerceIn(100, 160).dp + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Start + ) { + NovayaPlanetaLogo( + modifier = Modifier + .padding(bottom = 4.dp) + .clickable { + navController.navigate("login") { + popUpTo("login") { inclusive = false } + } + }, + size = logoSize + ) + } + + Spacer(modifier = Modifier.weight(0.05f)) + + // Центрированный контент - адаптивная ширина (50-70% экрана) + val contentWidthRatio = if (isLandscape) 0.5f else 0.7f + Column( + modifier = Modifier + .fillMaxWidth(contentWidthRatio), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(24.dp) + ) { + // Заголовок "Восстановление пароля" + val titleFontSize = (screenHeightDp * 0.06f).toInt().coerceIn(44, 76).sp + Text( + text = "Восстановление пароля", + fontSize = titleFontSize, + fontWeight = FontWeight.Bold, + color = LoginGreenAccent, + textAlign = TextAlign.Center + ) + + // Поля ввода - адаптивные размеры + val inputHeightValue = (screenHeightDp * 0.075f).toInt().coerceIn(64, 100) + val inputHeight: androidx.compose.ui.unit.Dp = inputHeightValue.dp + val inputTextSizeValue = (screenHeightDp * 0.026f).toInt().coerceIn(20, 28) + val inputTextSize: androidx.compose.ui.unit.TextUnit = inputTextSizeValue.sp + + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + // Поле email (всегда видимо) + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(height = inputHeight) + .background( + color = if (uiState.emailError != null) { + Color(0xFFFFEBEE) // Светло-красный фон при ошибке + } else { + LoginInputLightBlue + }, + shape = RoundedCornerShape(20.dp) + ), + contentAlignment = Alignment.CenterStart + ) { + TextField( + value = uiState.email, + onValueChange = { viewModel.onEmailChange(it) }, + placeholder = { + Text( + text = "Введи email", + fontSize = inputTextSize, + color = Color.Gray.copy(alpha = 0.7f) + ) + }, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + colors = TextFieldDefaults.colors( + unfocusedContainerColor = Color.Transparent, + focusedContainerColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + unfocusedTextColor = Color.Black, + focusedTextColor = Color.Black + ), + textStyle = MaterialTheme.typography.bodyLarge.copy( + fontSize = inputTextSize + ), + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email), + isError = uiState.emailError != null + ) + } + + // Сообщение об ошибке email + if (uiState.emailError != null) { + Text( + text = uiState.emailError!!, + fontSize = (inputTextSizeValue * 0.8f).toInt().sp, + color = MaterialTheme.colorScheme.error, + modifier = Modifier.padding(horizontal = 16.dp) + ) + } + } + + // Поле кода (показывается после ввода email) + if (uiState.showCodeField) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(height = inputHeight) + .background( + color = LoginInputLightBlue, + shape = RoundedCornerShape(20.dp) + ), + contentAlignment = Alignment.CenterStart + ) { + TextField( + value = uiState.code, + onValueChange = { viewModel.onCodeChange(it) }, + placeholder = { + Text( + text = "Введи код из письма", + fontSize = inputTextSize, + color = Color.Gray.copy(alpha = 0.7f) + ) + }, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + colors = TextFieldDefaults.colors( + unfocusedContainerColor = Color.Transparent, + focusedContainerColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + unfocusedTextColor = Color.Black, + focusedTextColor = Color.Black + ), + textStyle = MaterialTheme.typography.bodyLarge.copy( + fontSize = inputTextSize + ), + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number) + ) + } + } + + // Поля пароля (показываются после ввода кода) + if (uiState.showPasswordFields) { + // Новый пароль + Box( + modifier = Modifier + .fillMaxWidth() + .height(height = inputHeight) + .background( + color = LoginInputLightBlue, + shape = RoundedCornerShape(20.dp) + ), + contentAlignment = Alignment.CenterStart + ) { + TextField( + value = uiState.newPassword, + onValueChange = { viewModel.onNewPasswordChange(it) }, + placeholder = { + Text( + text = "Введи новый пароль", + fontSize = inputTextSize, + color = Color.Gray.copy(alpha = 0.7f) + ) + }, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + visualTransformation = PasswordVisualTransformation(), + colors = TextFieldDefaults.colors( + unfocusedContainerColor = Color.Transparent, + focusedContainerColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + unfocusedTextColor = Color.Black, + focusedTextColor = Color.Black + ), + textStyle = MaterialTheme.typography.bodyLarge.copy( + fontSize = inputTextSize + ), + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password) + ) + } + + // Подтверждение пароля + Box( + modifier = Modifier + .fillMaxWidth() + .height(height = inputHeight) + .background( + color = LoginInputLightBlue, + shape = RoundedCornerShape(20.dp) + ), + contentAlignment = Alignment.CenterStart + ) { + TextField( + value = uiState.confirmPassword, + onValueChange = { viewModel.onConfirmPasswordChange(it) }, + placeholder = { + Text( + text = "Повтори новый пароль", + fontSize = inputTextSize, + color = Color.Gray.copy(alpha = 0.7f) + ) + }, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + visualTransformation = PasswordVisualTransformation(), + colors = TextFieldDefaults.colors( + unfocusedContainerColor = Color.Transparent, + focusedContainerColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + unfocusedTextColor = Color.Black, + focusedTextColor = Color.Black + ), + textStyle = MaterialTheme.typography.bodyLarge.copy( + fontSize = inputTextSize + ), + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password) + ) + } + } + } + + // Кнопка "Готово!" - адаптивный размер + val isButtonEnabled = when { + !uiState.showCodeField -> uiState.isBasicFormValid + !uiState.showPasswordFields -> uiState.isCodeFormValid + else -> uiState.isFormValid + } + + Button( + onClick = { + viewModel.onReadyClick { + navController.navigate("login") { + popUpTo("login") { inclusive = false } + } + } + }, + modifier = Modifier + .fillMaxWidth() + .height(height = inputHeight), + shape = RoundedCornerShape(20.dp), + enabled = isButtonEnabled && !uiState.isLoading, + colors = if (isButtonEnabled) { + ButtonDefaults.buttonColors( + containerColor = LoginGreenAccent, + contentColor = Color.White + ) + } else { + ButtonDefaults.buttonColors( + containerColor = LoginInputLightBlue, + contentColor = Color.Gray, + disabledContainerColor = LoginInputLightBlue, + disabledContentColor = Color.Gray + ) + } + ) { + if (uiState.isLoading) { + CircularProgressIndicator( + modifier = Modifier.size(40.dp), + color = Color.White + ) + } else { + val buttonTextSize = (screenHeightDp * 0.027f).toInt().coerceIn(22, 34).sp + Text( + text = "Готово!", + fontSize = buttonTextSize, + fontWeight = FontWeight.Bold + ) + } + } + } + + Spacer(modifier = Modifier.weight(0.1f)) + } + } + } +} + diff --git a/app/src/main/java/com/novayaplaneta/ui/screens/auth/ForgotPasswordViewModel.kt b/app/src/main/java/com/novayaplaneta/ui/screens/auth/ForgotPasswordViewModel.kt new file mode 100644 index 0000000..42de985 --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/ui/screens/auth/ForgotPasswordViewModel.kt @@ -0,0 +1,120 @@ +package com.novayaplaneta.ui.screens.auth + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class ForgotPasswordViewModel @Inject constructor() : ViewModel() { + + private val _uiState = MutableStateFlow(ForgotPasswordUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + fun onEmailChange(email: String) { + _uiState.value = _uiState.value.copy(email = email) + // Если email стал невалидным, скрываем поле кода и сбрасываем его значение + if (!EmailValidator.isValid(email) && _uiState.value.showCodeField) { + _uiState.value = _uiState.value.copy( + showCodeField = false, + code = "", + showPasswordFields = false, + newPassword = "", + confirmPassword = "" + ) + } + } + + fun onCodeChange(code: String) { + _uiState.value = _uiState.value.copy(code = code) + // Если код был удален, скрываем поля пароля + if (code.isBlank() && _uiState.value.showPasswordFields) { + _uiState.value = _uiState.value.copy( + showPasswordFields = false, + newPassword = "", + confirmPassword = "" + ) + } + } + + fun onNewPasswordChange(password: String) { + _uiState.value = _uiState.value.copy(newPassword = password) + } + + fun onConfirmPasswordChange(password: String) { + _uiState.value = _uiState.value.copy(confirmPassword = password) + } + + fun onReadyClick(onSuccess: () -> Unit) { + val currentState = _uiState.value + + // Шаг 1: Если поле кода еще не показано, но email валиден - показываем поле кода + if (!currentState.showCodeField && currentState.isEmailValid) { + _uiState.value = currentState.copy(showCodeField = true) + return + } + + // Шаг 2: Если код введен, но поля пароля еще не показаны - показываем поля пароля + if (currentState.showCodeField && currentState.code.isNotBlank() && !currentState.showPasswordFields) { + _uiState.value = currentState.copy(showPasswordFields = true) + return + } + + // Шаг 3: Если форма полностью валидна (включая код и пароли), выполняем восстановление пароля + if (currentState.isFormValid) { + viewModelScope.launch { + _uiState.value = _uiState.value.copy(isLoading = true, errorMessage = null) + + // TODO: Реализовать вызов API для восстановления пароля + // Пока что просто эмулируем успешное восстановление + delay(1000) + + _uiState.value = _uiState.value.copy(isLoading = false) + onSuccess() + } + } + } +} + +data class ForgotPasswordUiState( + val email: String = "", + val code: String = "", + val newPassword: String = "", + val confirmPassword: String = "", + val showCodeField: Boolean = false, + val showPasswordFields: Boolean = false, + val isLoading: Boolean = false, + val errorMessage: String? = null +) { + // Валидация email + val isEmailValid: Boolean + get() = EmailValidator.isValid(email) + + // Сообщение об ошибке email + val emailError: String? + get() = if (email.isNotBlank() && !isEmailValid) { + "Введите корректный email" + } else null + + // Валидация для первой кнопки (только email) + val isBasicFormValid: Boolean + get() = isEmailValid + + // Валидация для второй кнопки (email + код) + val isCodeFormValid: Boolean + get() = isEmailValid && code.isNotBlank() + + // Валидация полной формы (включая код и пароли) + val isFormValid: Boolean + get() = isEmailValid && + code.isNotBlank() && + newPassword.isNotBlank() && + confirmPassword.isNotBlank() && + newPassword == confirmPassword +} + diff --git a/app/src/main/java/com/novayaplaneta/ui/screens/auth/LoginScreen.kt b/app/src/main/java/com/novayaplaneta/ui/screens/auth/LoginScreen.kt index e1234a6..49d5f8c 100644 --- a/app/src/main/java/com/novayaplaneta/ui/screens/auth/LoginScreen.kt +++ b/app/src/main/java/com/novayaplaneta/ui/screens/auth/LoginScreen.kt @@ -212,10 +212,13 @@ fun LoginScreen( // Кнопка "Войти" - адаптивный размер Button( onClick = { - viewModel.login { + if (uiState.isFormValid) { + // Переход на экран расписания при заполненных полях navController.navigate("schedule") { popUpTo(0) { inclusive = true } } + // Также вызываем логин для проверки через API (в фоне) + viewModel.login { } } }, modifier = Modifier diff --git a/app/src/main/java/com/novayaplaneta/ui/screens/auth/RegistrationScreen.kt b/app/src/main/java/com/novayaplaneta/ui/screens/auth/RegistrationScreen.kt new file mode 100644 index 0000000..b6263cf --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/ui/screens/auth/RegistrationScreen.kt @@ -0,0 +1,419 @@ +package com.novayaplaneta.ui.screens.auth + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavController +import com.novayaplaneta.ui.components.NovayaPlanetaLogo +import com.novayaplaneta.ui.theme.* +import kotlin.math.min + +@Composable +fun RegistrationScreen( + navController: NavController, + viewModel: RegistrationViewModel = hiltViewModel(), + modifier: Modifier = Modifier +) { + val uiState by viewModel.uiState.collectAsState() + val snackbarHostState = remember { SnackbarHostState() } + + LaunchedEffect(uiState.errorMessage) { + val errorMsg = uiState.errorMessage + if (errorMsg != null) { + snackbarHostState.showSnackbar( + message = errorMsg, + actionLabel = null + ) + } + } + + Scaffold( + snackbarHost = { + SnackbarHost( + hostState = snackbarHostState, + snackbar = { snackbarData -> + Snackbar( + snackbarData = snackbarData, + containerColor = MaterialTheme.colorScheme.error, + contentColor = Color.White + ) + } + ) + }, + containerColor = LoginBackgroundTurquoise + ) { paddingValues -> + Box( + modifier = modifier + .fillMaxSize() + .padding(paddingValues) + .background(color = LoginBackgroundTurquoise) + ) { + val configuration = LocalConfiguration.current + val screenWidthDp: Int = configuration.screenWidthDp + val screenHeightDp: Int = configuration.screenHeightDp + val isLandscape = screenWidthDp > screenHeightDp + + // Адаптивные отступы + val horizontalPadding = (screenWidthDp * 0.04f).toInt().coerceIn(24, 48).dp + val verticalPadding = (screenHeightDp * 0.03f).toInt().coerceIn(16, 32).dp + + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = horizontalPadding, vertical = verticalPadding), + horizontalAlignment = Alignment.CenterHorizontally + ) { + // Логотип вверху слева + val logoSizeByHeight = (screenHeightDp * 0.15f).toInt() + val logoSizeByWidth = screenWidthDp / 5 + val logoSize = min(logoSizeByHeight, logoSizeByWidth).coerceIn(100, 160).dp + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Start + ) { + NovayaPlanetaLogo( + modifier = Modifier + .padding(bottom = 4.dp) + .clickable { + navController.navigate("login") { + popUpTo("login") { inclusive = false } + } + }, + size = logoSize + ) + } + + Spacer(modifier = Modifier.weight(0.05f)) + + // Центрированный контент - адаптивная ширина (50-70% экрана) + val contentWidthRatio = if (isLandscape) 0.5f else 0.7f + Column( + modifier = Modifier + .fillMaxWidth(contentWidthRatio), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(24.dp) + ) { + // Заголовок "Регистрация" + val titleFontSize = (screenHeightDp * 0.06f).toInt().coerceIn(44, 76).sp + Text( + text = "Регистрация", + fontSize = titleFontSize, + fontWeight = FontWeight.Bold, + color = LoginGreenAccent, + textAlign = TextAlign.Center + ) + + // Поля ввода - адаптивные размеры + val inputHeightValue = (screenHeightDp * 0.075f).toInt().coerceIn(64, 100) + val inputHeight: androidx.compose.ui.unit.Dp = inputHeightValue.dp + val inputTextSizeValue = (screenHeightDp * 0.026f).toInt().coerceIn(20, 28) + val inputTextSize: androidx.compose.ui.unit.TextUnit = inputTextSizeValue.sp + + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + // Поле логина + Box( + modifier = Modifier + .fillMaxWidth() + .height(height = inputHeight) + .background( + color = LoginInputLightBlue, + shape = RoundedCornerShape(20.dp) + ), + contentAlignment = Alignment.CenterStart + ) { + TextField( + value = uiState.login, + onValueChange = { viewModel.onLoginChange(it) }, + placeholder = { + Text( + text = "Введи логин", + fontSize = inputTextSize, + color = Color.Gray.copy(alpha = 0.7f) + ) + }, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + colors = TextFieldDefaults.colors( + unfocusedContainerColor = Color.Transparent, + focusedContainerColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + unfocusedTextColor = Color.Black, + focusedTextColor = Color.Black + ), + textStyle = MaterialTheme.typography.bodyLarge.copy( + fontSize = inputTextSize + ), + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text) + ) + } + + // Поле пароля + Box( + modifier = Modifier + .fillMaxWidth() + .height(height = inputHeight) + .background( + color = LoginInputLightBlue, + shape = RoundedCornerShape(20.dp) + ), + contentAlignment = Alignment.CenterStart + ) { + TextField( + value = uiState.password, + onValueChange = { viewModel.onPasswordChange(it) }, + placeholder = { + Text( + text = "Введи пароль", + fontSize = inputTextSize, + color = Color.Gray.copy(alpha = 0.7f) + ) + }, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + visualTransformation = PasswordVisualTransformation(), + colors = TextFieldDefaults.colors( + unfocusedContainerColor = Color.Transparent, + focusedContainerColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + unfocusedTextColor = Color.Black, + focusedTextColor = Color.Black + ), + textStyle = MaterialTheme.typography.bodyLarge.copy( + fontSize = inputTextSize + ), + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password) + ) + } + + // Поле повторения пароля + Box( + modifier = Modifier + .fillMaxWidth() + .height(height = inputHeight) + .background( + color = LoginInputLightBlue, + shape = RoundedCornerShape(20.dp) + ), + contentAlignment = Alignment.CenterStart + ) { + TextField( + value = uiState.confirmPassword, + onValueChange = { viewModel.onConfirmPasswordChange(it) }, + placeholder = { + Text( + text = "Повтори пароль", + fontSize = inputTextSize, + color = Color.Gray.copy(alpha = 0.7f) + ) + }, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + visualTransformation = PasswordVisualTransformation(), + colors = TextFieldDefaults.colors( + unfocusedContainerColor = Color.Transparent, + focusedContainerColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + unfocusedTextColor = Color.Black, + focusedTextColor = Color.Black + ), + textStyle = MaterialTheme.typography.bodyLarge.copy( + fontSize = inputTextSize + ), + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password) + ) + } + + // Поле email + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(height = inputHeight) + .background( + color = if (uiState.emailError != null) { + Color(0xFFFFEBEE) // Светло-красный фон при ошибке + } else { + LoginInputLightBlue + }, + shape = RoundedCornerShape(20.dp) + ), + contentAlignment = Alignment.CenterStart + ) { + TextField( + value = uiState.email, + onValueChange = { viewModel.onEmailChange(it) }, + placeholder = { + Text( + text = "Введи email", + fontSize = inputTextSize, + color = Color.Gray.copy(alpha = 0.7f) + ) + }, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + colors = TextFieldDefaults.colors( + unfocusedContainerColor = Color.Transparent, + focusedContainerColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + unfocusedTextColor = Color.Black, + focusedTextColor = Color.Black + ), + textStyle = MaterialTheme.typography.bodyLarge.copy( + fontSize = inputTextSize + ), + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email), + isError = uiState.emailError != null + ) + } + + // Сообщение об ошибке email + if (uiState.emailError != null) { + Text( + text = uiState.emailError!!, + fontSize = (inputTextSizeValue * 0.8f).toInt().sp, + color = MaterialTheme.colorScheme.error, + modifier = Modifier.padding(horizontal = 16.dp) + ) + } + } + + // Поле кода (показывается после нажатия "Готово!" когда все 4 поля заполнены) + if (uiState.showCodeField) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(height = inputHeight) + .background( + color = LoginInputLightBlue, + shape = RoundedCornerShape(20.dp) + ), + contentAlignment = Alignment.CenterStart + ) { + TextField( + value = uiState.code, + onValueChange = { viewModel.onCodeChange(it) }, + placeholder = { + Text( + text = "Введи код из письма на почте", + fontSize = inputTextSize, + color = Color.Gray.copy(alpha = 0.7f) + ) + }, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + colors = TextFieldDefaults.colors( + unfocusedContainerColor = Color.Transparent, + focusedContainerColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + unfocusedTextColor = Color.Black, + focusedTextColor = Color.Black + ), + textStyle = MaterialTheme.typography.bodyLarge.copy( + fontSize = inputTextSize + ), + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number) + ) + } + } + } + + // Кнопка "Готово!" - адаптивный размер + // Активна когда заполнены все 4 поля (до показа кода) или когда заполнен код + val isButtonEnabled = if (!uiState.showCodeField) { + uiState.isBasicFormValid && !uiState.isLoading + } else { + uiState.isFormValid && !uiState.isLoading + } + + val buttonColor = if (isButtonEnabled) { + ButtonDefaults.buttonColors( + containerColor = LoginGreenAccent, + contentColor = Color.White + ) + } else { + ButtonDefaults.buttonColors( + containerColor = LoginInputLightBlue, + contentColor = Color.Gray, + disabledContainerColor = LoginInputLightBlue, + disabledContentColor = Color.Gray + ) + } + + Button( + onClick = { + viewModel.onReadyClick { + navController.navigate("login") { + popUpTo("login") { inclusive = false } + } + } + }, + modifier = Modifier + .fillMaxWidth() + .height(height = inputHeight), + shape = RoundedCornerShape(20.dp), + enabled = isButtonEnabled, + colors = buttonColor + ) { + if (uiState.isLoading) { + CircularProgressIndicator( + modifier = Modifier.size(40.dp), + color = Color.White + ) + } else { + val buttonTextSize = (screenHeightDp * 0.027f).toInt().coerceIn(22, 34).sp + Text( + text = "Готово!", + fontSize = buttonTextSize, + fontWeight = FontWeight.Bold + ) + } + } + } + + Spacer(modifier = Modifier.weight(0.1f)) + } + } + } +} + diff --git a/app/src/main/java/com/novayaplaneta/ui/screens/auth/RegistrationViewModel.kt b/app/src/main/java/com/novayaplaneta/ui/screens/auth/RegistrationViewModel.kt new file mode 100644 index 0000000..32f1135 --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/ui/screens/auth/RegistrationViewModel.kt @@ -0,0 +1,97 @@ +package com.novayaplaneta.ui.screens.auth + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class RegistrationViewModel @Inject constructor() : ViewModel() { + + private val _uiState = MutableStateFlow(RegistrationUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + fun onLoginChange(login: String) { + _uiState.value = _uiState.value.copy(login = login) + } + + fun onPasswordChange(password: String) { + _uiState.value = _uiState.value.copy(password = password) + } + + fun onConfirmPasswordChange(password: String) { + _uiState.value = _uiState.value.copy(confirmPassword = password) + } + + fun onEmailChange(email: String) { + _uiState.value = _uiState.value.copy(email = email) + } + + fun onCodeChange(code: String) { + _uiState.value = _uiState.value.copy(code = code) + } + + fun onReadyClick(onSuccess: () -> Unit) { + val currentState = _uiState.value + + // Если поле кода еще не показано, но все 4 поля заполнены - показываем поле кода + if (!currentState.showCodeField && currentState.isBasicFormValid) { + _uiState.value = currentState.copy(showCodeField = true) + return + } + + // Если форма полностью валидна (включая код), выполняем регистрацию + if (currentState.isFormValid) { + viewModelScope.launch { + _uiState.value = _uiState.value.copy(isLoading = true, errorMessage = null) + + // TODO: Реализовать вызов API для регистрации + // Пока что просто эмулируем успешную регистрацию + delay(1000) + + _uiState.value = _uiState.value.copy(isLoading = false) + onSuccess() + } + } + } +} + +data class RegistrationUiState( + val login: String = "", + val password: String = "", + val confirmPassword: String = "", + val email: String = "", + val code: String = "", + val showCodeField: Boolean = false, + val isLoading: Boolean = false, + val errorMessage: String? = null +) { + // Валидация email + val isEmailValid: Boolean + get() = EmailValidator.isValid(email) + + // Сообщение об ошибке email + val emailError: String? + get() = if (email.isNotBlank() && !isEmailValid) { + "Введите корректный email" + } else null + + // Валидация первых 4 полей (без кода) + val isBasicFormValid: Boolean + get() = login.isNotBlank() && + password.isNotBlank() && + confirmPassword.isNotBlank() && + isEmailValid && + password == confirmPassword + + // Валидация полной формы (включая код) + val isFormValid: Boolean + get() = isBasicFormValid && + code.isNotBlank() +} + diff --git a/app/src/main/java/com/novayaplaneta/ui/screens/rewards/RewardsScreen.kt b/app/src/main/java/com/novayaplaneta/ui/screens/rewards/RewardsScreen.kt index 95be903..281782c 100644 --- a/app/src/main/java/com/novayaplaneta/ui/screens/rewards/RewardsScreen.kt +++ b/app/src/main/java/com/novayaplaneta/ui/screens/rewards/RewardsScreen.kt @@ -1,81 +1,202 @@ package com.novayaplaneta.ui.screens.rewards +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CalendarToday +import androidx.compose.material.icons.filled.Person +import androidx.compose.material.icons.filled.Public import androidx.compose.material.icons.filled.Star import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel +import com.novayaplaneta.ui.components.NovayaPlanetaLogo +import com.novayaplaneta.ui.theme.* +import java.text.SimpleDateFormat +import java.util.* -@OptIn(ExperimentalMaterial3Api::class) @Composable fun RewardsScreen( + navController: androidx.navigation.NavController? = null, viewModel: RewardsViewModel = hiltViewModel(), modifier: Modifier = Modifier ) { val uiState by viewModel.uiState.collectAsState() + val configuration = LocalConfiguration.current + val screenWidthDp = configuration.screenWidthDp + val screenHeightDp = configuration.screenHeightDp - Scaffold( - topBar = { - TopAppBar( - title = { Text("Награды") } - ) - } - ) { paddingValues -> - Column( - modifier = modifier - .fillMaxSize() - .padding(paddingValues) - .padding(16.dp) + // Цвета из autism-friendly палитры + val backgroundColor = LoginBackgroundTurquoise + val navPanelColor = LoginInputLightBlue + val accentGreen = LoginGreenAccent + val dateCardColor = LoginInputLightBlue + + // Форматирование даты и времени по московскому времени + val moscowTimeZone = TimeZone.getTimeZone("Europe/Moscow") + val dateFormat = SimpleDateFormat("dd.MM EEEE", Locale("ru")).apply { + timeZone = moscowTimeZone + } + val timeFormat = SimpleDateFormat("HH:mm", Locale("ru")).apply { + timeZone = moscowTimeZone + } + val currentDate = dateFormat.format(Date()) + val currentTime = timeFormat.format(Date()) + val dayOfWeek = currentDate.split(" ").getOrNull(1) ?: "" + val dateOnly = currentDate.split(" ").getOrNull(0) ?: "" + + Box( + modifier = modifier + .fillMaxSize() + .background(color = backgroundColor) + ) { + Row( + modifier = Modifier.fillMaxSize() ) { - if (uiState.isLoading) { - CircularProgressIndicator( + // Левая панель навигации с логотипом над ней (с отступами и закругленными краями) + Column( + modifier = Modifier + .width((screenWidthDp * 0.22f).dp.coerceIn(160.dp, 240.dp)) + .fillMaxHeight() + .padding(vertical = 20.dp, horizontal = 16.dp) + ) { + // Логотип над панелью навигации (увеличен) + Box( modifier = Modifier .fillMaxWidth() - .wrapContentWidth() - ) - } else { - LazyColumn( - verticalArrangement = Arrangement.spacedBy(8.dp) + .padding(bottom = 16.dp), + contentAlignment = Alignment.Center ) { - items(uiState.rewards) { reward -> - Card( - modifier = Modifier.fillMaxWidth() - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - ) { - Column(modifier = Modifier.weight(1f)) { - Text( - text = reward.title, - style = MaterialTheme.typography.titleLarge - ) - reward.description?.let { - Text( - text = it, - style = MaterialTheme.typography.bodyMedium - ) - } - Text( - text = "${reward.points} очков", - style = MaterialTheme.typography.bodySmall - ) - } - if (reward.earnedAt != null) { - Icon( - imageVector = androidx.compose.material.icons.Icons.Default.Star, - contentDescription = "Получено", - tint = MaterialTheme.colorScheme.primary - ) - } - } + val logoSize = (screenHeightDp * 0.18f).toInt().coerceIn(120, 200).dp + NovayaPlanetaLogo(size = logoSize) + } + + // Панель навигации (закругленная со всех сторон) + Column( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .clip(RoundedCornerShape(32.dp)) + .background(color = navPanelColor) + .padding(vertical = 24.dp, horizontal = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(32.dp) + ) { + // Расписание + NavItem( + icon = Icons.Filled.CalendarToday, + text = "Расписание", + isSelected = false, + onClick = { navController?.navigate("schedule") } + ) + + // Награды (активный) + NavItem( + icon = Icons.Filled.Star, + text = "Награды", + isSelected = true, + onClick = { } + ) + + // Профиль + NavItem( + icon = Icons.Filled.Person, + text = "Профиль", + isSelected = false, + onClick = { navController?.navigate("settings") } + ) + + // Земля + NavItem( + icon = Icons.Filled.Public, + text = "Земля", + isSelected = false, + onClick = { navController?.navigate("ai") } + ) + } + } + + // Основная область + Column( + modifier = Modifier + .weight(1f) + .fillMaxHeight() + .padding(24.dp) + ) { + // Верхняя панель: только дата/время справа + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically + ) { + // Дата и время (зеленый цвет) + Column( + horizontalAlignment = Alignment.End + ) { + val dateTextSize = (screenHeightDp * 0.06f).toInt().coerceIn(48, 80).sp + Text( + text = "$dateOnly $dayOfWeek", + fontSize = dateTextSize, + fontWeight = FontWeight.Bold, + color = accentGreen + ) + Text( + text = currentTime, + fontSize = dateTextSize, + fontWeight = FontWeight.Bold, + color = accentGreen + ) + } + } + + Spacer(modifier = Modifier.height(48.dp)) + + // Заголовок "Награды" + val titleSize = (screenHeightDp * 0.04f).toInt().coerceIn(32, 52).sp + Text( + text = "Награды", + fontSize = titleSize, + fontWeight = FontWeight.Bold, + color = Color.Black, + modifier = Modifier.padding(bottom = 24.dp) + ) + + // Список наград в сетке (несколько колонок) + if (uiState.isLoading) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator(color = accentGreen) + } + } else { + LazyVerticalGrid( + columns = GridCells.Adaptive(minSize = 200.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.fillMaxSize() + ) { + items(uiState.rewards) { reward -> + RewardCard( + reward = reward, + screenHeightDp = screenHeightDp + ) } } } @@ -84,3 +205,95 @@ fun RewardsScreen( } } +@Composable +fun NavItem( + icon: androidx.compose.ui.graphics.vector.ImageVector, + text: String, + isSelected: Boolean, + onClick: () -> Unit +) { + val iconSize = 40.dp + val textSize = 18.sp + + Column( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(16.dp)) + .clickable { onClick() } + .background( + color = if (isSelected) Color.White.copy(alpha = 0.3f) else Color.Transparent + ) + .padding(12.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + imageVector = icon, + contentDescription = text, + modifier = Modifier.size(iconSize), + tint = LoginGreenAccent + ) + Text( + text = text, + fontSize = textSize, + fontWeight = FontWeight.Bold, + color = Color.Black + ) + } +} + +@Composable +fun RewardCard( + reward: com.novayaplaneta.domain.model.Reward, + screenHeightDp: Int +) { + val cardHeight = 180.dp + + Box( + modifier = Modifier + .fillMaxWidth() + .height(cardHeight) + .clip(RoundedCornerShape(16.dp)) + .background(color = Color.LightGray) + ) { + Column( + modifier = Modifier.fillMaxSize() + ) { + // Верхняя часть (для изображения) - серая область со звездой + Box( + modifier = Modifier + .fillMaxWidth() + .weight(0.6f) + .background(color = Color.White.copy(alpha = 0.5f)), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.Filled.Star, + contentDescription = reward.title, + modifier = Modifier.size(60.dp), + tint = AccentGold + ) + } + + // Нижняя часть (для текста) - темно-серая область с названием + Box( + modifier = Modifier + .fillMaxWidth() + .weight(0.4f) + .background(color = Color.LightGray) + .padding(12.dp), + contentAlignment = Alignment.Center + ) { + val textSize = (screenHeightDp * 0.02f).toInt().coerceIn(16, 24).sp + Text( + text = reward.title, + fontSize = textSize, + fontWeight = FontWeight.Bold, + color = Color.Black, + textAlign = TextAlign.Center + ) + } + } + } +} + diff --git a/app/src/main/java/com/novayaplaneta/ui/screens/rewards/RewardsViewModel.kt b/app/src/main/java/com/novayaplaneta/ui/screens/rewards/RewardsViewModel.kt index 14b77ae..cf393e1 100644 --- a/app/src/main/java/com/novayaplaneta/ui/screens/rewards/RewardsViewModel.kt +++ b/app/src/main/java/com/novayaplaneta/ui/screens/rewards/RewardsViewModel.kt @@ -2,6 +2,7 @@ package com.novayaplaneta.ui.screens.rewards import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.novayaplaneta.domain.model.Reward import com.novayaplaneta.domain.repository.RewardRepository import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow @@ -18,12 +19,96 @@ class RewardsViewModel @Inject constructor( private val _uiState = MutableStateFlow(RewardsUiState()) val uiState: StateFlow = _uiState.asStateFlow() + init { + loadDefaultRewards() + } + + private fun loadDefaultRewards() { + // Создаем список наград по умолчанию + val defaultRewards = listOf( + Reward( + id = "reward_1", + title = "Золотая звезда", + description = "За выполнение 3 задач", + imageUrl = null, + points = 10, + earnedAt = null, + userId = "default" + ), + Reward( + id = "reward_2", + title = "Подарочная коробка", + description = "За выполнение 5 задач", + imageUrl = null, + points = 20, + earnedAt = null, + userId = "default" + ), + Reward( + id = "reward_3", + title = "Игрушка", + description = "За выполнение всех задач дня", + imageUrl = null, + points = 30, + earnedAt = null, + userId = "default" + ), + Reward( + id = "reward_4", + title = "Мультфильм", + description = "За неделю без пропусков", + imageUrl = null, + points = 50, + earnedAt = null, + userId = "default" + ), + Reward( + id = "reward_5", + title = "Поход в парк", + description = "За месяц работы", + imageUrl = null, + points = 100, + earnedAt = null, + userId = "default" + ), + Reward( + id = "reward_6", + title = "Новая книга", + description = "За выполнение 10 задач", + imageUrl = null, + points = 40, + earnedAt = null, + userId = "default" + ), + Reward( + id = "reward_7", + title = "Любимая игра", + description = "За хорошее поведение", + imageUrl = null, + points = 25, + earnedAt = null, + userId = "default" + ), + Reward( + id = "reward_8", + title = "Специальный обед", + description = "За старание в учебе", + imageUrl = null, + points = 35, + earnedAt = null, + userId = "default" + ) + ) + + _uiState.value = _uiState.value.copy(rewards = defaultRewards, isLoading = false) + } + fun loadRewards(userId: String) { viewModelScope.launch { _uiState.value = _uiState.value.copy(isLoading = true) rewardRepository.getRewards(userId).collect { rewards -> _uiState.value = _uiState.value.copy( - rewards = rewards, + rewards = if (rewards.isEmpty()) _uiState.value.rewards else rewards, isLoading = false ) } @@ -38,7 +123,7 @@ class RewardsViewModel @Inject constructor( } data class RewardsUiState( - val rewards: List = emptyList(), + val rewards: List = emptyList(), val isLoading: Boolean = false ) diff --git a/app/src/main/java/com/novayaplaneta/ui/screens/schedule/ScheduleScreen.kt b/app/src/main/java/com/novayaplaneta/ui/screens/schedule/ScheduleScreen.kt index dd47a24..dfe0adf 100644 --- a/app/src/main/java/com/novayaplaneta/ui/screens/schedule/ScheduleScreen.kt +++ b/app/src/main/java/com/novayaplaneta/ui/screens/schedule/ScheduleScreen.kt @@ -1,66 +1,471 @@ package com.novayaplaneta.ui.screens.schedule +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.CalendarToday +import androidx.compose.material.icons.filled.Person +import androidx.compose.material.icons.filled.Public +import androidx.compose.material.icons.filled.Star import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog import androidx.hilt.navigation.compose.hiltViewModel +import com.novayaplaneta.ui.components.NovayaPlanetaLogo +import com.novayaplaneta.ui.theme.* +import java.text.SimpleDateFormat +import java.util.* -@OptIn(ExperimentalMaterial3Api::class) @Composable fun ScheduleScreen( + navController: androidx.navigation.NavController? = null, viewModel: ScheduleViewModel = hiltViewModel(), modifier: Modifier = Modifier ) { val uiState by viewModel.uiState.collectAsState() + val configuration = LocalConfiguration.current + val screenWidthDp = configuration.screenWidthDp + val screenHeightDp = configuration.screenHeightDp - Scaffold( - topBar = { - TopAppBar( - title = { Text("Расписание") } - ) - } - ) { paddingValues -> - Column( - modifier = modifier - .fillMaxSize() - .padding(paddingValues) - .padding(16.dp) + // Цвета из autism-friendly палитры + val backgroundColor = LoginBackgroundTurquoise + val navPanelColor = LoginInputLightBlue + val accentGreen = LoginGreenAccent + val dateCardColor = LoginInputLightBlue + + // Форматирование даты и времени по московскому времени + val moscowTimeZone = TimeZone.getTimeZone("Europe/Moscow") + val dateFormat = SimpleDateFormat("dd.MM EEEE", Locale("ru")).apply { + timeZone = moscowTimeZone + } + val timeFormat = SimpleDateFormat("HH:mm", Locale("ru")).apply { + timeZone = moscowTimeZone + } + val currentDate = dateFormat.format(Date()) + val currentTime = timeFormat.format(Date()) + val dayOfWeek = currentDate.split(" ").getOrNull(1) ?: "" + val dateOnly = currentDate.split(" ").getOrNull(0) ?: "" + + Box( + modifier = modifier + .fillMaxSize() + .background(color = backgroundColor) + ) { + Row( + modifier = Modifier.fillMaxSize() ) { - if (uiState.isLoading) { - CircularProgressIndicator( + // Левая панель навигации с логотипом над ней (с отступами и закругленными краями) + Column( + modifier = Modifier + .width((screenWidthDp * 0.22f).dp.coerceIn(160.dp, 240.dp)) + .fillMaxHeight() + .padding(vertical = 20.dp, horizontal = 16.dp) + ) { + // Логотип над панелью навигации (увеличен) + Box( modifier = Modifier .fillMaxWidth() - .wrapContentWidth() - ) - } else { - LazyColumn( - verticalArrangement = Arrangement.spacedBy(8.dp) + .padding(bottom = 16.dp), + contentAlignment = Alignment.Center ) { - items(uiState.schedules) { schedule -> - Card( - modifier = Modifier.fillMaxWidth() - ) { - Column( - modifier = Modifier.padding(16.dp) - ) { - Text( - text = schedule.title, - style = MaterialTheme.typography.titleLarge - ) - schedule.description?.let { - Text( - text = it, - style = MaterialTheme.typography.bodyMedium - ) - } - } - } + val logoSize = (screenHeightDp * 0.18f).toInt().coerceIn(120, 200).dp + NovayaPlanetaLogo(size = logoSize) + } + + // Панель навигации (закругленная со всех сторон) + Column( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .clip(RoundedCornerShape(32.dp)) + .background(color = navPanelColor) + .padding(vertical = 24.dp, horizontal = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(32.dp) + ) { + // Расписание (активный) + NavItem( + icon = Icons.Filled.CalendarToday, + text = "Расписание", + isSelected = true, + onClick = { } + ) + + // Награды + NavItem( + icon = Icons.Filled.Star, + text = "Награды", + isSelected = false, + onClick = { navController?.navigate("rewards") } + ) + + // Профиль + NavItem( + icon = Icons.Filled.Person, + text = "Профиль", + isSelected = false, + onClick = { navController?.navigate("settings") } + ) + + // Земля + NavItem( + icon = Icons.Filled.Public, + text = "Земля", + isSelected = false, + onClick = { navController?.navigate("ai") } + ) + } + } + + // Основная область + Column( + modifier = Modifier + .weight(1f) + .fillMaxHeight() + .padding(24.dp) + ) { + // Верхняя панель: только дата/время справа + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically + ) { + // Дата и время (увеличены в 2 раза, зеленый цвет) + Column( + horizontalAlignment = Alignment.End + ) { + val dateTextSize = (screenHeightDp * 0.06f).toInt().coerceIn(48, 80).sp + Text( + text = "$dateOnly $dayOfWeek", + fontSize = dateTextSize, + fontWeight = FontWeight.Bold, + color = accentGreen + ) + Text( + text = currentTime, + fontSize = dateTextSize, + fontWeight = FontWeight.Bold, + color = accentGreen + ) + } + } + + Spacer(modifier = Modifier.height(48.dp)) + + // Основной контент: только сегодняшняя дата (опущена ниже) + DateSection( + date = dateOnly, + dayOfWeek = dayOfWeek, + dateCardColor = dateCardColor, + accentGreen = accentGreen, + screenHeightDp = screenHeightDp, + tasks = uiState.tasks, + onAddClick = { viewModel.showAddDialog() } + ) + } + } + + // Диалог выбора задачи + if (uiState.showAddDialog) { + AddTaskDialog( + selectedTaskType = uiState.selectedTaskType, + onTaskTypeSelected = { viewModel.selectTaskType(it) }, + onSelect = { viewModel.addTask() }, + onDismiss = { viewModel.hideAddDialog() }, + accentGreen = accentGreen, + screenHeightDp = screenHeightDp + ) + } + } +} + +@Composable +fun NavItem( + icon: androidx.compose.ui.graphics.vector.ImageVector, + text: String, + isSelected: Boolean, + onClick: () -> Unit +) { + val iconSize = 40.dp + val textSize = 18.sp // Увеличен размер текста + + Column( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(16.dp)) + .clickable { onClick() } + .background( + color = if (isSelected) Color.White.copy(alpha = 0.3f) else Color.Transparent + ) + .padding(12.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + imageVector = icon, + contentDescription = text, + modifier = Modifier.size(iconSize), + tint = LoginGreenAccent + ) + Text( + text = text, + fontSize = textSize, + fontWeight = FontWeight.Bold, // Увеличена жирность для читаемости + color = Color.Black // Черный цвет для лучшей видимости + ) + } +} + +@Composable +fun DateSection( + date: String, + dayOfWeek: String, + dateCardColor: Color, + accentGreen: Color, + screenHeightDp: Int, + tasks: List, + onAddClick: () -> Unit +) { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + // Дата и кнопка + в одной строке + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // Заголовок даты (в две строки) + Box( + modifier = Modifier + .wrapContentWidth() + .clip(RoundedCornerShape(16.dp)) + .background(color = dateCardColor) + .padding(horizontal = 20.dp, vertical = 12.dp) + ) { + val dateTextSize = (screenHeightDp * 0.025f).toInt().coerceIn(20, 32).sp + Column( + horizontalAlignment = Alignment.Start + ) { + Text( + text = date, + fontSize = dateTextSize, + fontWeight = FontWeight.Bold, + color = Color.Black + ) + Text( + text = dayOfWeek, + fontSize = dateTextSize, + fontWeight = FontWeight.Bold, + color = Color.Black + ) + } + } + + // Кнопка добавления (зеленая круглая с плюсом) + FloatingActionButton( + onClick = onAddClick, + modifier = Modifier.size(80.dp), + containerColor = accentGreen, + contentColor = Color.White + ) { + Icon( + imageVector = Icons.Default.Add, + contentDescription = "Добавить", + modifier = Modifier.size(40.dp) + ) + } + } + + // Задачи в ряд + if (tasks.isNotEmpty()) { + LazyRow( + horizontalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.fillMaxWidth() + ) { + items(tasks) { task -> + TaskCard( + taskType = task, + screenHeightDp = screenHeightDp + ) + } + } + } + } +} + +@Composable +fun TaskCard( + taskType: TaskType, + screenHeightDp: Int +) { + val cardWidth = 200.dp + val cardHeight = 180.dp + + Box( + modifier = Modifier + .width(cardWidth) + .height(cardHeight) + .clip(RoundedCornerShape(16.dp)) + .background(color = Color.LightGray) + ) { + Column( + modifier = Modifier.fillMaxSize() + ) { + // Верхняя часть (для изображения) + Box( + modifier = Modifier + .fillMaxWidth() + .weight(0.6f) + .background(color = Color.White.copy(alpha = 0.5f)) + ) + + // Нижняя часть (для текста) + Box( + modifier = Modifier + .fillMaxWidth() + .weight(0.4f) + .background(color = Color.LightGray) + .padding(12.dp), + contentAlignment = Alignment.Center + ) { + val textSize = (screenHeightDp * 0.02f).toInt().coerceIn(16, 24).sp + Text( + text = taskType.title, + fontSize = textSize, + fontWeight = FontWeight.Bold, + color = Color.Black, + textAlign = TextAlign.Center + ) + } + } + } +} + +@Composable +fun AddTaskDialog( + selectedTaskType: TaskType?, + onTaskTypeSelected: (TaskType) -> Unit, + onSelect: () -> Unit, + onDismiss: () -> Unit, + accentGreen: Color, + screenHeightDp: Int +) { + Dialog(onDismissRequest = onDismiss) { + Card( + modifier = Modifier + .fillMaxWidth(0.8f) + .wrapContentHeight(), + shape = RoundedCornerShape(24.dp), + colors = CardDefaults.cardColors( + containerColor = Color.White + ) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(24.dp) + ) { + // Заголовок + val titleSize = (screenHeightDp * 0.03f).toInt().coerceIn(24, 36).sp + Text( + text = "Выберите задачу", + fontSize = titleSize, + fontWeight = FontWeight.Bold, + color = Color.Black + ) + + // Опции выбора + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.fillMaxWidth() + ) { + // Подарок + TaskOption( + taskType = TaskType.Gift, + isSelected = selectedTaskType == TaskType.Gift, + onClick = { onTaskTypeSelected(TaskType.Gift) }, + accentGreen = accentGreen, + screenHeightDp = screenHeightDp + ) + + // Кушать ложкой + TaskOption( + taskType = TaskType.EatWithSpoon, + isSelected = selectedTaskType == TaskType.EatWithSpoon, + onClick = { onTaskTypeSelected(TaskType.EatWithSpoon) }, + accentGreen = accentGreen, + screenHeightDp = screenHeightDp + ) + } + + // Кнопки внизу + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + // Кнопка "Назад" + Button( + onClick = onDismiss, + modifier = Modifier + .weight(1f) + .height(56.dp), + shape = RoundedCornerShape(16.dp), + colors = ButtonDefaults.buttonColors( + containerColor = Color.LightGray, + contentColor = Color.Black + ) + ) { + val buttonTextSize = (screenHeightDp * 0.022f).toInt().coerceIn(18, 26).sp + Text( + text = "Назад", + fontSize = buttonTextSize, + fontWeight = FontWeight.Bold + ) + } + + // Кнопка "Выбрать" + Button( + onClick = onSelect, + modifier = Modifier + .weight(1f) + .height(56.dp), + enabled = selectedTaskType != null, + shape = RoundedCornerShape(16.dp), + colors = ButtonDefaults.buttonColors( + containerColor = accentGreen, + contentColor = Color.White, + disabledContainerColor = Color.LightGray, + disabledContentColor = Color.Gray + ) + ) { + val buttonTextSize = (screenHeightDp * 0.022f).toInt().coerceIn(18, 26).sp + Text( + text = "Выбрать", + fontSize = buttonTextSize, + fontWeight = FontWeight.Bold + ) } } } @@ -68,3 +473,34 @@ fun ScheduleScreen( } } +@Composable +fun TaskOption( + taskType: TaskType, + isSelected: Boolean, + onClick: () -> Unit, + accentGreen: Color, + screenHeightDp: Int +) { + val borderWidth = if (isSelected) 4.dp else 2.dp + val borderColor = if (isSelected) accentGreen else Color.Gray + + Box( + modifier = Modifier + .fillMaxWidth() + .height(120.dp) + .clip(RoundedCornerShape(16.dp)) + .border(borderWidth, borderColor, RoundedCornerShape(16.dp)) + .background(color = if (isSelected) accentGreen.copy(alpha = 0.1f) else Color.Transparent) + .clickable { onClick() }, + contentAlignment = Alignment.Center + ) { + val textSize = (screenHeightDp * 0.025f).toInt().coerceIn(20, 28).sp + Text( + text = taskType.title, + fontSize = textSize, + fontWeight = FontWeight.Bold, + color = if (isSelected) accentGreen else Color.Black + ) + } +} + diff --git a/app/src/main/java/com/novayaplaneta/ui/screens/schedule/ScheduleViewModel.kt b/app/src/main/java/com/novayaplaneta/ui/screens/schedule/ScheduleViewModel.kt index e2d5ac8..b8d5ec8 100644 --- a/app/src/main/java/com/novayaplaneta/ui/screens/schedule/ScheduleViewModel.kt +++ b/app/src/main/java/com/novayaplaneta/ui/screens/schedule/ScheduleViewModel.kt @@ -10,6 +10,11 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import javax.inject.Inject +sealed class TaskType(val title: String) { + object Gift : TaskType("Подарок") + object EatWithSpoon : TaskType("Кушать ложкой") +} + @HiltViewModel class ScheduleViewModel @Inject constructor( private val getSchedulesUseCase: GetSchedulesUseCase @@ -29,10 +34,35 @@ class ScheduleViewModel @Inject constructor( } } } + + fun showAddDialog() { + _uiState.value = _uiState.value.copy(showAddDialog = true, selectedTaskType = null) + } + + fun hideAddDialog() { + _uiState.value = _uiState.value.copy(showAddDialog = false, selectedTaskType = null) + } + + fun selectTaskType(taskType: TaskType) { + _uiState.value = _uiState.value.copy(selectedTaskType = taskType) + } + + fun addTask() { + val selected = _uiState.value.selectedTaskType ?: return + val newTasks = _uiState.value.tasks + selected + _uiState.value = _uiState.value.copy( + tasks = newTasks, + showAddDialog = false, + selectedTaskType = null + ) + } } data class ScheduleUiState( val schedules: List = emptyList(), - val isLoading: Boolean = false + val tasks: List = emptyList(), + val isLoading: Boolean = false, + val showAddDialog: Boolean = false, + val selectedTaskType: TaskType? = null ) diff --git a/app/src/main/java/com/novayaplaneta/ui/screens/settings/SettingsScreen.kt b/app/src/main/java/com/novayaplaneta/ui/screens/settings/SettingsScreen.kt index 3a8d511..d47a3f1 100644 --- a/app/src/main/java/com/novayaplaneta/ui/screens/settings/SettingsScreen.kt +++ b/app/src/main/java/com/novayaplaneta/ui/screens/settings/SettingsScreen.kt @@ -1,74 +1,312 @@ package com.novayaplaneta.ui.screens.settings +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.* import androidx.compose.material3.* import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavController +import com.novayaplaneta.ui.components.NovayaPlanetaLogo +import com.novayaplaneta.ui.theme.* +import java.text.SimpleDateFormat +import java.util.* -@OptIn(ExperimentalMaterial3Api::class) @Composable fun SettingsScreen( + navController: NavController? = null, viewModel: SettingsViewModel = hiltViewModel(), modifier: Modifier = Modifier ) { val uiState by viewModel.uiState.collectAsState() + val configuration = LocalConfiguration.current + val screenWidthDp = configuration.screenWidthDp + val screenHeightDp = configuration.screenHeightDp - Scaffold( - topBar = { - TopAppBar( - title = { Text("Настройки") } - ) + // Навигация на экран входа при выходе + LaunchedEffect(uiState.isLoggedOut) { + if (uiState.isLoggedOut) { + navController?.navigate("login") { + popUpTo(0) { inclusive = true } + } } - ) { paddingValues -> - Column( - modifier = modifier - .fillMaxSize() - .padding(paddingValues) - .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) + } + + // Цвета из autism-friendly палитры + val backgroundColor = LoginBackgroundTurquoise + val navPanelColor = LoginInputLightBlue + val accentGreen = LoginGreenAccent + val dateCardColor = LoginInputLightBlue + + // Форматирование даты и времени по московскому времени + val moscowTimeZone = TimeZone.getTimeZone("Europe/Moscow") + val dateFormat = SimpleDateFormat("dd.MM EEEE", Locale("ru")).apply { + timeZone = moscowTimeZone + } + val timeFormat = SimpleDateFormat("HH:mm", Locale("ru")).apply { + timeZone = moscowTimeZone + } + val currentDate = dateFormat.format(Date()) + val currentTime = timeFormat.format(Date()) + val dayOfWeek = currentDate.split(" ").getOrNull(1) ?: "" + val dateOnly = currentDate.split(" ").getOrNull(0) ?: "" + + Box( + modifier = modifier + .fillMaxSize() + .background(color = backgroundColor) + ) { + Row( + modifier = Modifier.fillMaxSize() ) { - uiState.currentUser?.let { user -> - Card( - modifier = Modifier.fillMaxWidth() + // Левая панель навигации с логотипом над ней (с отступами и закругленными краями) + Column( + modifier = Modifier + .width((screenWidthDp * 0.22f).dp.coerceIn(160.dp, 240.dp)) + .fillMaxHeight() + .padding(vertical = 20.dp, horizontal = 16.dp) + ) { + // Логотип над панелью навигации (увеличен) + Box( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 16.dp), + contentAlignment = Alignment.Center ) { - Column( - modifier = Modifier.padding(16.dp) - ) { - Text( - text = "Пользователь", - style = MaterialTheme.typography.titleLarge - ) - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = "Имя: ${user.name}", - style = MaterialTheme.typography.bodyLarge - ) - Text( - text = "Email: ${user.email}", - style = MaterialTheme.typography.bodyLarge - ) - Text( - text = "Роль: ${user.role.name}", - style = MaterialTheme.typography.bodyLarge - ) - } + val logoSize = (screenHeightDp * 0.18f).toInt().coerceIn(120, 200).dp + NovayaPlanetaLogo(size = logoSize) + } + + // Панель навигации (закругленная со всех сторон) + Column( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .clip(RoundedCornerShape(32.dp)) + .background(color = navPanelColor) + .padding(vertical = 24.dp, horizontal = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(32.dp) + ) { + // Расписание + NavItem( + icon = Icons.Filled.CalendarToday, + text = "Расписание", + isSelected = false, + onClick = { navController?.navigate("schedule") } + ) + + // Награды + NavItem( + icon = Icons.Filled.Star, + text = "Награды", + isSelected = false, + onClick = { navController?.navigate("rewards") } + ) + + // Профиль (активный) + NavItem( + icon = Icons.Filled.Person, + text = "Профиль", + isSelected = true, + onClick = { } + ) + + // Земля + NavItem( + icon = Icons.Filled.Public, + text = "Земля", + isSelected = false, + onClick = { navController?.navigate("ai") } + ) } } - Button( - onClick = { viewModel.logout() }, - modifier = Modifier.fillMaxWidth(), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.error - ) + // Основная область + Column( + modifier = Modifier + .weight(1f) + .fillMaxHeight() + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally ) { - Text("Выйти") + // Верхняя панель: только дата/время справа + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically + ) { + // Дата и время (зеленый цвет) + Column( + horizontalAlignment = Alignment.End + ) { + val dateTextSize = (screenHeightDp * 0.06f).toInt().coerceIn(48, 80).sp + Text( + text = "$dateOnly $dayOfWeek", + fontSize = dateTextSize, + fontWeight = FontWeight.Bold, + color = accentGreen + ) + Text( + text = currentTime, + fontSize = dateTextSize, + fontWeight = FontWeight.Bold, + color = accentGreen + ) + } + } + + Spacer(modifier = Modifier.height(48.dp)) + + // Фото профиля (placeholder) + val photoSize = (screenHeightDp * 0.2f).toInt().coerceIn(150, 250).dp + Box( + modifier = Modifier + .size(photoSize) + .clip(CircleShape) + .background(color = dateCardColor), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.Filled.CameraAlt, + contentDescription = "Фото профиля", + modifier = Modifier.size(photoSize * 0.4f), + tint = Color.Gray + ) + } + + Spacer(modifier = Modifier.height(32.dp)) + + // Карточка с данными пользователя + val user = uiState.currentUser + if (user != null) { + Box( + modifier = Modifier + .fillMaxWidth(0.7f) + .clip(RoundedCornerShape(20.dp)) + .background(color = dateCardColor) + .padding(24.dp) + ) { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + val textSize = (screenHeightDp * 0.025f).toInt().coerceIn(20, 28).sp + UserInfoRow( + label = "Имя:", + value = user.name, + textSize = textSize + ) + UserInfoRow( + label = "Логин:", + value = "${user.name}12", // Используем имя + число как логин + textSize = textSize + ) + UserInfoRow( + label = "email:", + value = user.email, + textSize = textSize + ) + } + } + + Spacer(modifier = Modifier.height(24.dp)) + + // Кнопка "Выйти" + TextButton( + onClick = { viewModel.logout() }, + modifier = Modifier.fillMaxWidth(0.7f) + ) { + val buttonTextSize = (screenHeightDp * 0.025f).toInt().coerceIn(20, 28).sp + Text( + text = "Выйти", + fontSize = buttonTextSize, + fontWeight = FontWeight.Bold, + color = Color.Red + ) + } + } } } } } +@Composable +fun NavItem( + icon: androidx.compose.ui.graphics.vector.ImageVector, + text: String, + isSelected: Boolean, + onClick: () -> Unit +) { + val iconSize = 40.dp + val textSize = 18.sp + + Column( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(16.dp)) + .clickable { onClick() } + .background( + color = if (isSelected) Color.White.copy(alpha = 0.3f) else Color.Transparent + ) + .padding(12.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + imageVector = icon, + contentDescription = text, + modifier = Modifier.size(iconSize), + tint = LoginGreenAccent + ) + Text( + text = text, + fontSize = textSize, + fontWeight = FontWeight.Bold, + color = Color.Black + ) + } +} + +@Composable +fun UserInfoRow( + label: String, + value: String, + textSize: androidx.compose.ui.unit.TextUnit +) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Start + ) { + Text( + text = label, + fontSize = textSize, + fontWeight = FontWeight.Medium, + color = Color.Black + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = value, + fontSize = textSize, + fontWeight = FontWeight.Bold, + color = Color.Black + ) + } +} + diff --git a/app/src/main/java/com/novayaplaneta/ui/screens/settings/SettingsViewModel.kt b/app/src/main/java/com/novayaplaneta/ui/screens/settings/SettingsViewModel.kt index 26d32dc..78c36c9 100644 --- a/app/src/main/java/com/novayaplaneta/ui/screens/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/novayaplaneta/ui/screens/settings/SettingsViewModel.kt @@ -2,11 +2,14 @@ package com.novayaplaneta.ui.screens.settings import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.novayaplaneta.domain.model.User +import com.novayaplaneta.domain.model.UserRole import com.novayaplaneta.domain.repository.AuthRepository import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import javax.inject.Inject @@ -24,12 +27,34 @@ class SettingsViewModel @Inject constructor( private fun loadCurrentUser() { viewModelScope.launch { - authRepository.getCurrentUser().collect { user -> - _uiState.value = _uiState.value.copy(currentUser = user) + // Пытаемся получить пользователя из базы + try { + val user = authRepository.getCurrentUser().first() + + // Если пользователя нет, используем заглушку + _uiState.value = _uiState.value.copy( + currentUser = user ?: getDefaultUser() + ) + } catch (e: Exception) { + // Если произошла ошибка, используем заглушку + _uiState.value = _uiState.value.copy( + currentUser = getDefaultUser() + ) } } } + private fun getDefaultUser(): User { + // Заглушка с тестовыми данными + return User( + id = "user_123", + name = "Коля", + email = "kolya12@mail.ru", + role = UserRole.CHILD, + token = null + ) + } + fun logout() { viewModelScope.launch { authRepository.logout() @@ -39,7 +64,7 @@ class SettingsViewModel @Inject constructor( } data class SettingsUiState( - val currentUser: com.novayaplaneta.domain.model.User? = null, + val currentUser: User? = null, val isLoggedOut: Boolean = false )