списки + ник
Some checks failed
Android Test / validate-and-test (pull_request) Has been cancelled

This commit is contained in:
CryptoDruid802
2025-12-12 00:10:08 +03:00
parent e5df83f4e3
commit 08f40f72ca
9 changed files with 141 additions and 87 deletions

View File

@@ -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"

View File

@@ -0,0 +1,9 @@
package ru.myitschool.work.data.models
import kotlinx.serialization.Serializable
@Serializable
data class BookingInfo(
val id: Int,
val place: String
)

View File

@@ -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<String, BookingInfo>
)

View File

@@ -9,6 +9,10 @@ object AuthRepository {
codeCache = null
}
fun getCode(): String? {
return codeCache
}
suspend fun checkAndSave(text: String): Result<Boolean> {
return NetworkDataSource.checkAuth(text).onSuccess { success ->

View File

@@ -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<UserInfo> = 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<UserInfo>(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<Boolean> = 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"
}
}

View File

@@ -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()
}
}

View File

@@ -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/<CODE>/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
)
)

View File

@@ -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
}
data class Data(val userInfo: UserInfo) : MainState
}

View File

@@ -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>(MainState.Data)
val uiState: StateFlow<MainState> = _uiState.asStateFlow()
private val _actionFlow: MutableSharedFlow<Unit> = MutableSharedFlow()
val actionFlow: SharedFlow<Unit> = _actionFlow
private val _uiState = MutableStateFlow<MainState>(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()
}
}
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")
}
}
}
}