Compare commits

...

3 Commits

14 changed files with 2215 additions and 181 deletions

View File

@@ -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
)
)
}
}

View File

@@ -6,7 +6,9 @@ import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import com.novayaplaneta.ui.screens.ai.AIScreen
import com.novayaplaneta.ui.screens.auth.ForgotPasswordScreen
import com.novayaplaneta.ui.screens.auth.LoginScreen
import com.novayaplaneta.ui.screens.auth.RegistrationScreen
import com.novayaplaneta.ui.screens.rewards.RewardsScreen
import com.novayaplaneta.ui.screens.schedule.ScheduleScreen
import com.novayaplaneta.ui.screens.settings.SettingsScreen
@@ -27,14 +29,14 @@ fun NewPlanetNavigation(
composable("login") {
LoginScreen(navController = navController)
}
// composable("registration") {
// RegistrationScreen(navController = navController)
// }
// composable("forgot_password") {
// ForgotPasswordScreen(navController = navController)
// }
composable("registration") {
RegistrationScreen(navController = navController)
}
composable("forgot_password") {
ForgotPasswordScreen(navController = navController)
}
composable("schedule") {
ScheduleScreen()
ScheduleScreen(navController = navController)
}
composable("tasks") {
TaskScreen()
@@ -43,13 +45,13 @@ fun NewPlanetNavigation(
TimerScreen()
}
composable("rewards") {
RewardsScreen()
RewardsScreen(navController = navController)
}
composable("ai") {
AIScreen()
}
composable("settings") {
SettingsScreen()
SettingsScreen(navController = navController)
}
}
}

View File

@@ -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()
}
}

View File

@@ -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))
}
}
}
}

View File

@@ -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<ForgotPasswordUiState> = _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
}

View File

@@ -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

View File

@@ -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))
}
}
}
}

View File

@@ -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<RegistrationUiState> = _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()
}

View File

@@ -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
)
}
}
}
}

View File

@@ -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<RewardsUiState> = _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<com.novayaplaneta.domain.model.Reward> = emptyList(),
val rewards: List<Reward> = emptyList(),
val isLoading: Boolean = false
)

View File

@@ -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<TaskType>,
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
)
}
}

View File

@@ -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<com.novayaplaneta.domain.model.Schedule> = emptyList(),
val isLoading: Boolean = false
val tasks: List<TaskType> = emptyList(),
val isLoading: Boolean = false,
val showAddDialog: Boolean = false,
val selectedTaskType: TaskType? = null
)

View File

@@ -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
)
}
}

View File

@@ -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
)