Compare commits

...

6 Commits

13 changed files with 659 additions and 186 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

@@ -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,29 +32,22 @@ 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( id = UUID.randomUUID().toString(),
id = UUID.randomUUID().toString(), message = aiResponse,
message = aiResponse, isFromAI = true,
isFromAI = true, timestamp = LocalDateTime.now(),
timestamp = LocalDateTime.now(), userId = userId
userId = userId )
) 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,12 +29,12 @@ 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(navController = navController) ScheduleScreen(navController = navController)
} }
@@ -46,7 +48,7 @@ fun NewPlanetNavigation(
RewardsScreen(navController = navController) RewardsScreen(navController = navController)
} }
composable("ai") { composable("ai") {
AIScreen() AIScreen(navController = navController)
} }
composable("settings") { composable("settings") {
SettingsScreen(navController = navController) 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("ИИ-помощник \"Земля\"") } }
)
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)
} }
) { paddingValues -> }
Column(
modifier = modifier Box(
.fillMaxSize() modifier = modifier
.padding(paddingValues) .fillMaxSize()
.background(color = backgroundColor)
) {
Row(
modifier = Modifier.fillMaxSize()
) { ) {
LazyColumn( // Левая панель навигации с логотипом над ней
Column(
modifier = Modifier modifier = Modifier
.weight(1f) .width((screenWidthDp * 0.22f).dp.coerceIn(160.dp, 240.dp))
.padding(16.dp), .fillMaxHeight()
verticalArrangement = Arrangement.spacedBy(8.dp) .padding(vertical = 20.dp, horizontal = 16.dp)
) { ) {
items(uiState.messages) { message -> // Логотип над панелью навигации
Card( Box(
modifier = Modifier.fillMaxWidth(), modifier = Modifier
colors = CardDefaults.cardColors( .fillMaxWidth()
containerColor = if (message.isFromAI) { .padding(bottom = 16.dp),
MaterialTheme.colorScheme.primaryContainer contentAlignment = Alignment.Center
} else { ) {
MaterialTheme.colorScheme.surfaceVariant val logoSize = (screenHeightDp * 0.22f).toInt().coerceIn(140, 240).dp
} NovayaPlanetaLogo(size = logoSize)
) }
) {
Text( // Панель навигации
text = message.message, Column(
modifier = Modifier.padding(16.dp), modifier = Modifier
style = MaterialTheme.typography.bodyLarge .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 = { }
)
} }
} }
if (uiState.isLoading) { // Основная область с AI агентом и чатом
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
}
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .weight(1f)
.padding(16.dp), .fillMaxHeight()
horizontalArrangement = Arrangement.spacedBy(8.dp) .padding(24.dp),
horizontalArrangement = Arrangement.spacedBy(24.dp)
) { ) {
OutlinedTextField( // Левая часть: Анимированная картинка AI агента
value = messageText, Column(
onValueChange = { messageText = it }, modifier = Modifier
modifier = Modifier.weight(1f), .width((screenWidthDp * 0.3f).dp.coerceIn(200.dp, 400.dp))
placeholder = { Text("Введите сообщение...") } .fillMaxHeight(),
) horizontalAlignment = Alignment.CenterHorizontally,
Button( verticalArrangement = Arrangement.Center
onClick = { ) {
if (messageText.isNotBlank()) { AnimatedAIAgent(
viewModel.sendMessage("userId", messageText) isSpeaking = uiState.isLoading,
messageText = "" 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(
value = messageText,
onValueChange = { messageText = it },
modifier = Modifier.weight(1f),
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(
onClick = {
if (messageText.isNotBlank()) {
viewModel.sendMessage("default", 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
) {
Icon(
imageVector = Icons.Filled.Send,
contentDescription = "Отправить",
modifier = Modifier.size(24.dp)
)
} }
} }
) {
Text("Отправить")
} }
} }
} }
} }
} }
@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

@@ -10,3 +10,6 @@ object EmailValidator {
} }

View File

@@ -76,39 +76,33 @@ fun ForgotPasswordScreen(
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( NovayaPlanetaLogo(
modifier = Modifier.fillMaxWidth(), modifier = Modifier
horizontalArrangement = Arrangement.Start .align(Alignment.TopStart)
) { .padding(bottom = 4.dp)
NovayaPlanetaLogo( .clickable {
modifier = Modifier navController.navigate("login") {
.padding(bottom = 4.dp) popUpTo("login") { inclusive = false }
.clickable { }
navController.navigate("login") { },
popUpTo("login") { inclusive = false } 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)
@@ -370,8 +364,6 @@ fun ForgotPasswordScreen(
} }
} }
} }
Spacer(modifier = Modifier.weight(0.1f))
} }
} }
} }

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( NovayaPlanetaLogo(
modifier = Modifier.fillMaxWidth(), modifier = Modifier
horizontalArrangement = Arrangement.Start .align(Alignment.TopStart)
) { .padding(bottom = 4.dp),
NovayaPlanetaLogo( size = logoSize
modifier = Modifier.padding(bottom = 4.dp), )
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)
@@ -228,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 {
@@ -261,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")
} }
@@ -276,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

@@ -76,39 +76,33 @@ fun RegistrationScreen(
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( NovayaPlanetaLogo(
modifier = Modifier.fillMaxWidth(), modifier = Modifier
horizontalArrangement = Arrangement.Start .align(Alignment.TopStart)
) { .padding(bottom = 4.dp)
NovayaPlanetaLogo( .clickable {
modifier = Modifier navController.navigate("login") {
.padding(bottom = 4.dp) popUpTo("login") { inclusive = false }
.clickable { }
navController.navigate("login") { },
popUpTo("login") { inclusive = false } 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)
@@ -410,8 +404,6 @@ fun RegistrationScreen(
} }
} }
} }
Spacer(modifier = Modifier.weight(0.1f))
} }
} }
} }

View File

@@ -8,6 +8,7 @@ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons 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.CalendarToday
import androidx.compose.material.icons.filled.Person import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Public import androidx.compose.material.icons.filled.Public
@@ -21,10 +22,13 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.ui.text.font.FontWeight 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.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.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.components.NovayaPlanetaLogo
import com.novayaplaneta.ui.theme.* import com.novayaplaneta.ui.theme.*
@@ -76,14 +80,14 @@ fun RewardsScreen(
.fillMaxHeight() .fillMaxHeight()
.padding(vertical = 20.dp, horizontal = 16.dp) .padding(vertical = 20.dp, horizontal = 16.dp)
) { ) {
// Логотип над панелью навигации (увеличен) // Логотип над панелью навигации
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(bottom = 16.dp), .padding(bottom = 16.dp),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
val logoSize = (screenHeightDp * 0.18f).toInt().coerceIn(120, 200).dp val logoSize = (screenHeightDp * 0.22f).toInt().coerceIn(140, 240).dp
NovayaPlanetaLogo(size = logoSize) NovayaPlanetaLogo(size = logoSize)
} }
@@ -198,10 +202,31 @@ fun RewardsScreen(
screenHeightDp = screenHeightDp 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
)
}
} }
} }
@@ -297,3 +322,148 @@ fun RewardCard(
} }
} }
@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(
imageVector = Icons.Filled.Add,
contentDescription = "Добавить награду",
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

@@ -120,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<Reward> = emptyList(), val rewards: List<Reward> = emptyList(),
val isLoading: Boolean = false val isLoading: Boolean = false,
val showAddDialog: Boolean = false,
val newRewardTitle: String = ""
) )

View File

@@ -80,14 +80,14 @@ fun ScheduleScreen(
.fillMaxHeight() .fillMaxHeight()
.padding(vertical = 20.dp, horizontal = 16.dp) .padding(vertical = 20.dp, horizontal = 16.dp)
) { ) {
// Логотип над панелью навигации (увеличен) // Логотип над панелью навигации
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(bottom = 16.dp), .padding(bottom = 16.dp),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
val logoSize = (screenHeightDp * 0.18f).toInt().coerceIn(120, 200).dp val logoSize = (screenHeightDp * 0.22f).toInt().coerceIn(140, 240).dp
NovayaPlanetaLogo(size = logoSize) NovayaPlanetaLogo(size = logoSize)
} }

View File

@@ -81,14 +81,14 @@ fun SettingsScreen(
.fillMaxHeight() .fillMaxHeight()
.padding(vertical = 20.dp, horizontal = 16.dp) .padding(vertical = 20.dp, horizontal = 16.dp)
) { ) {
// Логотип над панелью навигации (увеличен) // Логотип над панелью навигации
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(bottom = 16.dp), .padding(bottom = 16.dp),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
val logoSize = (screenHeightDp * 0.18f).toInt().coerceIn(120, 200).dp val logoSize = (screenHeightDp * 0.22f).toInt().coerceIn(140, 240).dp
NovayaPlanetaLogo(size = logoSize) NovayaPlanetaLogo(size = logoSize)
} }

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) // Очень мягкий зеленый для темных элементов