diff --git a/.idea/compiler.xml b/.idea/compiler.xml index b86273d..b589d56 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml new file mode 100644 index 0000000..0c0c338 --- /dev/null +++ b/.idea/deploymentTargetDropDown.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml new file mode 100644 index 0000000..bb44937 --- /dev/null +++ b/.idea/kotlinc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index b2c751a..0ad17cb 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,6 +1,7 @@ + - + diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4b04a59..0547ef6 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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) diff --git a/app/src/main/java/com/novayaplaneta/MainActivity.kt b/app/src/main/java/com/novayaplaneta/MainActivity.kt index d381f52..9581495 100644 --- a/app/src/main/java/com/novayaplaneta/MainActivity.kt +++ b/app/src/main/java/com/novayaplaneta/MainActivity.kt @@ -27,18 +27,20 @@ class MainActivity : ComponentActivity() { Scaffold( modifier = Modifier.fillMaxSize(), bottomBar = { - BottomNavigationBar( - currentRoute = currentRoute, - onNavigate = { route -> - navController.navigate(route) { - popUpTo(navController.graph.startDestinationId) { - saveState = true + if (currentRoute != "login") { + BottomNavigationBar( + currentRoute = currentRoute, + onNavigate = { route -> + navController.navigate(route) { + popUpTo(navController.graph.startDestinationId) { + saveState = true + } + launchSingleTop = true + restoreState = true } - launchSingleTop = true - restoreState = true } - } - ) + ) + } } ) { innerPadding -> NewPlanetNavigation( diff --git a/app/src/main/java/com/novayaplaneta/ui/components/Logo.kt b/app/src/main/java/com/novayaplaneta/ui/components/Logo.kt new file mode 100644 index 0000000..606b73b --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/ui/components/Logo.kt @@ -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 + ) + } +} diff --git a/app/src/main/java/com/novayaplaneta/ui/navigation/NewPlanetNavigation.kt b/app/src/main/java/com/novayaplaneta/ui/navigation/NewPlanetNavigation.kt index 5ab0fcd..b381408 100644 --- a/app/src/main/java/com/novayaplaneta/ui/navigation/NewPlanetNavigation.kt +++ b/app/src/main/java/com/novayaplaneta/ui/navigation/NewPlanetNavigation.kt @@ -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() } diff --git a/app/src/main/java/com/novayaplaneta/ui/screens/auth/LoginScreen.kt b/app/src/main/java/com/novayaplaneta/ui/screens/auth/LoginScreen.kt new file mode 100644 index 0000000..46e09e5 --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/ui/screens/auth/LoginScreen.kt @@ -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)) + } + } + } +} diff --git a/app/src/main/java/com/novayaplaneta/ui/screens/auth/LoginViewModel.kt b/app/src/main/java/com/novayaplaneta/ui/screens/auth/LoginViewModel.kt new file mode 100644 index 0000000..03ac640 --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/ui/screens/auth/LoginViewModel.kt @@ -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 = _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 +) diff --git a/app/src/main/java/com/novayaplaneta/ui/theme/Color.kt b/app/src/main/java/com/novayaplaneta/ui/theme/Color.kt index 8077fd8..867dd1a 100644 --- a/app/src/main/java/com/novayaplaneta/ui/theme/Color.kt +++ b/app/src/main/java/com/novayaplaneta/ui/theme/Color.kt @@ -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) // Пастельно-зелёный темнее + diff --git a/app/src/main/res/drawable/logo_earth.png b/app/src/main/res/drawable/logo_earth.png new file mode 100644 index 0000000..c1d2e98 Binary files /dev/null and b/app/src/main/res/drawable/logo_earth.png differ diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2a9ff7d..232f61d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 86468ce..d29f63c 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -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