Compare commits

..

8 Commits

18 changed files with 2808 additions and 303 deletions

2
.idea/gradle.xml generated
View File

@@ -4,7 +4,6 @@
<component name="GradleSettings"> <component name="GradleSettings">
<option name="linkedExternalProjectsSettings"> <option name="linkedExternalProjectsSettings">
<GradleProjectSettings> <GradleProjectSettings>
<option name="testRunner" value="CHOOSE_PER_TEST" />
<option name="externalProjectPath" value="$PROJECT_DIR$" /> <option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" /> <option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="modules"> <option name="modules">
@@ -13,6 +12,7 @@
<option value="$PROJECT_DIR$/app" /> <option value="$PROJECT_DIR$/app" />
</set> </set>
</option> </option>
<option name="resolveExternalAnnotations" value="false" />
</GradleProjectSettings> </GradleProjectSettings>
</option> </option>
</component> </component>

View File

@@ -28,39 +28,14 @@ class MainActivity : ComponentActivity() {
val navBackStackEntry by navController.currentBackStackEntryAsState() val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route val currentRoute = navBackStackEntry?.destination?.route
// Нижняя панель не показывается на экранах входа, регистрации и восстановления пароля // Нижняя панель скрыта везде (используется левая панель навигации)
val showBottomBar = currentRoute != null &&
currentRoute != "login" &&
currentRoute != "registration" &&
currentRoute != "forgot_password"
Scaffold( Scaffold(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize()
bottomBar = {
if (showBottomBar) {
BottomNavigationBar(
currentRoute = currentRoute,
onNavigate = { route ->
navController.navigate(route) {
popUpTo(navController.graph.startDestinationId) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
}
)
}
}
) { innerPadding -> ) { innerPadding ->
NewPlanetNavigation( NewPlanetNavigation(
navController = navController, navController = navController,
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(
// Убираем нижний отступ на экране входа
bottom = if (showBottomBar) innerPadding.calculateBottomPadding() else 0.dp
)
) )
} }
} }

View File

@@ -22,10 +22,6 @@ class AIRepositoryImpl @Inject constructor(
override suspend fun sendMessage(userId: String, message: String): Result<String> { override suspend fun sendMessage(userId: String, message: String): Result<String> {
return try { return try {
// Get token from current user
val user = authRepository.getCurrentUser().first()
val token = user?.token ?: ""
// Save user message // Save user message
val userMessage = ChatMessage( val userMessage = ChatMessage(
id = UUID.randomUUID().toString(), id = UUID.randomUUID().toString(),
@@ -36,11 +32,10 @@ class AIRepositoryImpl @Inject constructor(
) )
chatMessageDao.insertMessage(userMessage.toEntity()) chatMessageDao.insertMessage(userMessage.toEntity())
// Send to API (only if token exists) // Тестовый ответ для демонстрации
if (token.isNotEmpty()) { kotlinx.coroutines.delay(1500) // Небольшая задержка для анимации
val response = api.chatWithAI("Bearer $token", ChatRequest(message))
if (response.isSuccessful && response.body() != null) { val aiResponse = "!!!!"
val aiResponse = response.body()!!.message
// Save AI response // Save AI response
val aiMessage = ChatMessage( val aiMessage = ChatMessage(
@@ -53,12 +48,6 @@ class AIRepositoryImpl @Inject constructor(
chatMessageDao.insertMessage(aiMessage.toEntity()) chatMessageDao.insertMessage(aiMessage.toEntity())
Result.success(aiResponse) Result.success(aiResponse)
} else {
Result.failure(Exception("Failed to get AI response"))
}
} else {
Result.failure(Exception("User not authenticated"))
}
} catch (e: Exception) { } catch (e: Exception) {
Result.failure(e) Result.failure(e)
} }

View File

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

View File

@@ -1,94 +1,387 @@
package com.novayaplaneta.ui.screens.ai package com.novayaplaneta.ui.screens.ai
import androidx.compose.animation.core.*
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
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.Send
import androidx.compose.material.icons.filled.Star
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import kotlin.math.sin
import androidx.compose.ui.unit.lerp
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import com.novayaplaneta.R
import com.novayaplaneta.ui.components.NovayaPlanetaLogo
import com.novayaplaneta.ui.theme.*
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun AIScreen( fun AIScreen(
navController: androidx.navigation.NavController? = null,
viewModel: AIViewModel = hiltViewModel(), viewModel: AIViewModel = hiltViewModel(),
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val uiState by viewModel.uiState.collectAsState() val uiState by viewModel.uiState.collectAsState()
var messageText by remember { mutableStateOf("") } var messageText by remember { mutableStateOf("") }
Scaffold( // Загружаем историю чата при первом открытии экрана
topBar = { LaunchedEffect(Unit) {
TopAppBar( viewModel.loadChatHistory("default")
title = { Text("ИИ-помощник \"Земля\"") }
)
} }
) { paddingValues ->
Column( val configuration = LocalConfiguration.current
val screenWidthDp = configuration.screenWidthDp
val screenHeightDp = configuration.screenHeightDp
// Цвета из autism-friendly палитры
val backgroundColor = LoginBackgroundTurquoise
val navPanelColor = LoginInputLightBlue
val accentGreen = LoginGreenAccent
// Состояние прокрутки чата
val listState = rememberLazyListState()
// Автопрокрутка вниз при новых сообщениях
LaunchedEffect(uiState.messages.size) {
if (uiState.messages.isNotEmpty()) {
listState.animateScrollToItem(uiState.messages.size - 1)
}
}
Box(
modifier = modifier modifier = modifier
.fillMaxSize() .fillMaxSize()
.padding(paddingValues) .background(color = backgroundColor)
) { ) {
LazyColumn(
modifier = Modifier
.weight(1f)
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(uiState.messages) { message ->
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = if (message.isFromAI) {
MaterialTheme.colorScheme.primaryContainer
} else {
MaterialTheme.colorScheme.surfaceVariant
}
)
) {
Text(
text = message.message,
modifier = Modifier.padding(16.dp),
style = MaterialTheme.typography.bodyLarge
)
}
}
}
if (uiState.isLoading) {
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
}
Row( Row(
modifier = Modifier.fillMaxSize()
) {
// Левая панель навигации с логотипом над ней
Column(
modifier = Modifier
.width((screenWidthDp * 0.22f).dp.coerceIn(160.dp, 240.dp))
.fillMaxHeight()
.padding(vertical = 20.dp, horizontal = 16.dp)
) {
// Логотип над панелью навигации
Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(16.dp), .padding(bottom = 16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp) contentAlignment = Alignment.Center
) {
val logoSize = (screenHeightDp * 0.22f).toInt().coerceIn(140, 240).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 = false,
onClick = { navController?.navigate("settings") }
)
NavItem(
icon = Icons.Filled.Public,
text = "Земля",
isSelected = true,
onClick = { }
)
}
}
// Основная область с AI агентом и чатом
Row(
modifier = Modifier
.weight(1f)
.fillMaxHeight()
.padding(24.dp),
horizontalArrangement = Arrangement.spacedBy(24.dp)
) {
// Левая часть: Анимированная картинка AI агента
Column(
modifier = Modifier
.width((screenWidthDp * 0.3f).dp.coerceIn(200.dp, 400.dp))
.fillMaxHeight(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
AnimatedAIAgent(
isSpeaking = uiState.isLoading,
screenHeightDp = screenHeightDp
)
}
// Правая часть: Чат
Column(
modifier = Modifier
.weight(1f)
.fillMaxHeight()
.clip(RoundedCornerShape(24.dp))
.background(color = LoginCardLightBlue)
.padding(24.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)
)
// Список сообщений
LazyColumn(
state = listState,
modifier = Modifier
.weight(1f)
.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
items(uiState.messages) { message ->
ChatBubble(
message = message.message,
isFromAI = message.isFromAI,
screenHeightDp = screenHeightDp,
accentGreen = accentGreen
)
}
}
Spacer(modifier = Modifier.height(16.dp))
// Поле ввода и кнопка отправки
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.Bottom
) { ) {
OutlinedTextField( OutlinedTextField(
value = messageText, value = messageText,
onValueChange = { messageText = it }, onValueChange = { messageText = it },
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
placeholder = { Text("Введите сообщение...") } placeholder = {
Text(
text = "Напишите сообщение...",
color = Color.Gray.copy(alpha = 0.7f)
) )
},
singleLine = false,
maxLines = 3,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text),
colors = OutlinedTextFieldDefaults.colors(
focusedTextColor = Color.Black,
unfocusedTextColor = Color.Black,
focusedBorderColor = accentGreen,
unfocusedBorderColor = Color.Gray
),
shape = RoundedCornerShape(20.dp)
)
Button( Button(
onClick = { onClick = {
if (messageText.isNotBlank()) { if (messageText.isNotBlank()) {
viewModel.sendMessage("userId", messageText) viewModel.sendMessage("default", messageText)
messageText = "" messageText = ""
} }
} },
modifier = Modifier
.height(56.dp)
.width(56.dp),
shape = RoundedCornerShape(20.dp),
colors = ButtonDefaults.buttonColors(
containerColor = accentGreen,
contentColor = Color.White
),
enabled = messageText.isNotBlank() && !uiState.isLoading
) { ) {
Text("Отправить") Icon(
imageVector = Icons.Filled.Send,
contentDescription = "Отправить",
modifier = Modifier.size(24.dp)
)
}
}
} }
} }
} }
} }
} }
@Composable
fun AnimatedAIAgent(
isSpeaking: Boolean,
screenHeightDp: Int
) {
val infiniteTransition = rememberInfiniteTransition(label = "ai_agent_animation")
// Анимация покачивания Земли - более быстрое когда говорит
val bounceFloat by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 2f * kotlin.math.PI.toFloat(),
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = if (isSpeaking) 400 else 2000,
easing = LinearEasing
),
repeatMode = RepeatMode.Reverse
),
label = "bounce_float"
)
// Интенсивное покачивание когда говорит, легкое когда молчит
val offsetY = if (isSpeaking) {
sin(bounceFloat) * 20f // Активное прыгание при ответе
} else {
sin(bounceFloat) * 8f // Легкое покачивание в покое
}
val earthSize = (screenHeightDp * 0.4f).dp.coerceIn(200.dp, 400.dp)
Box(
modifier = Modifier
.size(earthSize),
contentAlignment = Alignment.Center
) {
// Изображение Земли с покачиванием
androidx.compose.foundation.Image(
painter = painterResource(id = R.drawable.logo_earth),
contentDescription = "AI Агент Земля",
modifier = Modifier
.fillMaxSize()
.offset(y = offsetY.dp),
contentScale = ContentScale.Fit
)
}
}
@Composable
fun ChatBubble(
message: String,
isFromAI: Boolean,
screenHeightDp: Int,
accentGreen: Color
) {
val textSize = (screenHeightDp * 0.02f).toInt().coerceIn(16, 20).sp
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = if (isFromAI) Arrangement.Start else Arrangement.End
) {
Card(
modifier = Modifier.widthIn(max = (screenHeightDp * 0.4f).dp.coerceIn(200.dp, 400.dp)),
shape = RoundedCornerShape(
topStart = 20.dp,
topEnd = 20.dp,
bottomStart = if (isFromAI) 4.dp else 20.dp,
bottomEnd = if (isFromAI) 20.dp else 4.dp
),
colors = CardDefaults.cardColors(
containerColor = if (isFromAI) {
LoginGreenSoft // Мягкий зеленый для AI
} else {
LoginGreenAccent // Зеленый акцент для пользователя
}
)
) {
Text(
text = message,
modifier = Modifier.padding(16.dp),
fontSize = textSize,
color = Color.Black,
fontWeight = FontWeight.Medium
)
}
}
}
@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
)
}
}

View File

@@ -0,0 +1,15 @@
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,371 @@
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
Box(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = horizontalPadding, vertical = verticalPadding)
) {
// Логотип вверху слева - увеличенный размер
val logoSizeByHeight = (screenHeightDp * 0.28f).toInt()
val logoSizeByWidth = screenWidthDp / 3
val logoSize = min(logoSizeByHeight, logoSizeByWidth).coerceIn(180, 280).dp
NovayaPlanetaLogo(
modifier = Modifier
.align(Alignment.TopStart)
.padding(bottom = 4.dp)
.clickable {
navController.navigate("login") {
popUpTo("login") { inclusive = false }
}
},
size = logoSize
)
// Центрированный контент - адаптивная ширина (50-70% экрана)
val contentWidthRatio = if (isLandscape) 0.5f else 0.7f
Column(
modifier = Modifier
.align(Alignment.Center)
.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
)
}
}
}
}
}
}
}

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

@@ -76,33 +76,28 @@ fun LoginScreen(
val horizontalPadding = (screenWidthDp * 0.04f).toInt().coerceIn(24, 48).dp val horizontalPadding = (screenWidthDp * 0.04f).toInt().coerceIn(24, 48).dp
val verticalPadding = (screenHeightDp * 0.03f).toInt().coerceIn(16, 32).dp val verticalPadding = (screenHeightDp * 0.03f).toInt().coerceIn(16, 32).dp
Column( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(horizontal = horizontalPadding, vertical = verticalPadding), .padding(horizontal = horizontalPadding, vertical = verticalPadding)
horizontalAlignment = Alignment.CenterHorizontally
) { ) {
// Логотип вверху слева - уменьшенный размер // Логотип вверху слева - большой размер
val logoSizeByHeight = (screenHeightDp * 0.15f).toInt() val logoSizeByHeight = (screenHeightDp * 0.28f).toInt()
val logoSizeByWidth = screenWidthDp / 5 val logoSizeByWidth = screenWidthDp / 3
val logoSize = min(logoSizeByHeight, logoSizeByWidth).coerceIn(100, 160).dp val logoSize = min(logoSizeByHeight, logoSizeByWidth).coerceIn(180, 280).dp
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Start
) {
NovayaPlanetaLogo( NovayaPlanetaLogo(
modifier = Modifier.padding(bottom = 4.dp), modifier = Modifier
.align(Alignment.TopStart)
.padding(bottom = 4.dp),
size = logoSize size = logoSize
) )
}
Spacer(modifier = Modifier.weight(0.05f))
// Центрированный контент - адаптивная ширина (50-70% экрана) // Центрированный контент - адаптивная ширина (50-70% экрана)
val contentWidthRatio = if (isLandscape) 0.5f else 0.7f val contentWidthRatio = if (isLandscape) 0.5f else 0.7f
Column( Column(
modifier = Modifier modifier = Modifier
.align(Alignment.Center)
.fillMaxWidth(contentWidthRatio), .fillMaxWidth(contentWidthRatio),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(24.dp) verticalArrangement = Arrangement.spacedBy(24.dp)
@@ -212,10 +207,13 @@ fun LoginScreen(
// Кнопка "Войти" - адаптивный размер // Кнопка "Войти" - адаптивный размер
Button( Button(
onClick = { onClick = {
viewModel.login { if (uiState.isFormValid) {
// Переход на экран расписания при заполненных полях
navController.navigate("schedule") { navController.navigate("schedule") {
popUpTo(0) { inclusive = true } popUpTo(0) { inclusive = true }
} }
// Также вызываем логин для проверки через API (в фоне)
viewModel.login { }
} }
}, },
modifier = Modifier modifier = Modifier
@@ -225,7 +223,7 @@ fun LoginScreen(
enabled = uiState.isFormValid && !uiState.isLoading, enabled = uiState.isFormValid && !uiState.isLoading,
colors = if (uiState.isFormValid) { colors = if (uiState.isFormValid) {
ButtonDefaults.buttonColors( ButtonDefaults.buttonColors(
containerColor = LoginButtonBlue, containerColor = LoginGreenAccent,
contentColor = Color.White contentColor = Color.White
) )
} else { } else {
@@ -258,12 +256,12 @@ fun LoginScreen(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(12.dp) verticalArrangement = Arrangement.spacedBy(12.dp)
) { ) {
val linkTextSize = (screenHeightDp * 0.024f).toInt().coerceIn(18, 26).sp val linkTextSize = (screenHeightDp * 0.032f).toInt().coerceIn(24, 32).sp
Text( Text(
text = "Нет логина и пароля?", text = "Нет логина и пароля?",
fontSize = linkTextSize, fontSize = linkTextSize,
color = LoginGreenAccent, color = LoginGreenAccent,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Bold,
modifier = Modifier.clickable { modifier = Modifier.clickable {
navController.navigate("registration") navController.navigate("registration")
} }
@@ -273,15 +271,13 @@ fun LoginScreen(
text = "Не помнишь пароль?", text = "Не помнишь пароль?",
fontSize = linkTextSize, fontSize = linkTextSize,
color = LoginGreenAccent, color = LoginGreenAccent,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Bold,
modifier = Modifier.clickable { modifier = Modifier.clickable {
navController.navigate("forgot_password") navController.navigate("forgot_password")
} }
) )
} }
} }
Spacer(modifier = Modifier.weight(0.1f))
} }
} }
} }

View File

@@ -0,0 +1,411 @@
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
Box(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = horizontalPadding, vertical = verticalPadding)
) {
// Логотип вверху слева - увеличенный размер
val logoSizeByHeight = (screenHeightDp * 0.28f).toInt()
val logoSizeByWidth = screenWidthDp / 3
val logoSize = min(logoSizeByHeight, logoSizeByWidth).coerceIn(180, 280).dp
NovayaPlanetaLogo(
modifier = Modifier
.align(Alignment.TopStart)
.padding(bottom = 4.dp)
.clickable {
navController.navigate("login") {
popUpTo("login") { inclusive = false }
}
},
size = logoSize
)
// Центрированный контент - адаптивная ширина (50-70% экрана)
val contentWidthRatio = if (isLandscape) 0.5f else 0.7f
Column(
modifier = Modifier
.align(Alignment.Center)
.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
)
}
}
}
}
}
}
}

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,82 +1,465 @@
package com.novayaplaneta.ui.screens.rewards 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.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.items 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.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.material.icons.filled.Star
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.foundation.text.KeyboardOptions
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp 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 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 @Composable
fun RewardsScreen( fun RewardsScreen(
navController: androidx.navigation.NavController? = null,
viewModel: RewardsViewModel = hiltViewModel(), viewModel: RewardsViewModel = hiltViewModel(),
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val uiState by viewModel.uiState.collectAsState() val uiState by viewModel.uiState.collectAsState()
val configuration = LocalConfiguration.current
val screenWidthDp = configuration.screenWidthDp
val screenHeightDp = configuration.screenHeightDp
Scaffold( // Цвета из autism-friendly палитры
topBar = { val backgroundColor = LoginBackgroundTurquoise
TopAppBar( val navPanelColor = LoginInputLightBlue
title = { Text("Награды") } val accentGreen = LoginGreenAccent
) val dateCardColor = LoginInputLightBlue
// Форматирование даты и времени по московскому времени
val moscowTimeZone = TimeZone.getTimeZone("Europe/Moscow")
val dateFormat = SimpleDateFormat("dd.MM EEEE", Locale("ru")).apply {
timeZone = moscowTimeZone
} }
) { paddingValues -> val timeFormat = SimpleDateFormat("HH:mm", Locale("ru")).apply {
Column( 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 modifier = modifier
.fillMaxSize() .fillMaxSize()
.padding(paddingValues) .background(color = backgroundColor)
.padding(16.dp)
) {
if (uiState.isLoading) {
CircularProgressIndicator(
modifier = Modifier
.fillMaxWidth()
.wrapContentWidth()
)
} else {
LazyColumn(
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(uiState.rewards) { reward ->
Card(
modifier = Modifier.fillMaxWidth()
) { ) {
Row( Row(
modifier = Modifier.fillMaxSize()
) {
// Левая панель навигации с логотипом над ней (с отступами и закругленными краями)
Column(
modifier = Modifier
.width((screenWidthDp * 0.22f).dp.coerceIn(160.dp, 240.dp))
.fillMaxHeight()
.padding(vertical = 20.dp, horizontal = 16.dp)
) {
// Логотип над панелью навигации
Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(16.dp) .padding(bottom = 16.dp),
contentAlignment = Alignment.Center
) { ) {
Column(modifier = Modifier.weight(1f)) { val logoSize = (screenHeightDp * 0.22f).toInt().coerceIn(140, 240).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
)
}
// Кнопка добавления в конце списка
item {
AddRewardButton(
onClick = { viewModel.showAddDialog() },
screenHeightDp = screenHeightDp,
accentGreen = accentGreen
)
}
}
}
}
}
// Диалог добавления награды
if (uiState.showAddDialog) {
AddRewardDialog(
rewardTitle = uiState.newRewardTitle,
onTitleChange = { viewModel.updateNewRewardTitle(it) },
onAdd = { viewModel.addReward("default") },
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 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(
text = reward.title, text = reward.title,
style = MaterialTheme.typography.titleLarge fontSize = textSize,
) fontWeight = FontWeight.Bold,
reward.description?.let { color = Color.Black,
Text( textAlign = TextAlign.Center
text = it,
style = MaterialTheme.typography.bodyMedium
) )
} }
Text(
text = "${reward.points} очков",
style = MaterialTheme.typography.bodySmall
)
} }
if (reward.earnedAt != null) { }
}
@Composable
fun AddRewardButton(
onClick: () -> Unit,
screenHeightDp: Int,
accentGreen: Color
) {
val cardHeight = 180.dp
Box(
modifier = Modifier
.fillMaxWidth()
.height(cardHeight)
.clip(RoundedCornerShape(16.dp))
.background(color = accentGreen)
.clickable(onClick = onClick)
) {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Icon( Icon(
imageVector = androidx.compose.material.icons.Icons.Default.Star, imageVector = Icons.Filled.Add,
contentDescription = "Получено", contentDescription = "Добавить награду",
tint = MaterialTheme.colorScheme.primary modifier = Modifier.size(60.dp),
tint = Color.White
) )
} }
} }
} }
@Composable
fun AddRewardDialog(
rewardTitle: String,
onTitleChange: (String) -> Unit,
onAdd: () -> 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
)
// Поле ввода названия награды
val inputTextSize = (screenHeightDp * 0.02f).toInt().coerceIn(16, 22).sp
OutlinedTextField(
value = rewardTitle,
onValueChange = onTitleChange,
placeholder = {
Text(
text = "Введите название награды",
fontSize = inputTextSize,
color = Color.Gray.copy(alpha = 0.7f)
)
},
modifier = Modifier.fillMaxWidth(),
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text),
colors = OutlinedTextFieldDefaults.colors(
focusedTextColor = Color.Black,
unfocusedTextColor = Color.Black,
focusedBorderColor = accentGreen,
unfocusedBorderColor = Color.Gray
),
textStyle = androidx.compose.ui.text.TextStyle(
fontSize = inputTextSize
)
)
// Кнопки внизу
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 = onAdd,
modifier = Modifier
.weight(1f)
.height(56.dp),
enabled = rewardTitle.isNotBlank(),
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
)
} }
} }
} }

View File

@@ -2,6 +2,7 @@ package com.novayaplaneta.ui.screens.rewards
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.novayaplaneta.domain.model.Reward
import com.novayaplaneta.domain.repository.RewardRepository import com.novayaplaneta.domain.repository.RewardRepository
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@@ -18,12 +19,96 @@ class RewardsViewModel @Inject constructor(
private val _uiState = MutableStateFlow(RewardsUiState()) private val _uiState = MutableStateFlow(RewardsUiState())
val uiState: StateFlow<RewardsUiState> = _uiState.asStateFlow() 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) { fun loadRewards(userId: String) {
viewModelScope.launch { viewModelScope.launch {
_uiState.value = _uiState.value.copy(isLoading = true) _uiState.value = _uiState.value.copy(isLoading = true)
rewardRepository.getRewards(userId).collect { rewards -> rewardRepository.getRewards(userId).collect { rewards ->
_uiState.value = _uiState.value.copy( _uiState.value = _uiState.value.copy(
rewards = rewards, rewards = if (rewards.isEmpty()) _uiState.value.rewards else rewards,
isLoading = false isLoading = false
) )
} }
@@ -35,10 +120,49 @@ class RewardsViewModel @Inject constructor(
rewardRepository.earnReward(userId, rewardId) rewardRepository.earnReward(userId, rewardId)
} }
} }
fun showAddDialog() {
_uiState.value = _uiState.value.copy(showAddDialog = true, newRewardTitle = "")
}
fun hideAddDialog() {
_uiState.value = _uiState.value.copy(showAddDialog = false, newRewardTitle = "")
}
fun updateNewRewardTitle(title: String) {
_uiState.value = _uiState.value.copy(newRewardTitle = title)
}
fun addReward(userId: String) {
val title = _uiState.value.newRewardTitle.trim()
if (title.isBlank()) return
val newReward = Reward(
id = "reward_${System.currentTimeMillis()}",
title = title,
description = null,
imageUrl = null,
points = 10, // Базовое количество очков
earnedAt = null,
userId = userId
)
// Добавляем новую награду в список
val currentRewards = _uiState.value.rewards.toMutableList()
currentRewards.add(newReward)
_uiState.value = _uiState.value.copy(
rewards = currentRewards,
showAddDialog = false,
newRewardTitle = ""
)
}
} }
data class RewardsUiState( data class RewardsUiState(
val rewards: List<com.novayaplaneta.domain.model.Reward> = emptyList(), val rewards: List<Reward> = emptyList(),
val isLoading: Boolean = false val isLoading: Boolean = false,
val showAddDialog: Boolean = false,
val newRewardTitle: String = ""
) )

View File

@@ -1,70 +1,506 @@
package com.novayaplaneta.ui.screens.schedule 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.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items 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.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.hilt.navigation.compose.hiltViewModel 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 @Composable
fun ScheduleScreen( fun ScheduleScreen(
navController: androidx.navigation.NavController? = null,
viewModel: ScheduleViewModel = hiltViewModel(), viewModel: ScheduleViewModel = hiltViewModel(),
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val uiState by viewModel.uiState.collectAsState() val uiState by viewModel.uiState.collectAsState()
val configuration = LocalConfiguration.current
val screenWidthDp = configuration.screenWidthDp
val screenHeightDp = configuration.screenHeightDp
Scaffold( // Цвета из autism-friendly палитры
topBar = { val backgroundColor = LoginBackgroundTurquoise
TopAppBar( val navPanelColor = LoginInputLightBlue
title = { Text("Расписание") } val accentGreen = LoginGreenAccent
) val dateCardColor = LoginInputLightBlue
// Форматирование даты и времени по московскому времени
val moscowTimeZone = TimeZone.getTimeZone("Europe/Moscow")
val dateFormat = SimpleDateFormat("dd.MM EEEE", Locale("ru")).apply {
timeZone = moscowTimeZone
} }
) { paddingValues -> val timeFormat = SimpleDateFormat("HH:mm", Locale("ru")).apply {
Column( 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 modifier = modifier
.fillMaxSize() .fillMaxSize()
.padding(paddingValues) .background(color = backgroundColor)
.padding(16.dp)
) { ) {
if (uiState.isLoading) { Row(
CircularProgressIndicator( modifier = Modifier.fillMaxSize()
) {
// Левая панель навигации с логотипом над ней (с отступами и закругленными краями)
Column(
modifier = Modifier
.width((screenWidthDp * 0.22f).dp.coerceIn(160.dp, 240.dp))
.fillMaxHeight()
.padding(vertical = 20.dp, horizontal = 16.dp)
) {
// Логотип над панелью навигации
Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.wrapContentWidth() .padding(bottom = 16.dp),
contentAlignment = Alignment.Center
) {
val logoSize = (screenHeightDp * 0.22f).toInt().coerceIn(140, 240).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 = { }
) )
} else {
LazyColumn( // Награды
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) verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
items(uiState.schedules) { schedule -> Icon(
Card( imageVector = icon,
modifier = Modifier.fillMaxWidth() 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( Column(
modifier = Modifier.padding(16.dp) 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(
text = schedule.title, text = date,
style = MaterialTheme.typography.titleLarge fontSize = dateTextSize,
fontWeight = FontWeight.Bold,
color = Color.Black
) )
schedule.description?.let {
Text( Text(
text = it, text = dayOfWeek,
style = MaterialTheme.typography.bodyMedium 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
)
}
}
} }
} }
} }
} }
@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 kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
sealed class TaskType(val title: String) {
object Gift : TaskType("Подарок")
object EatWithSpoon : TaskType("Кушать ложкой")
}
@HiltViewModel @HiltViewModel
class ScheduleViewModel @Inject constructor( class ScheduleViewModel @Inject constructor(
private val getSchedulesUseCase: GetSchedulesUseCase 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( data class ScheduleUiState(
val schedules: List<com.novayaplaneta.domain.model.Schedule> = emptyList(), 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 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.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.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel 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 @Composable
fun SettingsScreen( fun SettingsScreen(
navController: NavController? = null,
viewModel: SettingsViewModel = hiltViewModel(), viewModel: SettingsViewModel = hiltViewModel(),
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val uiState by viewModel.uiState.collectAsState() val uiState by viewModel.uiState.collectAsState()
val configuration = LocalConfiguration.current
val screenWidthDp = configuration.screenWidthDp
val screenHeightDp = configuration.screenHeightDp
Scaffold( // Навигация на экран входа при выходе
topBar = { LaunchedEffect(uiState.isLoggedOut) {
TopAppBar( if (uiState.isLoggedOut) {
title = { Text("Настройки") } navController?.navigate("login") {
) popUpTo(0) { inclusive = true }
} }
) { paddingValues -> }
Column( }
// Цвета из 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 modifier = modifier
.fillMaxSize() .fillMaxSize()
.padding(paddingValues) .background(color = backgroundColor)
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
uiState.currentUser?.let { user -> Row(
Card( modifier = Modifier.fillMaxSize()
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
) {
val logoSize = (screenHeightDp * 0.22f).toInt().coerceIn(140, 240).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") }
)
}
}
// Основная область
Column(
modifier = Modifier
.weight(1f)
.fillMaxHeight()
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
// Верхняя панель: только дата/время справа
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( 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
)
}
}
}
Button(
onClick = { viewModel.logout() },
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.buttonColors( horizontalAlignment = Alignment.Start,
containerColor = MaterialTheme.colorScheme.error verticalArrangement = Arrangement.spacedBy(16.dp)
)
) { ) {
Text("Выйти") 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.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.novayaplaneta.domain.model.User
import com.novayaplaneta.domain.model.UserRole
import com.novayaplaneta.domain.repository.AuthRepository import com.novayaplaneta.domain.repository.AuthRepository
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@@ -24,12 +27,34 @@ class SettingsViewModel @Inject constructor(
private fun loadCurrentUser() { private fun loadCurrentUser() {
viewModelScope.launch { 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() { fun logout() {
viewModelScope.launch { viewModelScope.launch {
authRepository.logout() authRepository.logout()
@@ -39,7 +64,7 @@ class SettingsViewModel @Inject constructor(
} }
data class SettingsUiState( data class SettingsUiState(
val currentUser: com.novayaplaneta.domain.model.User? = null, val currentUser: User? = null,
val isLoggedOut: Boolean = false val isLoggedOut: Boolean = false
) )

View File

@@ -14,22 +14,22 @@ val SurfaceDark = Color(0xFF1A1A1A)
val OnBackgroundDark = Color(0xFFFFFFFF) val OnBackgroundDark = Color(0xFFFFFFFF)
val OnSurfaceDark = Color(0xFFFFFFFF) val OnSurfaceDark = Color(0xFFFFFFFF)
// Accent Colors // Accent Colors - очень мягкие пастельные тона для максимального комфорта детей с РАС
val AccentGreen = Color(0xFF4CAF50) val AccentGreen = Color(0xFFA8D5BA) // Очень мягкий мятно-зеленый (низкая насыщенность)
val AccentOrange = Color(0xFFFF6B35) val AccentOrange = Color(0xFFFFD4B3) // Очень мягкий персиковый (теплый, успокаивающий)
val AccentGold = Color(0xFFFFD700) val AccentGold = Color(0xFFFFEEC7) // Очень мягкий бежево-желтый (кремовый оттенок)
// Status Colors // Status Colors - очень приглушенные для комфорта
val SuccessColor = Color(0xFF4CAF50) val SuccessColor = Color(0xFFA8D5BA) // Очень мягкий зеленый
val WarningColor = Color(0xFFFF6B35) val WarningColor = Color(0xFFFFD4B3) // Очень мягкий персиковый
val ErrorColor = Color(0xFFE53935) val ErrorColor = Color(0xFFFFC8C8) // Очень мягкий розовый (вместо яркого красного)
// Цвета для экрана авторизации (благоприятные для РАС из PDF) // Цвета для экрана авторизации (оптимизированные для РАС - очень мягкие тона)
val LoginBackgroundTurquoise = Color(0xFFDAE7E9) // Мягкий голубой фон val LoginBackgroundTurquoise = Color(0xFFE8F4F5) // Очень мягкий мятный фон (еще мягче)
val LoginCardLightBlue = Color(0xFFBCDAEC) // Спокойный светло-голубой val LoginCardLightBlue = Color(0xFFD4E8F0) // Очень мягкий голубой для карточек
val LoginInputLightBlue = Color(0xFFBCDAEC) // Для полей ввода val LoginInputLightBlue = Color(0xFFD4E8F0) // Очень мягкий голубой для полей ввода
val LoginButtonBlue = Color(0xFFBCDAEC) // Для кнопки val LoginButtonBlue = Color(0xFFD4E8F0) // Очень мягкий голубой для кнопок
val LoginGreenAccent = Color(0xFF80EF80) // Пастельно-зелёный акцент val LoginGreenAccent = Color(0xFF95D5A3) // Очень мягкий зеленый акцент (приглушенный)
val LoginGreenSoft = Color(0xFFC5E6C5) // Мягкий пастельно-зелёный val LoginGreenSoft = Color(0xFFD1E8D7) // Очень мягкий светло-зеленый
val LoginGreenDark = Color(0xFF80EF80) // Пастельно-зелёный темнее val LoginGreenDark = Color(0xFF95D5A3) // Очень мягкий зеленый для темных элементов