списки + ник

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 package ru.myitschool.work.core
object Constants { 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 AUTH_URL = "/auth"
const val INFO_URL = "/info" const val INFO_URL = "/info"
const val BOOKING_URL = "/booking" 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 codeCache = null
} }
fun getCode(): String? {
return codeCache
}
suspend fun checkAndSave(text: String): Result<Boolean> { suspend fun checkAndSave(text: String): Result<Boolean> {
return NetworkDataSource.checkAuth(text).onSuccess { success -> return NetworkDataSource.checkAuth(text).onSuccess { success ->

View File

@@ -11,23 +11,50 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import ru.myitschool.work.core.Constants import ru.myitschool.work.core.Constants
import ru.myitschool.work.data.models.UserInfo
object NetworkDataSource { object NetworkDataSource {
private val client by lazy { private val json = Json {
HttpClient(CIO) {
install(ContentNegotiation) {
json(
Json {
isLenient = true isLenient = true
ignoreUnknownKeys = true ignoreUnknownKeys = true
explicitNulls = true explicitNulls = true
encodeDefaults = true encodeDefaults = true
} }
)
private val client by lazy {
HttpClient(CIO) {
install(ContentNegotiation) {
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) { suspend fun checkAuth(code: String): Result<Boolean> = withContext(Dispatchers.IO) {
val url = getUrl(code, Constants.AUTH_URL) val url = getUrl(code, Constants.AUTH_URL)

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 coil3.compose.AsyncImage
import ru.myitschool.work.R import ru.myitschool.work.R
import ru.myitschool.work.core.TestIds import ru.myitschool.work.core.TestIds
import ru.myitschool.work.data.models.UserInfo
import ru.myitschool.work.data.repo.AuthRepository import ru.myitschool.work.data.repo.AuthRepository
import ru.myitschool.work.ui.nav.AuthScreenDestination import ru.myitschool.work.ui.nav.AuthScreenDestination
import ru.myitschool.work.ui.nav.BookScreenDestination import ru.myitschool.work.ui.nav.BookScreenDestination
/*
По умолчанию скрытое текстовое поле с ошибкой (main_error).
Требования к компонентам:
В случае любой ошибки необходимо скрыть все элементы, кроме текстового поля с ошибкой и кнопки обновления данных.
Для получения данных необходимо использовать сетевой запрос /api/<CODE>/info.
При нажатии на кнопку для выхода, все сохранённые данные пользователя должны быть очищены, а приложение должно открыть экран авторизации.
При нажатии кнопки бронирования необходимо открыть экран бронирования.
При нажатии на кнопку обновления данных — необходимо повторно вызывать сетевой запрос для получения актуальных данных.
Список бронирований должен быть отсортирован в порядке увеличения даты (например, 5 января -> 6 января -> 9 января).
*/
@Composable @Composable
fun MainScreen( fun MainScreen(
viewModel: MainViewModel = viewModel(), viewModel: MainViewModel = viewModel(),
@@ -71,59 +57,72 @@ fun MainScreen(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top verticalArrangement = Arrangement.Top
) { ) {
when (val s = state) {
is MainState.Error -> {
Text(
text = s.message,
color = MaterialTheme.colorScheme.error,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.testTag(TestIds.Main.ERROR)
)
Spacer(modifier = Modifier.size(8.dp))
Button( Button(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.testTag(TestIds.Main.REFRESH_BUTTON), .testTag(TestIds.Main.REFRESH_BUTTON),
onClick = { /* обновить данные */ } onClick = viewModel::onRefresh
) { ) {
Text(stringResource(R.string.refresh)) Text(stringResource(R.string.refresh))
} }
Spacer(modifier = Modifier.size(8.dp))
when (state) {
is MainState.Error -> {
Text(
text = (state as MainState.Error).message,
color = MaterialTheme.colorScheme.error,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
} }
is MainState.Loading -> { is MainState.Loading -> {
CircularProgressIndicator() CircularProgressIndicator()
} }
is MainState.Data -> { is MainState.Data -> {
MainContent(navController = navController) MainContent(
userInfo = s.userInfo,
navController = navController,
onRefresh = viewModel::onRefresh
)
} }
} }
} }
} }
@Composable @Composable
fun MainContent(navController: NavController) { fun MainContent(
val bookings = listOf( userInfo: UserInfo,
Booking(date = "2025-12-01", place = "Аудитория 1"), navController: NavController,
Booking(date = "2025-12-01", place = "Аудитория 2"), onRefresh: () -> Unit
Booking(date = "2025-12-02", place = "Аудитория 3"), ) {
Booking(date = "2025-12-02", place = "Конференц-зал"), val bookings = remember {
Booking(date = "2025-12-03", place = "Аудитория с очень длинным названием. Lorem ipsum"), userInfo.booking.entries.sortedBy { it.key }.map { Booking(it.key, it.value.place) }
Booking(date = "2025-12-03", place = "Лаборатория №101"), }
Booking(date = "2025-12-04", place = "Переговорная комната"),
Booking(date = "2025-12-04", place = "Спортивный зал"),
)
Column( Column(
modifier = Modifier.fillMaxSize() 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( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Start, horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
AsyncImage( AsyncImage(
model = "https://palyulin.ru/netcat_files/23/21/rabotnik.jpg", model = userInfo.photoUrl,
contentDescription = null, contentDescription = null,
contentScale = ContentScale.Crop, contentScale = ContentScale.Crop,
modifier = Modifier modifier = Modifier
@@ -132,7 +131,7 @@ fun MainContent(navController: NavController) {
) )
Spacer(modifier = Modifier.size(16.dp)) Spacer(modifier = Modifier.size(16.dp))
Text( Text(
text = "Иванов Иван Иванович", text = userInfo.name,
style = MaterialTheme.typography.titleLarge style = MaterialTheme.typography.titleLarge
) )
} }

View File

@@ -1,7 +1,9 @@
package ru.myitschool.work.ui.screen.main package ru.myitschool.work.ui.screen.main
import ru.myitschool.work.data.models.UserInfo
sealed interface MainState { sealed interface MainState {
data object Data : MainState object Loading : MainState
data object Loading : MainState
data class Error(val message: String) : 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.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import ru.myitschool.work.data.repo.AuthRepository import ru.myitschool.work.domain.GetUserInfoUseCase
import ru.myitschool.work.domain.auth.CheckAndSaveAuthCodeUseCase
class MainViewModel : ViewModel() { 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() private val _uiState = MutableStateFlow<MainState>(MainState.Loading)
val actionFlow: SharedFlow<Unit> = _actionFlow val uiState = _uiState.asStateFlow()
fun onIntent(intent: MainIntent) { private val getUserInfoUseCase = GetUserInfoUseCase()
// when (intent) {
// is MainIntent.Send -> { init {
// viewModelScope.launch(Dispatchers.Default) { loadData()
// _uiState.update { MainState.Loading } }
// checkAndSaveAuthCodeUseCase.invoke("9999").fold(
// onSuccess = { fun onRefresh() {
// _actionFlow.emit(Unit) loadData()
// }, }
// onFailure = { error ->
// error.printStackTrace() private fun loadData() {
// _actionFlow.emit(Unit) viewModelScope.launch {
// } _uiState.value = MainState.Loading
// ) getUserInfoUseCase().onSuccess {
// } _uiState.value = MainState.Data(it)
// } }.onFailure {
// is MainIntent.TextInput -> Unit _uiState.value = MainState.Error(it.message ?: "Unknown error")
// } }
}
} }
} }