Compare commits
6 Commits
62c4fdb997
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| b41de4aaf5 | |||
| 5105e68970 | |||
| 59087fe6d9 | |||
| c56399dde9 | |||
| b809b1e6e6 | |||
| 103c69d7d1 |
2
.idea/gradle.xml
generated
2
.idea/gradle.xml
generated
@@ -4,7 +4,6 @@
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="testRunner" value="CHOOSE_PER_TEST" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
|
||||
<option name="modules">
|
||||
@@ -13,6 +12,7 @@
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
</set>
|
||||
</option>
|
||||
<option name="resolveExternalAnnotations" value="false" />
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
|
||||
@@ -22,10 +22,6 @@ class AIRepositoryImpl @Inject constructor(
|
||||
|
||||
override suspend fun sendMessage(userId: String, message: String): Result<String> {
|
||||
return try {
|
||||
// Get token from current user
|
||||
val user = authRepository.getCurrentUser().first()
|
||||
val token = user?.token ?: ""
|
||||
|
||||
// Save user message
|
||||
val userMessage = ChatMessage(
|
||||
id = UUID.randomUUID().toString(),
|
||||
@@ -36,29 +32,22 @@ class AIRepositoryImpl @Inject constructor(
|
||||
)
|
||||
chatMessageDao.insertMessage(userMessage.toEntity())
|
||||
|
||||
// Send to API (only if token exists)
|
||||
if (token.isNotEmpty()) {
|
||||
val response = api.chatWithAI("Bearer $token", ChatRequest(message))
|
||||
if (response.isSuccessful && response.body() != null) {
|
||||
val aiResponse = response.body()!!.message
|
||||
// Тестовый ответ для демонстрации
|
||||
kotlinx.coroutines.delay(1500) // Небольшая задержка для анимации
|
||||
|
||||
// Save AI response
|
||||
val aiMessage = ChatMessage(
|
||||
id = UUID.randomUUID().toString(),
|
||||
message = aiResponse,
|
||||
isFromAI = true,
|
||||
timestamp = LocalDateTime.now(),
|
||||
userId = userId
|
||||
)
|
||||
chatMessageDao.insertMessage(aiMessage.toEntity())
|
||||
val aiResponse = "!!!!"
|
||||
|
||||
Result.success(aiResponse)
|
||||
} else {
|
||||
Result.failure(Exception("Failed to get AI response"))
|
||||
}
|
||||
} else {
|
||||
Result.failure(Exception("User not authenticated"))
|
||||
}
|
||||
// Save AI response
|
||||
val aiMessage = ChatMessage(
|
||||
id = UUID.randomUUID().toString(),
|
||||
message = aiResponse,
|
||||
isFromAI = true,
|
||||
timestamp = LocalDateTime.now(),
|
||||
userId = userId
|
||||
)
|
||||
chatMessageDao.insertMessage(aiMessage.toEntity())
|
||||
|
||||
Result.success(aiResponse)
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,9 @@ import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import com.novayaplaneta.ui.screens.ai.AIScreen
|
||||
import com.novayaplaneta.ui.screens.auth.ForgotPasswordScreen
|
||||
import com.novayaplaneta.ui.screens.auth.LoginScreen
|
||||
import com.novayaplaneta.ui.screens.auth.RegistrationScreen
|
||||
import com.novayaplaneta.ui.screens.rewards.RewardsScreen
|
||||
import com.novayaplaneta.ui.screens.schedule.ScheduleScreen
|
||||
import com.novayaplaneta.ui.screens.settings.SettingsScreen
|
||||
@@ -27,12 +29,12 @@ fun NewPlanetNavigation(
|
||||
composable("login") {
|
||||
LoginScreen(navController = navController)
|
||||
}
|
||||
// composable("registration") {
|
||||
// RegistrationScreen(navController = navController)
|
||||
// }
|
||||
// composable("forgot_password") {
|
||||
// ForgotPasswordScreen(navController = navController)
|
||||
// }
|
||||
composable("registration") {
|
||||
RegistrationScreen(navController = navController)
|
||||
}
|
||||
composable("forgot_password") {
|
||||
ForgotPasswordScreen(navController = navController)
|
||||
}
|
||||
composable("schedule") {
|
||||
ScheduleScreen(navController = navController)
|
||||
}
|
||||
@@ -46,7 +48,7 @@ fun NewPlanetNavigation(
|
||||
RewardsScreen(navController = navController)
|
||||
}
|
||||
composable("ai") {
|
||||
AIScreen()
|
||||
AIScreen(navController = navController)
|
||||
}
|
||||
composable("settings") {
|
||||
SettingsScreen(navController = navController)
|
||||
|
||||
@@ -1,94 +1,387 @@
|
||||
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.lazy.LazyColumn
|
||||
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.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.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.sp
|
||||
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
|
||||
fun AIScreen(
|
||||
navController: androidx.navigation.NavController? = null,
|
||||
viewModel: AIViewModel = hiltViewModel(),
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
var messageText by remember { mutableStateOf("") }
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("ИИ-помощник \"Земля\"") }
|
||||
)
|
||||
// Загружаем историю чата при первом открытии экрана
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.loadChatHistory("default")
|
||||
}
|
||||
|
||||
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
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.background(color = backgroundColor)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
LazyColumn(
|
||||
// Левая панель навигации с логотипом над ней
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
.width((screenWidthDp * 0.22f).dp.coerceIn(160.dp, 240.dp))
|
||||
.fillMaxHeight()
|
||||
.padding(vertical = 20.dp, horizontal = 16.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
|
||||
)
|
||||
}
|
||||
// Логотип над панелью навигации
|
||||
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 = false,
|
||||
onClick = { navController?.navigate("settings") }
|
||||
)
|
||||
|
||||
NavItem(
|
||||
icon = Icons.Filled.Public,
|
||||
text = "Земля",
|
||||
isSelected = true,
|
||||
onClick = { }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (uiState.isLoading) {
|
||||
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
|
||||
}
|
||||
|
||||
// Основная область с AI агентом и чатом
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
.weight(1f)
|
||||
.fillMaxHeight()
|
||||
.padding(24.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(24.dp)
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = messageText,
|
||||
onValueChange = { messageText = it },
|
||||
modifier = Modifier.weight(1f),
|
||||
placeholder = { Text("Введите сообщение...") }
|
||||
)
|
||||
Button(
|
||||
onClick = {
|
||||
if (messageText.isNotBlank()) {
|
||||
viewModel.sendMessage("userId", messageText)
|
||||
messageText = ""
|
||||
// Левая часть: Анимированная картинка 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(
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,3 +10,6 @@ object EmailValidator {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -76,39 +76,33 @@ fun ForgotPasswordScreen(
|
||||
val horizontalPadding = (screenWidthDp * 0.04f).toInt().coerceIn(24, 48).dp
|
||||
val verticalPadding = (screenHeightDp * 0.03f).toInt().coerceIn(16, 32).dp
|
||||
|
||||
Column(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = horizontalPadding, vertical = verticalPadding),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
.padding(horizontal = horizontalPadding, vertical = verticalPadding)
|
||||
) {
|
||||
// Логотип вверху слева
|
||||
val logoSizeByHeight = (screenHeightDp * 0.15f).toInt()
|
||||
val logoSizeByWidth = screenWidthDp / 5
|
||||
val logoSize = min(logoSizeByHeight, logoSizeByWidth).coerceIn(100, 160).dp
|
||||
// Логотип вверху слева - увеличенный размер
|
||||
val logoSizeByHeight = (screenHeightDp * 0.28f).toInt()
|
||||
val logoSizeByWidth = screenWidthDp / 3
|
||||
val logoSize = min(logoSizeByHeight, logoSizeByWidth).coerceIn(180, 280).dp
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Start
|
||||
) {
|
||||
NovayaPlanetaLogo(
|
||||
modifier = Modifier
|
||||
.padding(bottom = 4.dp)
|
||||
.clickable {
|
||||
navController.navigate("login") {
|
||||
popUpTo("login") { inclusive = false }
|
||||
}
|
||||
},
|
||||
size = logoSize
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(0.05f))
|
||||
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)
|
||||
@@ -370,8 +364,6 @@ fun ForgotPasswordScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(0.1f))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,33 +76,28 @@ fun LoginScreen(
|
||||
val horizontalPadding = (screenWidthDp * 0.04f).toInt().coerceIn(24, 48).dp
|
||||
val verticalPadding = (screenHeightDp * 0.03f).toInt().coerceIn(16, 32).dp
|
||||
|
||||
Column(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = horizontalPadding, vertical = verticalPadding),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
.padding(horizontal = horizontalPadding, vertical = verticalPadding)
|
||||
) {
|
||||
// Логотип вверху слева - уменьшенный размер
|
||||
val logoSizeByHeight = (screenHeightDp * 0.15f).toInt()
|
||||
val logoSizeByWidth = screenWidthDp / 5
|
||||
val logoSize = min(logoSizeByHeight, logoSizeByWidth).coerceIn(100, 160).dp
|
||||
// Логотип вверху слева - большой размер
|
||||
val logoSizeByHeight = (screenHeightDp * 0.28f).toInt()
|
||||
val logoSizeByWidth = screenWidthDp / 3
|
||||
val logoSize = min(logoSizeByHeight, logoSizeByWidth).coerceIn(180, 280).dp
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Start
|
||||
) {
|
||||
NovayaPlanetaLogo(
|
||||
modifier = Modifier.padding(bottom = 4.dp),
|
||||
size = logoSize
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(0.05f))
|
||||
NovayaPlanetaLogo(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopStart)
|
||||
.padding(bottom = 4.dp),
|
||||
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)
|
||||
@@ -228,7 +223,7 @@ fun LoginScreen(
|
||||
enabled = uiState.isFormValid && !uiState.isLoading,
|
||||
colors = if (uiState.isFormValid) {
|
||||
ButtonDefaults.buttonColors(
|
||||
containerColor = LoginButtonBlue,
|
||||
containerColor = LoginGreenAccent,
|
||||
contentColor = Color.White
|
||||
)
|
||||
} else {
|
||||
@@ -261,12 +256,12 @@ fun LoginScreen(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
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 = "Нет логина и пароля?",
|
||||
fontSize = linkTextSize,
|
||||
color = LoginGreenAccent,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.clickable {
|
||||
navController.navigate("registration")
|
||||
}
|
||||
@@ -276,15 +271,13 @@ fun LoginScreen(
|
||||
text = "Не помнишь пароль?",
|
||||
fontSize = linkTextSize,
|
||||
color = LoginGreenAccent,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.clickable {
|
||||
navController.navigate("forgot_password")
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(0.1f))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,39 +76,33 @@ fun RegistrationScreen(
|
||||
val horizontalPadding = (screenWidthDp * 0.04f).toInt().coerceIn(24, 48).dp
|
||||
val verticalPadding = (screenHeightDp * 0.03f).toInt().coerceIn(16, 32).dp
|
||||
|
||||
Column(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = horizontalPadding, vertical = verticalPadding),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
.padding(horizontal = horizontalPadding, vertical = verticalPadding)
|
||||
) {
|
||||
// Логотип вверху слева
|
||||
val logoSizeByHeight = (screenHeightDp * 0.15f).toInt()
|
||||
val logoSizeByWidth = screenWidthDp / 5
|
||||
val logoSize = min(logoSizeByHeight, logoSizeByWidth).coerceIn(100, 160).dp
|
||||
// Логотип вверху слева - увеличенный размер
|
||||
val logoSizeByHeight = (screenHeightDp * 0.28f).toInt()
|
||||
val logoSizeByWidth = screenWidthDp / 3
|
||||
val logoSize = min(logoSizeByHeight, logoSizeByWidth).coerceIn(180, 280).dp
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Start
|
||||
) {
|
||||
NovayaPlanetaLogo(
|
||||
modifier = Modifier
|
||||
.padding(bottom = 4.dp)
|
||||
.clickable {
|
||||
navController.navigate("login") {
|
||||
popUpTo("login") { inclusive = false }
|
||||
}
|
||||
},
|
||||
size = logoSize
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(0.05f))
|
||||
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)
|
||||
@@ -410,8 +404,6 @@ fun RegistrationScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(0.1f))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ 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
|
||||
@@ -21,10 +22,13 @@ 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.sp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.novayaplaneta.ui.components.NovayaPlanetaLogo
|
||||
import com.novayaplaneta.ui.theme.*
|
||||
@@ -76,14 +80,14 @@ fun RewardsScreen(
|
||||
.fillMaxHeight()
|
||||
.padding(vertical = 20.dp, horizontal = 16.dp)
|
||||
) {
|
||||
// Логотип над панелью навигации (увеличен)
|
||||
// Логотип над панелью навигации
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 16.dp),
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -198,10 +202,31 @@ fun RewardsScreen(
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -120,10 +120,49 @@ class RewardsViewModel @Inject constructor(
|
||||
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(
|
||||
val rewards: List<Reward> = emptyList(),
|
||||
val isLoading: Boolean = false
|
||||
val isLoading: Boolean = false,
|
||||
val showAddDialog: Boolean = false,
|
||||
val newRewardTitle: String = ""
|
||||
)
|
||||
|
||||
|
||||
@@ -80,14 +80,14 @@ fun ScheduleScreen(
|
||||
.fillMaxHeight()
|
||||
.padding(vertical = 20.dp, horizontal = 16.dp)
|
||||
) {
|
||||
// Логотип над панелью навигации (увеличен)
|
||||
// Логотип над панелью навигации
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 16.dp),
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@@ -81,14 +81,14 @@ fun SettingsScreen(
|
||||
.fillMaxHeight()
|
||||
.padding(vertical = 20.dp, horizontal = 16.dp)
|
||||
) {
|
||||
// Логотип над панелью навигации (увеличен)
|
||||
// Логотип над панелью навигации
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 16.dp),
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@@ -14,22 +14,22 @@ val SurfaceDark = Color(0xFF1A1A1A)
|
||||
val OnBackgroundDark = Color(0xFFFFFFFF)
|
||||
val OnSurfaceDark = Color(0xFFFFFFFF)
|
||||
|
||||
// Accent Colors
|
||||
val AccentGreen = Color(0xFF4CAF50)
|
||||
val AccentOrange = Color(0xFFFF6B35)
|
||||
val AccentGold = Color(0xFFFFD700)
|
||||
// Accent Colors - очень мягкие пастельные тона для максимального комфорта детей с РАС
|
||||
val AccentGreen = Color(0xFFA8D5BA) // Очень мягкий мятно-зеленый (низкая насыщенность)
|
||||
val AccentOrange = Color(0xFFFFD4B3) // Очень мягкий персиковый (теплый, успокаивающий)
|
||||
val AccentGold = Color(0xFFFFEEC7) // Очень мягкий бежево-желтый (кремовый оттенок)
|
||||
|
||||
// Status Colors
|
||||
val SuccessColor = Color(0xFF4CAF50)
|
||||
val WarningColor = Color(0xFFFF6B35)
|
||||
val ErrorColor = Color(0xFFE53935)
|
||||
// Status Colors - очень приглушенные для комфорта
|
||||
val SuccessColor = Color(0xFFA8D5BA) // Очень мягкий зеленый
|
||||
val WarningColor = Color(0xFFFFD4B3) // Очень мягкий персиковый
|
||||
val ErrorColor = Color(0xFFFFC8C8) // Очень мягкий розовый (вместо яркого красного)
|
||||
|
||||
// Цвета для экрана авторизации (благоприятные для РАС из PDF)
|
||||
val LoginBackgroundTurquoise = Color(0xFFDAE7E9) // Мягкий голубой фон
|
||||
val LoginCardLightBlue = Color(0xFFBCDAEC) // Спокойный светло-голубой
|
||||
val LoginInputLightBlue = Color(0xFFBCDAEC) // Для полей ввода
|
||||
val LoginButtonBlue = Color(0xFFBCDAEC) // Для кнопки
|
||||
val LoginGreenAccent = Color(0xFF80EF80) // Пастельно-зелёный акцент
|
||||
val LoginGreenSoft = Color(0xFFC5E6C5) // Мягкий пастельно-зелёный
|
||||
val LoginGreenDark = Color(0xFF80EF80) // Пастельно-зелёный темнее
|
||||
// Цвета для экрана авторизации (оптимизированные для РАС - очень мягкие тона)
|
||||
val LoginBackgroundTurquoise = Color(0xFFE8F4F5) // Очень мягкий мятный фон (еще мягче)
|
||||
val LoginCardLightBlue = Color(0xFFD4E8F0) // Очень мягкий голубой для карточек
|
||||
val LoginInputLightBlue = Color(0xFFD4E8F0) // Очень мягкий голубой для полей ввода
|
||||
val LoginButtonBlue = Color(0xFFD4E8F0) // Очень мягкий голубой для кнопок
|
||||
val LoginGreenAccent = Color(0xFF95D5A3) // Очень мягкий зеленый акцент (приглушенный)
|
||||
val LoginGreenSoft = Color(0xFFD1E8D7) // Очень мягкий светло-зеленый
|
||||
val LoginGreenDark = Color(0xFF95D5A3) // Очень мягкий зеленый для темных элементов
|
||||
|
||||
|
||||
Reference in New Issue
Block a user