From 08f40f72caa7eee3cca59c677f2fe08a0d2ef544 Mon Sep 17 00:00:00 2001 From: CryptoDruid802 Date: Fri, 12 Dec 2025 00:10:08 +0300 Subject: [PATCH] =?UTF-8?q?=D1=81=D0=BF=D0=B8=D1=81=D0=BA=D0=B8=20+=20?= =?UTF-8?q?=D0=BD=D0=B8=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/ru/myitschool/work/core/Constants.kt | 2 +- .../work/data/models/BookingInfo.kt | 9 ++ .../myitschool/work/data/models/UserInfo.kt | 10 +++ .../work/data/repo/AuthRepository.kt | 4 + .../work/data/source/NetworkDataSource.kt | 45 ++++++++-- .../work/domain/GetUserInfoUseCase.kt | 11 +++ .../work/ui/screen/main/MainScreen.kt | 85 +++++++++---------- .../work/ui/screen/main/MainState.kt | 8 +- .../work/ui/screen/main/MainViewModel.kt | 54 +++++------- 9 files changed, 141 insertions(+), 87 deletions(-) create mode 100644 app/src/main/java/ru/myitschool/work/data/models/BookingInfo.kt create mode 100644 app/src/main/java/ru/myitschool/work/data/models/UserInfo.kt create mode 100644 app/src/main/java/ru/myitschool/work/domain/GetUserInfoUseCase.kt diff --git a/app/src/main/java/ru/myitschool/work/core/Constants.kt b/app/src/main/java/ru/myitschool/work/core/Constants.kt index a8b7cc5..13f7e39 100644 --- a/app/src/main/java/ru/myitschool/work/core/Constants.kt +++ b/app/src/main/java/ru/myitschool/work/core/Constants.kt @@ -1,7 +1,7 @@ package ru.myitschool.work.core object Constants { - const val HOST = "http://10.0.2.2:8080" + const val HOST = "http://192.168.0.111:8080" const val AUTH_URL = "/auth" const val INFO_URL = "/info" const val BOOKING_URL = "/booking" diff --git a/app/src/main/java/ru/myitschool/work/data/models/BookingInfo.kt b/app/src/main/java/ru/myitschool/work/data/models/BookingInfo.kt new file mode 100644 index 0000000..d915ec1 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/data/models/BookingInfo.kt @@ -0,0 +1,9 @@ +package ru.myitschool.work.data.models + +import kotlinx.serialization.Serializable + +@Serializable +data class BookingInfo( + val id: Int, + val place: String +) diff --git a/app/src/main/java/ru/myitschool/work/data/models/UserInfo.kt b/app/src/main/java/ru/myitschool/work/data/models/UserInfo.kt new file mode 100644 index 0000000..699b399 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/data/models/UserInfo.kt @@ -0,0 +1,10 @@ +package ru.myitschool.work.data.models + +import kotlinx.serialization.Serializable + +@Serializable +data class UserInfo( + val name: String, + val photoUrl: String, + val booking: Map +) diff --git a/app/src/main/java/ru/myitschool/work/data/repo/AuthRepository.kt b/app/src/main/java/ru/myitschool/work/data/repo/AuthRepository.kt index 9ed569a..57bbc44 100644 --- a/app/src/main/java/ru/myitschool/work/data/repo/AuthRepository.kt +++ b/app/src/main/java/ru/myitschool/work/data/repo/AuthRepository.kt @@ -9,6 +9,10 @@ object AuthRepository { codeCache = null } + fun getCode(): String? { + return codeCache + } + suspend fun checkAndSave(text: String): Result { return NetworkDataSource.checkAuth(text).onSuccess { success -> 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 da6c74f..a4afdc5 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 @@ -11,23 +11,50 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json import ru.myitschool.work.core.Constants +import ru.myitschool.work.data.models.UserInfo object NetworkDataSource { + private val json = Json { + isLenient = true + ignoreUnknownKeys = true + explicitNulls = true + encodeDefaults = true + } + private val client by lazy { HttpClient(CIO) { install(ContentNegotiation) { - json( - Json { - isLenient = true - ignoreUnknownKeys = true - explicitNulls = true - encodeDefaults = true - } - ) + json(json) } } } + suspend fun getUserInfo(code: String): Result = withContext(Dispatchers.IO) { + val url = getUrl(code, "/info") + + println("➡ Request URL: $url") + + runCatching { + val response = client.get(url) + + println("⬅ Response status: ${response.status}") + + when (response.status) { + HttpStatusCode.OK -> { + val body = response.bodyAsText() + println("⬅ Response body: $body") + json.decodeFromString(body) + } + HttpStatusCode.Unauthorized -> error("Код не существует") + HttpStatusCode.BadRequest -> error("Что-то пошло не так") + else -> error("Неизвестная ошибка: ${response.status}") + } + }.recoverCatching { e -> + println("❌ Error: ${e.message}") + throw Exception(e.message) + } + } + suspend fun checkAuth(code: String): Result = withContext(Dispatchers.IO) { val url = getUrl(code, Constants.AUTH_URL) @@ -55,4 +82,4 @@ object NetworkDataSource { 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/domain/GetUserInfoUseCase.kt b/app/src/main/java/ru/myitschool/work/domain/GetUserInfoUseCase.kt new file mode 100644 index 0000000..536c16d --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/domain/GetUserInfoUseCase.kt @@ -0,0 +1,11 @@ +package ru.myitschool.work.domain + +import ru.myitschool.work.data.repo.AuthRepository +import ru.myitschool.work.data.source.NetworkDataSource + +class GetUserInfoUseCase { + suspend operator fun invoke() = runCatching { + val code = AuthRepository.getCode() ?: error("Вы не авторизованы") + NetworkDataSource.getUserInfo(code).getOrThrow() + } +} 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 7d4fd94..eba5738 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 @@ -37,25 +37,11 @@ import androidx.navigation.NavController import coil3.compose.AsyncImage import ru.myitschool.work.R import ru.myitschool.work.core.TestIds +import ru.myitschool.work.data.models.UserInfo import ru.myitschool.work.data.repo.AuthRepository import ru.myitschool.work.ui.nav.AuthScreenDestination import ru.myitschool.work.ui.nav.BookScreenDestination - -/* -По умолчанию скрытое текстовое поле с ошибкой (main_error). - -Требования к компонентам: - -В случае любой ошибки необходимо скрыть все элементы, кроме текстового поля с ошибкой и кнопки обновления данных. -Для получения данных необходимо использовать сетевой запрос /api//info. -При нажатии на кнопку для выхода, все сохранённые данные пользователя должны быть очищены, а приложение должно открыть экран авторизации. -При нажатии кнопки бронирования необходимо открыть экран бронирования. -При нажатии на кнопку обновления данных — необходимо повторно вызывать сетевой запрос для получения актуальных данных. -Список бронирований должен быть отсортирован в порядке увеличения даты (например, 5 января -> 6 января -> 9 января). - */ - - @Composable fun MainScreen( viewModel: MainViewModel = viewModel(), @@ -71,59 +57,72 @@ fun MainScreen( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Top ) { - Button( - modifier = Modifier - .fillMaxWidth() - .testTag(TestIds.Main.REFRESH_BUTTON), - onClick = { /* обновить данные */ } - ) { - Text(stringResource(R.string.refresh)) - } - Spacer(modifier = Modifier.size(8.dp)) - - when (state) { + when (val s = state) { is MainState.Error -> { Text( - text = (state as MainState.Error).message, + text = s.message, color = MaterialTheme.colorScheme.error, textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth() + modifier = Modifier + .fillMaxWidth() + .testTag(TestIds.Main.ERROR) ) + Spacer(modifier = Modifier.size(8.dp)) + Button( + modifier = Modifier + .fillMaxWidth() + .testTag(TestIds.Main.REFRESH_BUTTON), + onClick = viewModel::onRefresh + ) { + Text(stringResource(R.string.refresh)) + } } is MainState.Loading -> { CircularProgressIndicator() } is MainState.Data -> { - MainContent(navController = navController) + MainContent( + userInfo = s.userInfo, + navController = navController, + onRefresh = viewModel::onRefresh + ) } } } } @Composable -fun MainContent(navController: NavController) { - val bookings = listOf( - Booking(date = "2025-12-01", place = "Аудитория 1"), - Booking(date = "2025-12-01", place = "Аудитория 2"), - Booking(date = "2025-12-02", place = "Аудитория 3"), - Booking(date = "2025-12-02", place = "Конференц-зал"), - Booking(date = "2025-12-03", place = "Аудитория с очень длинным названием. Lorem ipsum"), - Booking(date = "2025-12-03", place = "Лаборатория №101"), - Booking(date = "2025-12-04", place = "Переговорная комната"), - Booking(date = "2025-12-04", place = "Спортивный зал"), - ) +fun MainContent( + userInfo: UserInfo, + navController: NavController, + onRefresh: () -> Unit +) { + val bookings = remember { + userInfo.booking.entries.sortedBy { it.key }.map { Booking(it.key, it.value.place) } + } Column( modifier = Modifier.fillMaxSize() ) { + Button( + modifier = Modifier + .fillMaxWidth() + .testTag(TestIds.Main.REFRESH_BUTTON), + onClick = onRefresh + ) { + Text(stringResource(R.string.refresh)) + } + + Spacer(modifier = Modifier.size(8.dp)) + Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Start, verticalAlignment = Alignment.CenterVertically ) { AsyncImage( - model = "https://palyulin.ru/netcat_files/23/21/rabotnik.jpg", + model = userInfo.photoUrl, contentDescription = null, contentScale = ContentScale.Crop, modifier = Modifier @@ -132,7 +131,7 @@ fun MainContent(navController: NavController) { ) Spacer(modifier = Modifier.size(16.dp)) Text( - text = "Иванов Иван Иванович", + text = userInfo.name, style = MaterialTheme.typography.titleLarge ) } @@ -236,4 +235,4 @@ fun BookCard( data class Booking( val date: String, val place: String -) \ No newline at end of file +) diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/main/MainState.kt b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainState.kt index 680d44a..b02d4c6 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/main/MainState.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainState.kt @@ -1,7 +1,9 @@ package ru.myitschool.work.ui.screen.main +import ru.myitschool.work.data.models.UserInfo + sealed interface MainState { - data object Data : MainState - data object Loading : MainState + object Loading : MainState data class Error(val message: String) : MainState -} \ No newline at end of file + data class Data(val userInfo: UserInfo) : MainState +} diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/main/MainViewModel.kt b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainViewModel.kt index 80d525e..63c359c 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/main/MainViewModel.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainViewModel.kt @@ -2,42 +2,34 @@ package ru.myitschool.work.ui.screen.main import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import ru.myitschool.work.data.repo.AuthRepository -import ru.myitschool.work.domain.auth.CheckAndSaveAuthCodeUseCase +import ru.myitschool.work.domain.GetUserInfoUseCase class MainViewModel : ViewModel() { - private val checkAndSaveAuthCodeUseCase by lazy { CheckAndSaveAuthCodeUseCase(AuthRepository) } - private val _uiState = MutableStateFlow(MainState.Data) - val uiState: StateFlow = _uiState.asStateFlow() - private val _actionFlow: MutableSharedFlow = MutableSharedFlow() - val actionFlow: SharedFlow = _actionFlow + private val _uiState = MutableStateFlow(MainState.Loading) + val uiState = _uiState.asStateFlow() - fun onIntent(intent: MainIntent) { -// when (intent) { -// is MainIntent.Send -> { -// viewModelScope.launch(Dispatchers.Default) { -// _uiState.update { MainState.Loading } -// checkAndSaveAuthCodeUseCase.invoke("9999").fold( -// onSuccess = { -// _actionFlow.emit(Unit) -// }, -// onFailure = { error -> -// error.printStackTrace() -// _actionFlow.emit(Unit) -// } -// ) -// } -// } -// is MainIntent.TextInput -> Unit -// } + private val getUserInfoUseCase = GetUserInfoUseCase() + + init { + loadData() } -} \ No newline at end of file + + fun onRefresh() { + loadData() + } + + private fun loadData() { + viewModelScope.launch { + _uiState.value = MainState.Loading + getUserInfoUseCase().onSuccess { + _uiState.value = MainState.Data(it) + }.onFailure { + _uiState.value = MainState.Error(it.message ?: "Unknown error") + } + } + } +}