diff --git a/app/build.gradle.kts b/app/build.gradle.kts index fe08381..470e179 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -36,10 +36,5 @@ android { dependencies { defaultComposeLibrary() - val ktor = "3.3.1" - implementation("io.ktor:ktor-client-core:$ktor") - implementation("io.ktor:ktor-client-cio:$ktor") - implementation("io.ktor:ktor-client-content-negotiation:$ktor") - implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0") + implementation("androidx.compose.material:material-icons-extended:1.7.8") } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5531bab..95ba03b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -15,7 +15,7 @@ android:usesCleartextTraffic="true" tools:targetApi="31"> diff --git a/app/src/main/java/ru/myitschool/work/core/Constants.kt b/app/src/main/java/ru/myitschool/work/core/Constants.kt deleted file mode 100644 index 4d49e30..0000000 --- a/app/src/main/java/ru/myitschool/work/core/Constants.kt +++ /dev/null @@ -1,9 +0,0 @@ -package ru.myitschool.work.core - -object Constants { - const val HOST = "http://10.0.2.2:8080" - const val USER_URL = "/user" - const val FULL_USER_URL = "$HOST$USER_URL" - const val BOOK_URL = "/book" - const val FULL_BOOK_URL = "$HOST$BOOK_URL" -} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/core/TestIds.kt b/app/src/main/java/ru/myitschool/work/core/TestIds.kt new file mode 100644 index 0000000..fa3008b --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/core/TestIds.kt @@ -0,0 +1,13 @@ +package ru.myitschool.work.core + +object TestIds { + const val AUTH_TITLE = "auth_title" + const val AUTH_INPUT_EMAIL = "auth_input_email" + const val AUTH_INPUT_PASSWORD = "auth_input_password" + const val AUTH_INPUT_PASSWORD_VISIBILITY = "auth_input_password_visibility" + const val AUTH_LOGIN = "auth_login" + const val AUTH_ERROR = "auth_error" + const val AUTH_FORGOT_PASS = "auth_forgot_pass" + const val AUTH_CREATE_ACCOUNT = "auth_create_account" + const val AUTH_SUCCESS = "auth_success" +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/data/AppRepository.kt b/app/src/main/java/ru/myitschool/work/data/AppRepository.kt deleted file mode 100644 index a3d233a..0000000 --- a/app/src/main/java/ru/myitschool/work/data/AppRepository.kt +++ /dev/null @@ -1,26 +0,0 @@ -package ru.myitschool.work.data - -import ru.myitschool.work.data.source.NetworkDataSource -import ru.myitschool.work.domain.entities.BookingEntity -import ru.myitschool.work.domain.entities.UserEntity - -object AppRepository { - suspend fun loadData(): Result { - return NetworkDataSource.getUser().map { dto -> - UserEntity( - name = dto.name, - bookingList = dto.booking.map { bookingDto -> - BookingEntity( - roomName = bookingDto.room, - time = bookingDto.time - ) - } - ) - } - } - - suspend fun addBook(room: String, time: String): Result { - return NetworkDataSource.addBook(room, time) - } - -} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/data/dto/ErrorDto.kt b/app/src/main/java/ru/myitschool/work/data/dto/ErrorDto.kt deleted file mode 100644 index c04ff69..0000000 --- a/app/src/main/java/ru/myitschool/work/data/dto/ErrorDto.kt +++ /dev/null @@ -1,10 +0,0 @@ -package ru.myitschool.work.data.dto - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class ErrorDto( - @SerialName("error") - val error: String, -) \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/data/dto/UserDto.kt b/app/src/main/java/ru/myitschool/work/data/dto/UserDto.kt deleted file mode 100644 index dd574d3..0000000 --- a/app/src/main/java/ru/myitschool/work/data/dto/UserDto.kt +++ /dev/null @@ -1,20 +0,0 @@ -package ru.myitschool.work.data.dto - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class UserDto( - @SerialName("name") - val name: String, - @SerialName("booking") - val booking: List -) { - @Serializable - data class BookingDto( - @SerialName("room") - val room: String, - @SerialName("time") - val time: String, - ) -} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt b/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt deleted file mode 100644 index 305c70b..0000000 --- a/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt +++ /dev/null @@ -1,71 +0,0 @@ -package ru.myitschool.work.data.source - -import io.ktor.client.HttpClient -import io.ktor.client.call.body -import io.ktor.client.engine.cio.CIO -import io.ktor.client.plugins.contentnegotiation.ContentNegotiation -import io.ktor.client.request.forms.FormPart -import io.ktor.client.request.forms.MultiPartFormDataContent -import io.ktor.client.request.forms.formData -import io.ktor.client.request.get -import io.ktor.client.request.post -import io.ktor.client.request.setBody -import io.ktor.http.HttpStatusCode -import io.ktor.serialization.kotlinx.json.json -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import kotlinx.serialization.json.Json -import ru.myitschool.work.core.Constants -import ru.myitschool.work.data.dto.ErrorDto -import ru.myitschool.work.data.dto.UserDto - -object NetworkDataSource { - private val client by lazy { - HttpClient(CIO) { - install(ContentNegotiation) { - json( - Json { - isLenient = true - ignoreUnknownKeys = true - explicitNulls = true - encodeDefaults = true - } - ) - } - } - } - - suspend fun getUser(): Result = withContext(Dispatchers.IO) { - return@withContext runCatching { - val response = client.get(Constants.FULL_USER_URL) - if (response.status == HttpStatusCode.OK) { - response.body() - } else { - error(response.body().error) - } - } - } - - suspend fun addBook( - room: String, - time: String, - ): Result = withContext(Dispatchers.IO) { - return@withContext runCatching { - val response = client.post(Constants.FULL_BOOK_URL) { - setBody( - MultiPartFormDataContent( - formData( - FormPart("room", room), - FormPart("time", time), - ) - ) - ) - } - if (response.status == HttpStatusCode.OK) { - Unit - } else { - error(response.body().error) - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/domain/AddBookUseCase.kt b/app/src/main/java/ru/myitschool/work/domain/AddBookUseCase.kt deleted file mode 100644 index 12dc66b..0000000 --- a/app/src/main/java/ru/myitschool/work/domain/AddBookUseCase.kt +++ /dev/null @@ -1,17 +0,0 @@ -package ru.myitschool.work.domain - -import ru.myitschool.work.data.AppRepository - -class AddBookUseCase( - private val repository: AppRepository -) { - suspend operator fun invoke( - room: String, - time: String, - ): Result { - return repository.addBook( - room = room, - time = time, - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/domain/GetUserDataUseCase.kt b/app/src/main/java/ru/myitschool/work/domain/GetUserDataUseCase.kt deleted file mode 100644 index 3417a93..0000000 --- a/app/src/main/java/ru/myitschool/work/domain/GetUserDataUseCase.kt +++ /dev/null @@ -1,12 +0,0 @@ -package ru.myitschool.work.domain - -import ru.myitschool.work.data.AppRepository -import ru.myitschool.work.domain.entities.UserEntity - -class GetUserDataUseCase( - private val repository: AppRepository -) { - suspend operator fun invoke(): Result { - return repository.loadData() - } -} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/domain/entities/BookingEntity.kt b/app/src/main/java/ru/myitschool/work/domain/entities/BookingEntity.kt deleted file mode 100644 index 0bf603d..0000000 --- a/app/src/main/java/ru/myitschool/work/domain/entities/BookingEntity.kt +++ /dev/null @@ -1,6 +0,0 @@ -package ru.myitschool.work.domain.entities - -data class BookingEntity( - val roomName: String, - val time: String, -) \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/domain/entities/UserEntity.kt b/app/src/main/java/ru/myitschool/work/domain/entities/UserEntity.kt deleted file mode 100644 index 86a9851..0000000 --- a/app/src/main/java/ru/myitschool/work/domain/entities/UserEntity.kt +++ /dev/null @@ -1,6 +0,0 @@ -package ru.myitschool.work.domain.entities - -data class UserEntity( - val name: String, - val bookingList: List -) \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/RootActivity.kt b/app/src/main/java/ru/myitschool/work/ui/RootActivity.kt new file mode 100644 index 0000000..e48f1a6 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/RootActivity.kt @@ -0,0 +1,58 @@ +package ru.myitschool.work.ui + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Modifier +import androidx.lifecycle.viewmodel.compose.viewModel +import ru.myitschool.work.ui.auth.AuthScreen +import ru.myitschool.work.ui.auth.AuthViewModel +import ru.myitschool.work.ui.theme.WorkTheme + +/** + * Этот файл не меняем! + * + * Здесь уже подключена ViewModel, которая управляет логикой состояний: + * 1) uiState: текущее состояние (Default, Loading, Error, Success) + * 2) email/password: тексты из полей + * 3) isButtonEnabled: доступность кнопки "Войти" + * + * ВАЖНО: Дизайнерам редактировать только функцию AuthScreen(). + * Всё остальное — готовая логика, менять ничего не нужно. + */ +class RootActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + WorkTheme { + Scaffold(modifier = Modifier.Companion.fillMaxSize()) { innerPadding -> + val viewModel: AuthViewModel = viewModel() + val uiState = viewModel.uiState.collectAsState().value + val email = viewModel.email.collectAsState().value + val password = viewModel.password.collectAsState().value + val isButtonEnabled = viewModel.isButtonEnabled.collectAsState().value + AuthScreen( + modifier = Modifier.Companion + .fillMaxSize() + .padding(innerPadding), + uiState = uiState, + email = email, + password = password, + isButtonEnabled = isButtonEnabled, + onEmailChange = viewModel::onEmailChange, + onPasswordChange = viewModel::onPasswordChange, + onLoginClick = viewModel::onLoginClick, + onForgotPasswordClick = viewModel::onForgotPasswordClick, + onCreateAccountClick = viewModel::onCreateAccountClick + ) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/auth/AuthScreen.kt b/app/src/main/java/ru/myitschool/work/ui/auth/AuthScreen.kt new file mode 100644 index 0000000..53d7477 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/auth/AuthScreen.kt @@ -0,0 +1,213 @@ +package ru.myitschool.work.ui.auth + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Visibility +import androidx.compose.material.icons.filled.VisibilityOff +import androidx.compose.material3.Button +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +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.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import ru.myitschool.work.R +import ru.myitschool.work.core.TestIds + +/** + * Дизайнеры реализуют UI в функции AuthContent. + * + * Входные параметры: + * - uiState — текущее состояние экрана (Default / Loading / Error / Success) + * - email / password — тексты, которые сейчас в полях + * - isButtonEnabled — нужно визуально показать активность кнопки + * + * ️ Колбэки: + * - onEmailChange() — вызывается при изменении email + * - onPasswordChange() — вызывается при изменении пароля + * - onLoginClick() — вызывается при нажатии кнопки "Войти" + * - onForgotPasswordClick() — "Забыли пароль?" + * - onCreateAccountClick() — "Создать аккаунт" + * + * ВАЖНО: + * - Никакой логики добавлять не нужно! + * - Только визуальное оформление Compose по заданию олимпиады. + */ +@Composable +fun AuthScreen( + modifier: Modifier = Modifier, + uiState: AuthUiState, + email: String, + password: String, + isButtonEnabled: Boolean, + onEmailChange: (String) -> Unit, + onPasswordChange: (String) -> Unit, + onLoginClick: () -> Unit, + onForgotPasswordClick: () -> Unit, + onCreateAccountClick: () -> Unit, +) { + Box(modifier = modifier) { + when (uiState) { + + is AuthUiState.Default, + is AuthUiState.Error -> { + // Форма видна и в Default, и при ошибке + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp, vertical = 16.dp), + verticalArrangement = Arrangement.Top + ) { + Text( + modifier = Modifier + .padding(bottom = 16.dp) + .testTag(TestIds.AUTH_TITLE), + text = stringResource(R.string.auth_title), + style = MaterialTheme.typography.headlineMedium, + color = MaterialTheme.colorScheme.onBackground, + ) + + // Email + OutlinedTextField( + modifier = Modifier + .fillMaxWidth() + .testTag(TestIds.AUTH_INPUT_EMAIL), + value = email, + onValueChange = onEmailChange, + label = { Text(stringResource(R.string.auth_input_email_title)) }, + placeholder = { Text(stringResource(R.string.auth_input_email_placeholder)) }, + singleLine = true, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Email, + imeAction = ImeAction.Next + ), + isError = uiState is AuthUiState.Error + ) + Spacer(modifier = Modifier.height(4.dp)) + + // Пароль + var passwordVisible by remember { mutableStateOf(false) } + OutlinedTextField( + modifier = Modifier + .fillMaxWidth() + .testTag(TestIds.AUTH_INPUT_PASSWORD), + value = password, + onValueChange = onPasswordChange, + label = { Text(stringResource(R.string.auth_input_password_title)) }, + placeholder = { Text(stringResource(R.string.auth_input_password_placeholder)) }, + singleLine = true, + visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(), + trailingIcon = { + IconButton( + modifier = Modifier + .testTag(TestIds.AUTH_INPUT_PASSWORD_VISIBILITY), + onClick = { passwordVisible = !passwordVisible } + ) { + Icon( + imageVector = if (passwordVisible) Icons.Filled.Visibility else Icons.Filled.VisibilityOff, + contentDescription = stringResource( + if (passwordVisible) { + R.string.auth_input_password_visibility_hide + } else { + R.string.auth_input_password_visibility_show + } + ) + ) + } + }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Password, + imeAction = ImeAction.Done + ), + isError = uiState is AuthUiState.Error + ) + Spacer(modifier = Modifier.height(4.dp)) + if (uiState is AuthUiState.Error) { + Text( + modifier = Modifier + .padding(start = 16.dp, bottom = 8.dp) + .testTag(TestIds.AUTH_ERROR), + text = stringResource(R.string.auth_error), + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodyMedium, + ) + } + Spacer(modifier = Modifier.height(16.dp)) + + // Кнопка Войти + Button( + onClick = onLoginClick, + enabled = isButtonEnabled, + modifier = Modifier + .fillMaxWidth() + .height(48.dp) + .testTag(TestIds.AUTH_LOGIN) + ) { + Text(stringResource(R.string.auth_login)) + } + + Spacer(modifier = Modifier.height(16.dp)) + + // Ссылки + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + TextButton( + modifier = Modifier.testTag(TestIds.AUTH_FORGOT_PASS), + onClick = onForgotPasswordClick + ) { + Text(stringResource(R.string.auth_forgot_pass)) + } + TextButton( + modifier = Modifier.testTag(TestIds.AUTH_CREATE_ACCOUNT), + onClick = onCreateAccountClick + ) { + Text(stringResource(R.string.auth_create_account)) + } + } + } + } + + is AuthUiState.Loading -> { + CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) + } + + is AuthUiState.Success -> { + Text( + text = stringResource(R.string.auth_success), + color = MaterialTheme.colorScheme.onBackground, + style = MaterialTheme.typography.headlineSmall, + textAlign = TextAlign.Center, + modifier = Modifier + .align(Alignment.Center) + .testTag(TestIds.AUTH_SUCCESS), + ) + } + } + } +} diff --git a/app/src/main/java/ru/myitschool/work/ui/auth/AuthUiState.kt b/app/src/main/java/ru/myitschool/work/ui/auth/AuthUiState.kt new file mode 100755 index 0000000..a140630 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/auth/AuthUiState.kt @@ -0,0 +1,26 @@ +package ru.myitschool.work.ui.auth + +/** + * Этот файл не меняем! + * AuthUiState — это "состояния" экрана авторизации. + * + * Что это значит: + * Экран может быть в одном из четырёх состояний, + * ViewModel (логика) будет переключать эти состояния, + * дизайнеры — отображать соответствующий интерфейс. + * + * Состояния: + * - Default -> Обычная форма входа (поля, кнопки, ссылки) + * - Loading -> Показать крутящийся индикатор (ожидание) + * - Error -> Показать сообщение об ошибке + * - Success -> Показать сообщение об успешном входе + * + * Задача дизайнеров — в зависимости от состояния uiState + * отрисовать нужный экран в Compose. + */ +sealed interface AuthUiState { + data object Default : AuthUiState + data object Loading : AuthUiState + data object Error : AuthUiState + data object Success : AuthUiState +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/auth/AuthViewModel.kt b/app/src/main/java/ru/myitschool/work/ui/auth/AuthViewModel.kt new file mode 100755 index 0000000..2c88ab0 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/auth/AuthViewModel.kt @@ -0,0 +1,98 @@ +package ru.myitschool.work.ui.auth + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +/** + * Этот файл не меняем! + * + * AuthViewModel — это логика, которая управляет экраном авторизации. + * + * Дизайнерам важно понимать: + * - ViewModel не рисует интерфейс, она только "сообщает", что нужно показать. + * - Все нужные данные (email, пароль, состояние экрана, активность кнопки) + * передаются в ваш Composable через параметры. + * - Вам не нужно ничего здесь менять. + * + * Основная идея: + * - Пользователь вводит email и пароль. + * - ViewModel проверяет, можно ли активировать кнопку "Войти". + * - При нажатии "Войти" ViewModel меняет состояние на Loading, потом Success или Error. + */ +class AuthViewModel : ViewModel() { + + // uiState — текущее состояние экрана (Default / Loading / Error / Success) + private val _uiState = MutableStateFlow(AuthUiState.Default) + val uiState: StateFlow = _uiState.asStateFlow() + + // email и password — значения, введённые пользователем в поля ввода + private val _email = MutableStateFlow("") + val email: StateFlow = _email.asStateFlow() + + private val _password = MutableStateFlow("") + val password: StateFlow = _password.asStateFlow() + + // isButtonEnabled — активна ли кнопка "Войти" + // (true, если оба поля не пустые) + private val _isButtonEnabled = MutableStateFlow(false) + val isButtonEnabled: StateFlow = _isButtonEnabled.asStateFlow() + + /** + * Функции onEmailChange / onPasswordChange + * вызываются каждый раз, когда пользователь что-то вводит в поля. + * Они обновляют значения email / passwor + * и проверяют, можно ли активировать кнопку. + */ + fun onEmailChange(newEmail: String) { + _email.value = newEmail + validate() + } + + fun onPasswordChange(newPassword: String) { + _password.value = newPassword + validate() + } + + /** + * Проверка, можно ли нажимать кнопку "Войти". + * Если оба поля не пустые → isButtonEnabled = true + */ + private fun validate() { + _isButtonEnabled.value = _email.value.isNotBlank() && _password.value.isNotBlank() + } + + /** + * При нажатии на кнопку "Войти" + * ViewModel запускает имитацию сетевого запроса: + * 1. Меняет состояние на Loading -> дизайнеры должны показать индикатор. + * 2. Через 1.5 секунды: + * - Если логин "user@example.com" и пароль "1234" -> Success + * - Иначе → Error + * + * Это имитация. В реальном приложении здесь будет запрос на сервер. + */ + fun onLoginClick() { + _uiState.value = AuthUiState.Loading + + // имитация запроса — здесь только логика, без дизайна + viewModelScope.launch { + delay(1500) + _uiState.value = if (_email.value == "user@example.com" && _password.value == "1234") { + AuthUiState.Success + } else { + AuthUiState.Error + } + } + } + fun resetToDefault() { + _uiState.value = AuthUiState.Default + } + fun onForgotPasswordClick() { /* TODO */ } + fun onCreateAccountClick() { /* TODO */ } + +} diff --git a/app/src/main/java/ru/myitschool/work/ui/root/RootActivity.kt b/app/src/main/java/ru/myitschool/work/ui/root/RootActivity.kt deleted file mode 100644 index 8fcaefb..0000000 --- a/app/src/main/java/ru/myitschool/work/ui/root/RootActivity.kt +++ /dev/null @@ -1,181 +0,0 @@ -package ru.myitschool.work.ui.root - -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.activity.enableEdgeToEdge -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material3.Button -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TextField -import androidx.compose.runtime.Composable -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.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel -import ru.myitschool.work.ui.theme.WorkTheme - -class RootActivity : ComponentActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - enableEdgeToEdge() - setContent { - WorkTheme { - Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> - Screen( - modifier = Modifier - .fillMaxSize() - .padding(innerPadding) - ) - } - } - } - } -} - -@Composable -fun Screen( - modifier: Modifier = Modifier, - viewModel: RootViewModel = viewModel() -) { - val state by viewModel.uiState.collectAsState() - - when (val currentState = state) { - is RootState.Content -> { - Column( - modifier = modifier, - ) { - Row( - modifier = Modifier.padding(horizontal = 8.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - modifier = Modifier.weight(1f), - text = currentState.userEntity.name, - style = MaterialTheme.typography.headlineSmall - ) - ButtonGetData(viewModel) - } - LazyColumn( - modifier = Modifier.weight(1f) - ) { - items(currentState.userEntity.bookingList) { book -> - Row( - modifier = Modifier.padding(vertical = 16.dp, horizontal = 8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - modifier = Modifier.weight(1f), - text = book.roomName, - style = MaterialTheme.typography.bodyLarge - ) - Text( - text = book.time, - style = MaterialTheme.typography.bodySmall - ) - } - } - } - Row( - modifier = Modifier.padding(horizontal = 8.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - var roomText by remember { mutableStateOf("") } - var timeText by remember { mutableStateOf("") } - - Column(modifier = Modifier.weight(1f)) { - TextField( - modifier = Modifier.fillMaxWidth(), - value = roomText, - onValueChange = { roomText = it }, - label = { Text("Room") } - ) - TextField( - modifier = Modifier - .padding(top = 8.dp) - .fillMaxWidth(), - value = timeText, - onValueChange = { timeText = it }, - label = { Text("Time") } - ) - } - Column( - modifier = Modifier.padding(start = 8.dp), - ) { - Button( - onClick = { - viewModel.onIntent( - RootIntent.AddBook(room = roomText, time = timeText) - ) - roomText = "" - timeText = "" - } - ) { - Text(text = "Add") - } - if (currentState.errorText != null) { - Text(text = currentState.errorText) - } - } - } - } - - } - - is RootState.Error -> { - Column( - modifier = modifier, - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Text(text = currentState.message) - ButtonGetData(viewModel) - } - } - - is RootState.Loading -> { - Box( - modifier = modifier, - contentAlignment = Alignment.Center, - ) { - CircularProgressIndicator(modifier = Modifier.size(64.dp)) - } - } - - is RootState.NotLoaded -> { - Box( - modifier = modifier, - contentAlignment = Alignment.Center, - ) { - ButtonGetData(viewModel) - } - - } - } -} - -@Composable -private fun ButtonGetData( - viewModel: RootViewModel -) { - Button(onClick = { viewModel.onIntent(RootIntent.LoadData) }) { - Text(text = "Get load") - } -} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/root/RootIntent.kt b/app/src/main/java/ru/myitschool/work/ui/root/RootIntent.kt deleted file mode 100644 index 9804e07..0000000 --- a/app/src/main/java/ru/myitschool/work/ui/root/RootIntent.kt +++ /dev/null @@ -1,9 +0,0 @@ -package ru.myitschool.work.ui.root - -sealed interface RootIntent { - data object LoadData: RootIntent - data class AddBook( - val room: String, - val time: String - ): RootIntent -} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/root/RootState.kt b/app/src/main/java/ru/myitschool/work/ui/root/RootState.kt deleted file mode 100644 index 1c5d32f..0000000 --- a/app/src/main/java/ru/myitschool/work/ui/root/RootState.kt +++ /dev/null @@ -1,13 +0,0 @@ -package ru.myitschool.work.ui.root - -import ru.myitschool.work.domain.entities.UserEntity - -sealed interface RootState { - data object NotLoaded: RootState - data object Loading: RootState - data class Error(val message: String): RootState - data class Content( - val userEntity: UserEntity, - val errorText: String?, - ): RootState -} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/root/RootViewModel.kt b/app/src/main/java/ru/myitschool/work/ui/root/RootViewModel.kt deleted file mode 100644 index d95ac89..0000000 --- a/app/src/main/java/ru/myitschool/work/ui/root/RootViewModel.kt +++ /dev/null @@ -1,68 +0,0 @@ -package ru.myitschool.work.ui.root - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import ru.myitschool.work.data.AppRepository -import ru.myitschool.work.domain.AddBookUseCase -import ru.myitschool.work.domain.GetUserDataUseCase - -class RootViewModel : ViewModel() { - private val getUserDataUseCase by lazy { - GetUserDataUseCase( - repository = AppRepository - ) - } - private val addBookUseCase by lazy { - AddBookUseCase( - repository = AppRepository - ) - } - private val _uiState = MutableStateFlow(RootState.NotLoaded) - val uiState: StateFlow = _uiState.asStateFlow() - - fun onIntent(intent: RootIntent) { - when (intent) { - is RootIntent.LoadData -> loadData() - is RootIntent.AddBook -> addBook(intent) - } - } - - private fun loadData() { - viewModelScope.launch { - _uiState.emit(RootState.Loading) - getUserDataUseCase.invoke().fold( - onSuccess = { value -> - _uiState.emit(RootState.Content(userEntity = value, errorText = null)) - }, - onFailure = { error -> - _uiState.emit(RootState.Error(error.message.orEmpty())) - } - ) - } - } - - private fun addBook(intent: RootIntent.AddBook) { - viewModelScope.launch { - addBookUseCase.invoke( - room = intent.room, - time = intent.time - ).fold( - onSuccess = { - loadData() - }, - onFailure = { error -> - _uiState.update { state -> - (state as? RootState.Content)?.copy( - errorText = error.message - ) ?: state - } - } - ) - } - } -} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f175030..18aaea0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,4 +1,17 @@ Work RootActivity + + Вход в систему + Email + Введите email + Пароль + Введите пароль + Показать пароль + Скрыть пароль + Войти + Ошибка: неверный логин или пароль + Забыли пароль? + Создать аккаунт + Добро пожаловать! \ No newline at end of file