main #8

Open
student-m-kuchergin wants to merge 17 commits from KHBGPU-org/NTO-2026-Android-TeamTask-Template:main into main
22 changed files with 236 additions and 77 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
/.idea /.idea
/.gradle /.gradle
/build /build
/local.properties

BIN
Client_v1.apk Normal file

Binary file not shown.

BIN
Design_v0.penpot Normal file

Binary file not shown.

BIN
Design_v1.penpot Normal file

Binary file not shown.

2
README.md Normal file
View File

@ -0,0 +1,2 @@
# Penpot Доска
https://pp.sicampus.ru/#/workspace?team-id=14a6b474-d5fa-807f-8007-9f077cdd8786&file-id=14a6b474-d5fa-807f-8007-9f541769571d&page-id=14a6b474-d5fa-807f-8007-9f541769571e

View File

@ -34,6 +34,7 @@ android {
} }
dependencies { dependencies {
implementation("androidx.compose.foundation:foundation:1.10.3")
defaultComposeLibrary() defaultComposeLibrary()
implementation("androidx.datastore:datastore-preferences:1.1.7") implementation("androidx.datastore:datastore-preferences:1.1.7")
implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.4.0") implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.4.0")
@ -47,4 +48,8 @@ dependencies {
implementation("io.ktor:ktor-client-content-negotiation:$ktor") implementation("io.ktor:ktor-client-content-negotiation:$ktor")
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor") implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
implementation("androidx.compose.ui:ui-tooling-preview:1.10.3")
implementation("androidx.compose.runtime:runtime-tracing:1.10.3")
implementation("androidx.compose.runtime:runtime-tracing:1.10.3")
debugImplementation ("androidx.compose.ui:ui-tooling:1.10.3")
} }

View File

@ -1,8 +1,10 @@
package ru.myitschool.work.core package ru.myitschool.work.core
object Constants { object Constants {
const val HOST = "http://localhost:8090" const val HOST = "http://10.0.0.14:49183"
const val AUTH_URL = "/auth" const val AUTH_URL = "/auth"
//TODO заменить на /auth
const val INFO_URL = "/info" const val INFO_URL = "/info"
const val BOOKING_URL = "/booking" const val BOOKING_URL = "/booking"
const val BOOK_URL = "/book" const val BOOK_URL = "/book"

View File

@ -4,7 +4,8 @@ object TestIds {
object Auth { object Auth {
const val ERROR = "auth_error" const val ERROR = "auth_error"
const val SIGN_BUTTON = "auth_sign_button" const val SIGN_BUTTON = "auth_sign_button"
const val CODE_INPUT = "auth_code_input" const val LOGIN_INPUT = "auth_login_input"
const val PASSWORD_INPUT = "auth_password_input"
} }
object Main { object Main {
const val ERROR = "main_error" const val ERROR = "main_error"

View File

@ -1,14 +1,19 @@
package ru.myitschool.work.data.source package ru.myitschool.work.data.source
import android.util.Log
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.call.body import io.ktor.client.call.body
import io.ktor.client.engine.cio.CIO import io.ktor.client.engine.cio.CIO
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.request.basicAuth
import io.ktor.client.request.get import io.ktor.client.request.get
import io.ktor.client.request.header
import io.ktor.client.request.post import io.ktor.client.request.post
import io.ktor.client.request.setBody import io.ktor.client.request.setBody
import io.ktor.client.statement.bodyAsText import io.ktor.client.statement.bodyAsText
import io.ktor.http.ContentType import io.ktor.http.ContentType
import io.ktor.http.HttpHeaders
import io.ktor.http.HttpHeaders.Authorization
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.http.contentType import io.ktor.http.contentType
import io.ktor.serialization.kotlinx.json.json import io.ktor.serialization.kotlinx.json.json
@ -19,6 +24,7 @@ import ru.myitschool.work.core.Constants
import ru.myitschool.work.data.dto.PlaceDto import ru.myitschool.work.data.dto.PlaceDto
import ru.myitschool.work.data.dto.BookRequestDto import ru.myitschool.work.data.dto.BookRequestDto
import ru.myitschool.work.data.dto.UserDto import ru.myitschool.work.data.dto.UserDto
import kotlin.io.encoding.Base64
object NetworkDataSource { object NetworkDataSource {
private val client by lazy { private val client by lazy {
@ -36,9 +42,25 @@ object NetworkDataSource {
} }
} }
suspend fun checkAuth(code: String): Result<Boolean> = withContext(Dispatchers.IO) { suspend fun checkAuth(text: String): Result<Boolean> = withContext(Dispatchers.IO) {
return@withContext runCatching { return@withContext runCatching {
val response = client.get(getUrl(code, Constants.AUTH_URL)) /*
val response = client.post(
getUrl(Constants.AUTH_URL))
*/
val response = client.post(
urlString = "http://10.0.0.177:49183/api/auth" //getUrl(Constants.AUTH_URL)
) {
header(key = Authorization, value = basicAuth(text.split(Regex(":"))[0], text.split(Regex(":"))[1]))
}
// "Basic ${Base64.encode(text.toByteArray(), )}" "Basic ${Base64.encode(text.toByteArray())}"
//val response = client.get(getUrl(Constants.AUTH_URL)) {}
Log.d("RESPONSE", response.toString())
Log.d("RESPONSE_STATUS", response.status.toString())
when (response.status) { when (response.status) {
HttpStatusCode.OK -> true HttpStatusCode.OK -> true
else -> false else -> false
@ -49,7 +71,7 @@ object NetworkDataSource {
suspend fun getInfo(code: String): Result<UserDto> = withContext(Dispatchers.IO) { suspend fun getInfo(code: String): Result<UserDto> = withContext(Dispatchers.IO) {
return@withContext runCatching { return@withContext runCatching {
println("!!!!!!!!!!!!!! getInfo $code") println("!!!!!!!!!!!!!! getInfo $code")
val response = client.get(getUrl(code, Constants.INFO_URL)) val response = client.get(getUrl(Constants.INFO_URL))
if (response.status == HttpStatusCode.OK) { if (response.status == HttpStatusCode.OK) {
println("!!!!!!!!!!!!!! getInfo OK ${response.bodyAsText()}") println("!!!!!!!!!!!!!! getInfo OK ${response.bodyAsText()}")
response.body<UserDto>() response.body<UserDto>()
@ -62,7 +84,7 @@ object NetworkDataSource {
suspend fun getBooking(code: String): Result<Map<String, List<PlaceDto>>?> = withContext(Dispatchers.IO) { suspend fun getBooking(code: String): Result<Map<String, List<PlaceDto>>?> = withContext(Dispatchers.IO) {
return@withContext runCatching { return@withContext runCatching {
val response = client.get(getUrl(code, Constants.BOOKING_URL)) val response = client.get(getUrl(Constants.BOOKING_URL))
if (response.status == HttpStatusCode.OK) { if (response.status == HttpStatusCode.OK) {
response.body<Map<String, List<PlaceDto>>>() response.body<Map<String, List<PlaceDto>>>()
} else { } else {
@ -73,7 +95,7 @@ object NetworkDataSource {
suspend fun addBook(code: String, data: BookRequestDto): Result<Boolean> = withContext(Dispatchers.IO) { suspend fun addBook(code: String, data: BookRequestDto): Result<Boolean> = withContext(Dispatchers.IO) {
return@withContext runCatching { return@withContext runCatching {
val response = client.post(getUrl(code, Constants.BOOK_URL)) { val response = client.post(getUrl(Constants.BOOK_URL)) {
contentType(ContentType.Application.Json) contentType(ContentType.Application.Json)
setBody(data) setBody(data)
} }
@ -85,5 +107,5 @@ object NetworkDataSource {
} }
} }
private fun getUrl(code: String, targetUrl: String) = "${Constants.HOST}/api/$code$targetUrl" private fun getUrl(targetUrl: String) = "${Constants.HOST}/api$targetUrl"
} }

View File

@ -1,15 +0,0 @@
package ru.myitschool.work.domain.auth
import ru.myitschool.work.data.repo.AuthRepository
class CheckAndSaveAuthCodeUseCase(
private val repository: AuthRepository
) {
suspend operator fun invoke(
text: String
): Result<Unit> {
return repository.checkAndSave(text).mapCatching { success ->
if (!success) error("Code is incorrect")
}
}
}

View File

@ -0,0 +1,19 @@
package ru.myitschool.work.domain.auth
import ru.myitschool.work.data.repo.AuthRepository
class CheckAndSaveAuthDataUseCase(
private val repository: AuthRepository
) {
suspend operator fun invoke(
textLogin: String,
textPassword: String
): Result<Unit> {
return repository.checkAndSave("$textLogin:$textPassword").mapCatching { success ->
if (!success) {
error("Login or password is incorrect")
}
}
}
}

View File

@ -1,12 +0,0 @@
package ru.myitschool.work.domain.auth
class CheckCodeFormatUseCase {
operator fun invoke(
text: String
): Boolean {
return text.length == 4 && text.all { char ->
char.isLetterOrDigit() &&
((char >= 'A' && char <= 'Z') || (char >= 'a' && char <= 'z') || char.isDigit())
}
}
}

View File

@ -0,0 +1,14 @@
package ru.myitschool.work.domain.auth
class CheckLoginFormatUseCase {
operator fun invoke(
text: String
): Boolean {
return text.all { char ->
(char.isLetterOrDigit() &&
((char in ('A'..'Z')) || (char in ('a'..'z')) || char.isDigit()))
|| char == '.'
}
}
}

View File

@ -0,0 +1,20 @@
package ru.myitschool.work.domain.auth
import java.util.Locale
import java.util.Locale.getDefault
class CheckPasswordFormatUseCase {
operator fun invoke(
textLogin: String,
textPassword: String
):
Boolean {
val lowerCasePassword = textPassword.lowercase(getDefault())
val lowerCaseLogin = textLogin.lowercase(getDefault())
val intersect = lowerCasePassword.toList().intersect(lowerCaseLogin.toList());
return (textPassword.length >= 8) && (textPassword.all { char ->
textPassword.count { it == char } < 3
&& intersect.size < 3
}) && !("[A-Za-z0-9]+".toRegex().matches(textPassword))
}
}

View File

@ -1,6 +1,7 @@
package ru.myitschool.work.ui.root package ru.myitschool.work.ui.root
import android.os.Bundle import android.os.Bundle
import android.view.WindowManager
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
@ -10,11 +11,17 @@ import androidx.compose.material3.Scaffold
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import ru.myitschool.work.ui.screen.AppNavHost import ru.myitschool.work.ui.screen.AppNavHost
import ru.myitschool.work.ui.theme.WorkTheme import ru.myitschool.work.ui.theme.WorkTheme
import android.view.View
class RootActivity : ComponentActivity() { class RootActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
enableEdgeToEdge() enableEdgeToEdge()
window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE);
// Hide the status bar.
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN
actionBar?.hide()
setContent { setContent {
WorkTheme { WorkTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->

View File

@ -1,6 +1,6 @@
package ru.myitschool.work.ui.screen.auth package ru.myitschool.work.ui.screen.auth
sealed interface AuthIntent { sealed interface AuthIntent {
data class Send(val text: String): AuthIntent data class Send(val textLogin: String, val textPassword: String): AuthIntent
data class TextInput(val text: String): AuthIntent data class TextInput(val textLogin: String, val textPassword: String): AuthIntent
} }

View File

@ -3,21 +3,23 @@ package ru.myitschool.work.ui.screen.auth
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect 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.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -28,8 +30,18 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.compose.rememberNavController
import ru.myitschool.work.R import ru.myitschool.work.R
import ru.myitschool.work.core.TestIds import ru.myitschool.work.core.TestIds
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.material3.OutlinedTextField
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.unit.sp
@Composable @Composable
fun AuthScreen( fun AuthScreen(
@ -38,6 +50,7 @@ fun AuthScreen(
) { ) {
val state by viewModel.uiState.collectAsState() val state by viewModel.uiState.collectAsState()
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.actionFlow.collect { action -> viewModel.actionFlow.collect { action ->
when (action) { when (action) {
@ -51,15 +64,11 @@ fun AuthScreen(
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(all = 24.dp), .padding(all = 44.dp),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center verticalArrangement = Arrangement.Center
) { ) {
Text(
text = stringResource(R.string.auth_title),
style = MaterialTheme.typography.headlineSmall,
textAlign = TextAlign.Center
)
when (val currentState = state) { when (val currentState = state) {
is AuthState.Data -> Content(viewModel, currentState) is AuthState.Data -> Content(viewModel, currentState)
is AuthState.Loading -> { is AuthState.Loading -> {
@ -72,37 +81,108 @@ fun AuthScreen(
} }
@Composable @Composable
private fun Content( private fun Content
(
viewModel: AuthViewModel, viewModel: AuthViewModel,
state: AuthState.Data state: AuthState.Data
) { ) {
var inputText by remember { mutableStateOf("") } var inputLogin by rememberSaveable { mutableStateOf("") }
Spacer(modifier = Modifier.size(16.dp)) val googleSans = FontFamily(
TextField( Font(R.font.googlesans_regular, FontWeight.Medium, FontStyle.Normal)
modifier = Modifier.testTag(TestIds.Auth.CODE_INPUT).fillMaxWidth(),
value = inputText,
onValueChange = {
inputText = it
viewModel.onIntent(AuthIntent.TextInput(it))
},
label = { Text(stringResource(R.string.auth_label)) }
) )
Spacer(modifier = Modifier.size(16.dp)) Spacer(modifier = Modifier.size(5.dp))
Text(
text = stringResource(R.string.auth_title),
fontFamily = googleSans,
color = Color(0xFF284777),
fontSize = 35.sp,
textAlign = TextAlign.Center,
fontWeight = FontWeight.Medium,
fontStyle = FontStyle.Normal,
modifier = Modifier.fillMaxWidth(1f)
)
Spacer(modifier = Modifier.height(5.dp))
Text(
text = stringResource(R.string.auth_label_login),
fontFamily = googleSans,
color = Color(0xFF74777f),
textAlign = TextAlign.Start,
fontWeight = FontWeight.Medium,
fontStyle = FontStyle.Normal,
modifier = Modifier.fillMaxWidth(1f)
)
Spacer(modifier = Modifier.size(10.dp))
OutlinedTextField(
modifier = Modifier.testTag(TestIds.Auth.LOGIN_INPUT).fillMaxWidth(),
value = inputLogin,
shape = RoundedCornerShape(12.dp),
colors = TextFieldDefaults.colors(unfocusedContainerColor = Color(0xFFededf4)),
onValueChange = {
inputLogin = it
viewModel.onIntent(AuthIntent.TextInput(
it,
textPassword = ""
))
}
)
var inputPassword by rememberSaveable { mutableStateOf("") }
Spacer(modifier = Modifier.size(20.dp))
Text(
text = stringResource(R.string.auth_label_password),
fontFamily = googleSans,
color = Color(0xFF74777f),
textAlign = TextAlign.Start,
fontWeight = FontWeight.Medium,
fontStyle = FontStyle.Normal,
modifier = Modifier.fillMaxWidth(1f)
)
Spacer(modifier = Modifier.size(10.dp))
OutlinedTextField(
modifier = Modifier.testTag(TestIds.Auth.PASSWORD_INPUT).fillMaxWidth(),
value = inputPassword,
shape = RoundedCornerShape(12.dp),
colors = TextFieldDefaults.colors(unfocusedContainerColor = Color(0xFFededf4)),
onValueChange = {
inputPassword = it
viewModel.onIntent(AuthIntent.TextInput(
inputLogin,
textPassword = it
))
}
)
Spacer(modifier = Modifier.size(35.dp))
Button( Button(
modifier = Modifier.testTag(TestIds.Auth.SIGN_BUTTON).fillMaxWidth(), modifier = Modifier.testTag(TestIds.Auth.SIGN_BUTTON).fillMaxWidth(),
onClick = { onClick = {
viewModel.onIntent(AuthIntent.Send(inputText)) viewModel.onIntent(AuthIntent.Send(inputLogin, inputPassword))
}, },
enabled = state.isEnabledSend enabled = state.isEnabledSend
) { ) {
Text(stringResource(R.string.auth_sign_in)) Text(
text = "Войти",
modifier = Modifier.padding(10.dp),
fontFamily = googleSans,
fontWeight = FontWeight.Medium,
fontStyle = FontStyle.Normal,
fontSize = 18.sp
)
} }
if (state.error != null) { if (state.error != null) {
Text( Text(
modifier = Modifier.testTag(TestIds.Auth.ERROR), modifier = Modifier.testTag(TestIds.Auth.ERROR),
text = state.error, text = if (viewModel.incorrectAttemptNum >= 5) {
"Превышен лимит попыток входа"
} else {
state.error
},
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = Color.Red, color = Color.Red,
) )
} }
} }
@Preview
@Composable
fun AuthView() {
AuthScreen(navController = rememberNavController())
}

View File

@ -10,19 +10,25 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import ru.myitschool.work.data.repo.AuthRepository import ru.myitschool.work.data.repo.AuthRepository
import ru.myitschool.work.domain.auth.CheckAndSaveAuthCodeUseCase import ru.myitschool.work.domain.auth.CheckAndSaveAuthDataUseCase
import ru.myitschool.work.domain.auth.CheckCodeFormatUseCase import ru.myitschool.work.domain.auth.CheckLoginFormatUseCase
import ru.myitschool.work.domain.auth.CheckPasswordFormatUseCase
import ru.myitschool.work.ui.nav.MainScreenDestination import ru.myitschool.work.ui.nav.MainScreenDestination
class AuthViewModel : ViewModel() { class AuthViewModel : ViewModel() {
private val checkCodeFormatUseCase by lazy { CheckCodeFormatUseCase() } private val checkLoginFormatUseCase by lazy { CheckLoginFormatUseCase() }
private val checkAndSaveAuthCodeUseCase by lazy { CheckAndSaveAuthCodeUseCase(AuthRepository) } private val checkAndSaveAuthDataUseCase by lazy { CheckAndSaveAuthDataUseCase(AuthRepository) }
private val checkPasswordFormatUseCase by lazy { CheckPasswordFormatUseCase() }
private val _uiState = MutableStateFlow<AuthState>( private val _uiState = MutableStateFlow<AuthState>(
AuthState.Data( AuthState.Data(
isEnabledSend = false, isEnabledSend = false,
error = null error = null
) )
) )
var incorrectAttemptNum = 0
val uiState: StateFlow<AuthState> = _uiState.asStateFlow() val uiState: StateFlow<AuthState> = _uiState.asStateFlow()
private val _actionFlow: MutableSharedFlow<AuthAction> = MutableSharedFlow() private val _actionFlow: MutableSharedFlow<AuthAction> = MutableSharedFlow()
@ -32,7 +38,7 @@ class AuthViewModel : ViewModel() {
when (intent) { when (intent) {
is AuthIntent.Send -> { is AuthIntent.Send -> {
viewModelScope.launch { viewModelScope.launch {
checkAndSaveAuthCodeUseCase.invoke(intent.text).fold( checkAndSaveAuthDataUseCase.invoke(intent.textLogin, intent.textPassword).fold(
onSuccess = { onSuccess = {
_actionFlow.emit(AuthAction.Open(MainScreenDestination)) _actionFlow.emit(AuthAction.Open(MainScreenDestination))
}, },
@ -42,14 +48,20 @@ class AuthViewModel : ViewModel() {
error = error.message error = error.message
) )
} }
if (error.message == "Login or password is incorrect") {
incorrectAttemptNum += 1;
}
} }
) )
} }
} }
is AuthIntent.TextInput -> { is AuthIntent.TextInput -> {
updateStateIfData { oldState -> updateStateIfData { oldState ->
oldState.copy( oldState.copy(
isEnabledSend = checkCodeFormatUseCase.invoke(intent.text), isEnabledSend = (checkPasswordFormatUseCase.invoke(
intent.textLogin, intent.textPassword
) && checkLoginFormatUseCase.invoke(intent.textLogin) && incorrectAttemptNum <= 4),
error = null error = null
) )
} }

Binary file not shown.

View File

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<color name="purple_200">#FFBB86FC</color> <color name="purple_200">#415f91</color>
<color name="purple_500">#FF6200EE</color> <color name="purple_500">#565F71</color>
<color name="purple_700">#FF3700B3</color> <color name="purple_700">#33618D</color>
<color name="teal_200">#FF03DAC5</color> <color name="teal_200">#D6E3FF</color>
<color name="teal_700">#FF018786</color> <color name="teal_700">#DAE2F9</color>
<color name="black">#FF000000</color> <color name="black">#44474E</color>
<color name="white">#FFFFFFFF</color> <color name="white">#F3F3FA</color>
</resources> </resources>

View File

@ -1,8 +1,9 @@
<resources> <resources>
<string name="app_name">Work</string> <string name="app_name">Work</string>
<string name="title_activity_root">RootActivity</string> <string name="title_activity_root">Приложение</string>
<string name="auth_title">Привет! Введи код для авторизации</string> <string name="auth_title">Авторизация</string>
<string name="auth_label">Код</string> <string name="auth_label_login">Логин</string>
<string name="auth_label_password">Пароль</string>
<string name="auth_sign_in">Войти</string> <string name="auth_sign_in">Войти</string>
<string name="main_refresh">Обновить</string> <string name="main_refresh">Обновить</string>

BIN
khBGPU.apk Normal file

Binary file not shown.