From dbc4830418ddae1692269c89615f7a13b06bffe9 Mon Sep 17 00:00:00 2001 From: githubchikov Date: Sun, 30 Nov 2025 22:22:20 +0300 Subject: [PATCH] feat: edit auth screen --- .../work/data/source/NetworkDataSource.kt | 12 +++- .../work/ui/screen/auth/AuthScreen.kt | 67 ++++++++++++++++--- .../work/ui/screen/auth/AuthState.kt | 7 +- .../work/ui/screen/auth/AuthViewModel.kt | 28 +++++--- .../work/ui/screen/book/BookScreen.kt | 22 ++++++ .../work/ui/screen/main/MainScreen.kt | 26 +++++++ 6 files changed, 135 insertions(+), 27 deletions(-) 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 index fbdfef5..8772066 100644 --- a/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt +++ b/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt @@ -4,7 +4,6 @@ import io.ktor.client.HttpClient import io.ktor.client.engine.cio.CIO import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.request.get -import io.ktor.client.statement.bodyAsText import io.ktor.http.HttpStatusCode import io.ktor.serialization.kotlinx.json.json import kotlinx.coroutines.Dispatchers @@ -29,14 +28,21 @@ object NetworkDataSource { } suspend fun checkAuth(code: String): Result = withContext(Dispatchers.IO) { - return@withContext runCatching { + runCatching { val response = client.get(getUrl(code, Constants.AUTH_URL)) when (response.status) { HttpStatusCode.OK -> true - else -> error(response.bodyAsText()) + HttpStatusCode.Unauthorized -> error("Код не существует") + HttpStatusCode.BadRequest -> error("Что-то пошло не так") + else -> error("Неизвестная ошибка: ${response.status}") } + }.mapCatching { success -> + success + }.recoverCatching { _ -> + throw Exception("Не удалось соединиться с сервером") } } + private fun getUrl(code: String, targetUrl: String) = "${Constants.HOST}/api/$code$targetUrl" } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthScreen.kt b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthScreen.kt index 04a269d..1d78716 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthScreen.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthScreen.kt @@ -31,6 +31,12 @@ import androidx.navigation.NavController import ru.myitschool.work.R import ru.myitschool.work.core.TestIds import ru.myitschool.work.ui.nav.MainScreenDestination +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions @Composable fun AuthScreen( @@ -48,50 +54,89 @@ fun AuthScreen( Column( modifier = Modifier .fillMaxSize() - .padding(all = 24.dp), + .padding(24.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { - when (val currentState = state) { - is AuthState.Data -> Content(viewModel, currentState) + when (state) { is AuthState.Loading -> { - CircularProgressIndicator( - modifier = Modifier.size(64.dp) - ) + CircularProgressIndicator(modifier = Modifier.size(64.dp)) + } + + else -> { + Content(viewModel, state) } } } } + @Composable private fun Content( viewModel: AuthViewModel, - state: AuthState.Data + state: AuthState ) { var inputText by remember { mutableStateOf("") } + val isButtonEnabled = + inputText.length == 4 && inputText.all { it.isLetterOrDigit() } + Text( text = stringResource(R.string.auth_title), style = MaterialTheme.typography.headlineSmall, textAlign = TextAlign.Center ) + Spacer(modifier = Modifier.size(16.dp)) + TextField( - modifier = Modifier.testTag(TestIds.Auth.CODE_INPUT).fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .testTag(TestIds.Auth.CODE_INPUT), value = inputText, onValueChange = { - inputText = it + if (it.length <= 4 && it.all { ch -> ch.isLetterOrDigit() }) { + inputText = it + } viewModel.onIntent(AuthIntent.TextInput(it)) }, + keyboardOptions = KeyboardOptions.Default.copy( + imeAction = ImeAction.Done + ), + keyboardActions = KeyboardActions( + onDone = { + viewModel.onIntent(AuthIntent.Send(inputText)) + } + ), + singleLine = true, label = { Text(stringResource(R.string.auth_label)) } ) + + Spacer(modifier = Modifier.size(12.dp)) + + AnimatedVisibility( + visible = state is AuthState.Error, + enter = fadeIn(), + exit = fadeOut() + ) { + Text( + text = (state as AuthState.Error).message, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.error, + textAlign = TextAlign.Center + ) + } + Spacer(modifier = Modifier.size(16.dp)) + Button( - modifier = Modifier.testTag(TestIds.Auth.SIGN_BUTTON).fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .testTag(TestIds.Auth.SIGN_BUTTON), onClick = { viewModel.onIntent(AuthIntent.Send(inputText)) }, - enabled = true + enabled = isButtonEnabled ) { Text(stringResource(R.string.auth_sign_in)) } diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthState.kt b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthState.kt index a06ba76..a4c1793 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthState.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthState.kt @@ -1,6 +1,7 @@ package ru.myitschool.work.ui.screen.auth -sealed interface AuthState { - object Loading: AuthState - object Data: AuthState +sealed class AuthState { + data object Data : AuthState() + data object Loading : AuthState() + data class Error(val message: String) : AuthState() } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthViewModel.kt b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthViewModel.kt index 3153640..289896d 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthViewModel.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthViewModel.kt @@ -13,31 +13,39 @@ import kotlinx.coroutines.launch import ru.myitschool.work.data.repo.AuthRepository import ru.myitschool.work.domain.auth.CheckAndSaveAuthCodeUseCase -class AuthViewModel : ViewModel() { - private val checkAndSaveAuthCodeUseCase by lazy { CheckAndSaveAuthCodeUseCase(AuthRepository) } +class AuthViewModel() : ViewModel() { + private val checkAndSaveAuthCodeUseCase by lazy { + CheckAndSaveAuthCodeUseCase(AuthRepository) + } + private val _uiState = MutableStateFlow(AuthState.Data) val uiState: StateFlow = _uiState.asStateFlow() - private val _actionFlow: MutableSharedFlow = MutableSharedFlow() + private val _actionFlow = MutableSharedFlow() val actionFlow: SharedFlow = _actionFlow fun onIntent(intent: AuthIntent) { when (intent) { + is AuthIntent.Send -> { - viewModelScope.launch(Dispatchers.Default) { - _uiState.update { AuthState.Loading } - checkAndSaveAuthCodeUseCase.invoke("9999").fold( + viewModelScope.launch(Dispatchers.IO) { + _uiState.value = AuthState.Loading + + checkAndSaveAuthCodeUseCase.invoke(intent.text).fold( onSuccess = { _actionFlow.emit(Unit) }, - onFailure = { error -> - error.printStackTrace() - _actionFlow.emit(Unit) + onFailure = { throwable -> + val errorMessage = throwable.message ?: "Неизвестная ошибка" + _uiState.value = AuthState.Error(errorMessage) } ) } } - is AuthIntent.TextInput -> Unit + + is AuthIntent.TextInput -> { + _uiState.value = AuthState.Data + } } } } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt index c4af615..6adc0ed 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt @@ -31,6 +31,28 @@ fun BookScreen( } } + /* + Экран бронирования + На данном экране необходимо вывести возможные даты и места для бронирования. + Элементы, которые должны присутствовать на экране: + Группа вкладок. Каждая вкладка (book_date_pos_{индекс}) содержит текстовое поле (book_date) с датой бронирования в формате dd.MM. + В зависимости от выбранной даты необходимо отобразить группу с единственным выбором (пояснения на изображении ниже). Каждый элемент группы (book_place_pos_{индекс}) кликабелен и содержит: + Текстовое поле (book_place_text), в котором содержится место доступное для брони. + Селектор (book_place_selector), который отображает, выбран элемент или нет. У данного элемента обязательно наличие: (Modifier.selectable) + Кнопка (book_book_button) для бронирования. + Кнопка (book_back_button) для возвращения на предыдущий экран. + По умолчанию неотображаемое текстовое поле с ошибкой (book_error). Отметим, что это поле не должно рендериться. + По умолчанию неотображаемая кнопка обновить (book_refresh_button). + По умолчанию неотображаемый текст “Всё забронировано” (book_empty). + + Требования к компонентам: + По умолчанию выбирается самая ранняя доступная дата (например, из набора "5 января", "6 января", "9 января" будет показана дата "5 января"). + Список дат отсортирован по возрастанию. Даты без доступных мест для бронирования необходимо не отображать. + Если нет доступных для бронирования дат, необходимо скрыть все элементы, кроме элементов из п. 4 и 7. + В случае ошибки при получении данных о доступном бронировании в запросе api//booking, необходимо отобразить элемент из п. 4, 5 и 6 с возможностью обновить данные. + При успешном бронировании нужно закрыть текущий экран и вернуться на главный, обновив его. + */ + Column( modifier = Modifier .fillMaxSize() diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/main/MainScreen.kt b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainScreen.kt index 4e6d034..a4a71c8 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/main/MainScreen.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainScreen.kt @@ -31,6 +31,32 @@ fun MainScreen( } } + /* + Главный экран + Данный экран содержит информацию о пользователе и его текущие бронирования. Если пользователь авторизован, данный экран должен быть отображен при запуске приложения. + + Элементы, которые должны присутствовать на экране: + + Текстовое поле (main_name), в котором написано имя пользователя. + Изображение (main_photo), на котором отображено фото пользователя. + Кнопка (main_logout_button) для выхода пользователя из аккаунта. + Кнопка (main_refresh_button) для принудительного обновления данных. + Кнопка (main_add_button) для бронирования. + Список, содержащий однотипные элементы (main_book_pos_{индекс}), со следующим содержимым: + Текстовое поле (main_item_date) с датой бронирования в формате dd.MM.yyyy. + Текстовое поле (main_item_place) с местом, которое забронировано. + По умолчанию скрытое текстовое поле с ошибкой (main_error). + Требования к компонентам: + + В случае любой ошибки необходимо скрыть все элементы, кроме текстового поля с ошибкой и кнопки обновления данных. + Для получения данных необходимо использовать сетевой запрос /api//info. + При нажатии на кнопку для выхода, все сохранённые данные пользователя должны быть очищены, а приложение должно открыть экран авторизации. + При нажатии кнопки бронирования необходимо открыть экран бронирования. + При нажатии на кнопку обновления данных — необходимо повторно вызывать сетевой запрос для получения актуальных данных. + Список бронирований должен быть отсортирован в порядке увеличения даты (например, 5 января -> 6 января -> 9 января). + */ + + Column( modifier = Modifier .fillMaxSize()