Попробовал сделать экран входа с помощью курсора

This commit is contained in:
2025-12-19 00:57:58 +03:00
parent 58fe248a2c
commit 6ddd6f082b
14 changed files with 450 additions and 25 deletions

2
.idea/compiler.xml generated
View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="21" />
<bytecodeTargetLevel target="17" />
</component>
</project>

10
.idea/deploymentTargetDropDown.xml generated Normal file
View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetDropDown">
<value>
<entry key="app">
<State />
</entry>
</value>
</component>
</project>

6
.idea/kotlinc.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="version" value="2.1.0" />
</component>
</project>

3
.idea/misc.xml generated
View File

@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">

View File

@@ -97,8 +97,8 @@ dependencies {
implementation(libs.lottie.compose)
// Coroutines
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.coroutines.android)
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0")
// Testing
testImplementation(libs.junit)

View File

@@ -27,6 +27,7 @@ class MainActivity : ComponentActivity() {
Scaffold(
modifier = Modifier.fillMaxSize(),
bottomBar = {
if (currentRoute != "login") {
BottomNavigationBar(
currentRoute = currentRoute,
onNavigate = { route ->
@@ -40,6 +41,7 @@ class MainActivity : ComponentActivity() {
}
)
}
}
) { innerPadding ->
NewPlanetNavigation(
navController = navController,

View File

@@ -0,0 +1,38 @@
package com.novayaplaneta.ui.components
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.novayaplaneta.R
@Composable
fun NovayaPlanetaLogo(
modifier: Modifier = Modifier,
size: androidx.compose.ui.unit.Dp? = null
) {
// Адаптивный размер - используем размер экрана или переданный размер
val configuration = LocalConfiguration.current
val logoSize = size ?: kotlin.run {
// Базовый размер для планшета, адаптируется под экран
val minScreenSize = kotlin.math.min(configuration.screenWidthDp, configuration.screenHeightDp)
(minScreenSize / 3).dp
}
Box(
modifier = modifier.size(logoSize),
contentAlignment = Alignment.Center
) {
Image(
painter = painterResource(id = R.drawable.logo_earth),
contentDescription = "Логотип Новая Планета",
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Fit
)
}
}

View File

@@ -6,6 +6,7 @@ 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.LoginScreen
import com.novayaplaneta.ui.screens.rewards.RewardsScreen
import com.novayaplaneta.ui.screens.schedule.ScheduleScreen
import com.novayaplaneta.ui.screens.settings.SettingsScreen
@@ -16,13 +17,16 @@ import com.novayaplaneta.ui.screens.timer.TimerScreen
fun NewPlanetNavigation(
navController: NavHostController,
modifier: Modifier = Modifier,
startDestination: String = "schedule"
startDestination: String = "login"
) {
NavHost(
navController = navController,
startDestination = startDestination,
modifier = modifier
) {
composable("login") {
LoginScreen(navController = navController)
}
composable("schedule") {
ScheduleScreen()
}

View File

@@ -0,0 +1,288 @@
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 LoginScreen(
navController: NavController,
viewModel: LoginViewModel = hiltViewModel(),
modifier: Modifier = Modifier
) {
val uiState by viewModel.uiState.collectAsState()
val snackbarHostState = remember { SnackbarHostState() }
LaunchedEffect(uiState.errorMessage) {
val errorMsg = uiState.errorMessage
if (errorMsg != null) {
snackbarHostState.showSnackbar(
message = errorMsg,
actionLabel = null
)
}
}
Scaffold(
snackbarHost = {
SnackbarHost(
hostState = snackbarHostState,
snackbar = { snackbarData ->
Snackbar(
snackbarData = snackbarData,
containerColor = MaterialTheme.colorScheme.error,
contentColor = Color.White
)
}
)
},
containerColor = LoginBackgroundTurquoise
) { paddingValues ->
Box(
modifier = modifier
.fillMaxSize()
.padding(paddingValues)
.background(color = LoginBackgroundTurquoise)
) {
val configuration = LocalConfiguration.current
val screenWidthDp: Int = configuration.screenWidthDp
val screenHeightDp: Int = configuration.screenHeightDp
val isLandscape = screenWidthDp > screenHeightDp
// Адаптивные отступы
val horizontalPadding = (screenWidthDp * 0.04f).toInt().coerceIn(24, 48).dp
val verticalPadding = (screenHeightDp * 0.03f).toInt().coerceIn(16, 32).dp
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = horizontalPadding, vertical = verticalPadding),
horizontalAlignment = Alignment.CenterHorizontally
) {
// Логотип вверху слева - уменьшенный размер
val logoSizeByHeight = (screenHeightDp * 0.15f).toInt()
val logoSizeByWidth = screenWidthDp / 5
val logoSize = min(logoSizeByHeight, logoSizeByWidth).coerceIn(100, 160).dp
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Start
) {
NovayaPlanetaLogo(
modifier = Modifier.padding(bottom = 4.dp),
size = logoSize
)
}
Spacer(modifier = Modifier.weight(0.05f))
// Центрированный контент - адаптивная ширина (50-70% экрана)
val contentWidthRatio = if (isLandscape) 0.5f else 0.7f
Column(
modifier = Modifier
.fillMaxWidth(contentWidthRatio),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(24.dp)
) {
// Заголовок "Вход" - увеличен
val titleFontSize = (screenHeightDp * 0.06f).toInt().coerceIn(44, 76).sp
Text(
text = "Вход",
fontSize = titleFontSize,
fontWeight = FontWeight.Bold,
color = LoginGreenAccent,
textAlign = TextAlign.Center
)
// Поля ввода - увеличены
val inputHeightValue = (screenHeightDp * 0.075f).toInt().coerceIn(64, 100)
val inputHeight: androidx.compose.ui.unit.Dp = inputHeightValue.dp
val inputTextSizeValue = (screenHeightDp * 0.026f).toInt().coerceIn(20, 28)
val inputTextSize: androidx.compose.ui.unit.TextUnit = inputTextSizeValue.sp
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
// Поле логина
Box(
modifier = Modifier
.fillMaxWidth()
.height(height = inputHeight)
.background(
color = LoginInputLightBlue,
shape = RoundedCornerShape(20.dp)
),
contentAlignment = Alignment.CenterStart
) {
TextField(
value = uiState.login,
onValueChange = { viewModel.onLoginChange(it) },
placeholder = {
Text(
text = "Введи логин",
fontSize = inputTextSize,
color = Color.Gray.copy(alpha = 0.7f)
)
},
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
colors = TextFieldDefaults.colors(
unfocusedContainerColor = Color.Transparent,
focusedContainerColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
focusedIndicatorColor = Color.Transparent,
unfocusedTextColor = Color.Black,
focusedTextColor = Color.Black
),
textStyle = MaterialTheme.typography.bodyLarge.copy(
fontSize = inputTextSize
),
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
)
}
// Поле пароля
Box(
modifier = Modifier
.fillMaxWidth()
.height(height = inputHeight)
.background(
color = LoginInputLightBlue,
shape = RoundedCornerShape(20.dp)
),
contentAlignment = Alignment.CenterStart
) {
TextField(
value = uiState.password,
onValueChange = { viewModel.onPasswordChange(it) },
placeholder = {
Text(
text = "Введи пароль",
fontSize = inputTextSize,
color = Color.Gray.copy(alpha = 0.7f)
)
},
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
visualTransformation = PasswordVisualTransformation(),
colors = TextFieldDefaults.colors(
unfocusedContainerColor = Color.Transparent,
focusedContainerColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
focusedIndicatorColor = Color.Transparent,
unfocusedTextColor = Color.Black,
focusedTextColor = Color.Black
),
textStyle = MaterialTheme.typography.bodyLarge.copy(
fontSize = inputTextSize
),
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password)
)
}
}
// Кнопка "Войти" - адаптивный размер
Button(
onClick = {
viewModel.login {
navController.navigate("schedule") {
popUpTo(0) { inclusive = true }
}
}
},
modifier = Modifier
.fillMaxWidth()
.height(height = inputHeight),
shape = RoundedCornerShape(20.dp),
enabled = uiState.isFormValid && !uiState.isLoading,
colors = if (uiState.isFormValid) {
ButtonDefaults.buttonColors(
containerColor = LoginButtonBlue,
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
)
}
}
// Ссылки - зелёные, около кнопки
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
val linkTextSize = (screenHeightDp * 0.024f).toInt().coerceIn(18, 26).sp
Text(
text = "Нет логина и пароля?",
fontSize = linkTextSize,
color = LoginGreenAccent,
fontWeight = FontWeight.Medium,
modifier = Modifier.clickable {
// TODO: Переход на регистрацию
}
)
Text(
text = "Не помнишь пароль?",
fontSize = linkTextSize,
color = LoginGreenAccent,
fontWeight = FontWeight.Medium,
modifier = Modifier.clickable {
// TODO: Переход на восстановление пароля
}
)
}
}
Spacer(modifier = Modifier.weight(0.1f))
}
}
}
}

View File

@@ -0,0 +1,67 @@
package com.novayaplaneta.ui.screens.auth
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.novayaplaneta.domain.repository.AuthRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class LoginViewModel @Inject constructor(
private val authRepository: AuthRepository
) : ViewModel() {
private val _uiState = MutableStateFlow(LoginUiState())
val uiState: StateFlow<LoginUiState> = _uiState.asStateFlow()
fun onLoginChange(login: String) {
_uiState.value = _uiState.value.copy(
login = login,
isFormValid = isFormValid(login, _uiState.value.password)
)
}
fun onPasswordChange(password: String) {
_uiState.value = _uiState.value.copy(
password = password,
isFormValid = isFormValid(_uiState.value.login, password)
)
}
fun login(onSuccess: () -> Unit) {
if (!_uiState.value.isFormValid) return
viewModelScope.launch {
_uiState.value = _uiState.value.copy(isLoading = true, errorMessage = null)
val result = authRepository.login(_uiState.value.login, _uiState.value.password)
result.onSuccess { user ->
_uiState.value = _uiState.value.copy(isLoading = false, isLoggedIn = true)
onSuccess()
}.onFailure { exception ->
_uiState.value = _uiState.value.copy(
isLoading = false,
errorMessage = exception.message ?: "Ошибка входа"
)
}
}
}
private fun isFormValid(login: String, password: String): Boolean {
return login.isNotBlank() && password.isNotBlank()
}
}
data class LoginUiState(
val login: String = "",
val password: String = "",
val isLoading: Boolean = false,
val isFormValid: Boolean = false,
val isLoggedIn: Boolean = false,
val errorMessage: String? = null
)

View File

@@ -24,3 +24,12 @@ val SuccessColor = Color(0xFF4CAF50)
val WarningColor = Color(0xFFFF6B35)
val ErrorColor = Color(0xFFE53935)
// Цвета для экрана авторизации (благоприятные для РАС из 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) // Пастельно-зелёный темнее

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

@@ -1,14 +1,14 @@
[versions]
agp = "8.12.3"
kotlin = "2.0.21"
coreKtx = "1.17.0"
agp = "8.3.1"
kotlin = "2.1.0"
coreKtx = "1.13.1"
junit = "4.13.2"
junitVersion = "1.3.0"
espressoCore = "3.7.0"
lifecycleRuntimeKtx = "2.9.4"
lifecycleViewmodel = "2.9.4"
activityCompose = "1.11.0"
composeBom = "2024.09.00"
lifecycleRuntimeKtx = "2.8.7"
lifecycleViewmodel = "2.8.7"
activityCompose = "1.9.2"
composeBom = "2024.06.00"
hilt = "2.53"
hiltNavigationCompose = "1.2.0"
room = "2.6.1"
@@ -19,8 +19,8 @@ kotlinxSerializationConverter = "1.0.0"
navigation = "2.8.4"
coil = "2.7.0"
lottie = "6.1.0"
coroutines = "1.10.0"
ksp = "2.0.21-1.0.26"
coroutines = "1.9.0"
ksp = "2.1.0-1.0.28"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }

View File

@@ -1,6 +1,6 @@
#Fri Dec 05 21:32:23 MSK 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists