Compare commits

...

2 Commits

Author SHA1 Message Date
CryptoDruid802
08f40f72ca списки + ник 2025-12-12 00:10:08 +03:00
e5df83f4e3 fix: fix main screen 2025-12-11 22:33:57 +03:00
9 changed files with 181 additions and 134 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

@@ -16,30 +16,31 @@ import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import coil3.compose.AsyncImage
import ru.myitschool.work.R
import ru.myitschool.work.core.TestIds
import ru.myitschool.work.ui.nav.MainScreenDestination
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
@Composable
fun MainScreen(
@@ -48,80 +49,99 @@ fun MainScreen(
) {
val state by viewModel.uiState.collectAsState()
/*
По умолчанию скрытое текстовое поле с ошибкой (main_error).
Требования к компонентам:
В случае любой ошибки необходимо скрыть все элементы, кроме текстового поля с ошибкой и кнопки обновления данных.
Для получения данных необходимо использовать сетевой запрос /api/<CODE>/info.
При нажатии на кнопку для выхода, все сохранённые данные пользователя должны быть очищены, а приложение должно открыть экран авторизации. ГОТОВО
При нажатии кнопки бронирования необходимо открыть экран бронирования.
При нажатии на кнопку обновления данных — необходимо повторно вызывать сетевой запрос для получения актуальных данных.
Список бронирований должен быть отсортирован в порядке увеличения даты (например, 5 января -> 6 января -> 9 января).
*/
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 = "Спортивный зал"),
)
LaunchedEffect(Unit) {
viewModel.actionFlow.collect {
navController.navigate(MainScreenDestination)
}
}
Column(
modifier = Modifier
.fillMaxSize()
.imePadding()
.padding(
start = 20.dp,
top = 20.dp,
end = 20.dp,
bottom = 0.dp),
.padding(20.dp),
horizontalAlignment = Alignment.CenterHorizontally,
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(
modifier = Modifier
.fillMaxWidth()
.testTag(TestIds.Main.REFRESH_BUTTON),
onClick = viewModel::onRefresh
) {
Text(stringResource(R.string.refresh))
}
}
is MainState.Loading -> {
CircularProgressIndicator()
}
is MainState.Data -> {
MainContent(
userInfo = s.userInfo,
navController = navController,
onRefresh = viewModel::onRefresh
)
}
}
}
}
@Composable
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
.size(60.dp)
.clip(CircleShape)
.testTag(TestIds.Main.PROFILE_IMAGE)
)
Spacer(modifier = Modifier.size(16.dp))
Text(
text = "Иванов Иван Иванович",
style = MaterialTheme.typography.titleLarge,
modifier = Modifier
.testTag(TestIds.Main.PROFILE_NAME)
text = userInfo.name,
style = MaterialTheme.typography.titleLarge
)
}
Spacer(modifier = Modifier.size(8.dp))
Button(
modifier = Modifier
.fillMaxWidth()
.testTag(TestIds.Main.LOGOUT_BUTTON),
modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.error,
contentColor = MaterialTheme.colorScheme.onError
containerColor = MaterialTheme.colorScheme.error
),
onClick = {
AuthRepository.clearCode()
@@ -134,35 +154,14 @@ fun MainScreen(
Spacer(modifier = Modifier.size(8.dp))
Button(
modifier = Modifier
.fillMaxWidth()
.testTag(TestIds.Main.REFRESH_BUTTON),
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimary
),
onClick = {
},
) {
Text(stringResource(R.string.refresh))
}
Spacer(modifier = Modifier.size(8.dp))
Button(
modifier = Modifier
.fillMaxWidth()
.testTag(TestIds.Main.ADD_BUTTON),
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFF2E7D32),
contentColor = Color.White
),
onClick = {
},
modifier = Modifier.fillMaxWidth(),
onClick = { navController.navigate(BookScreenDestination) }
) {
Text(stringResource(R.string.book_new))
}
Spacer(modifier = Modifier.size(8.dp))
Scaffold(
modifier = Modifier.fillMaxSize()
) { paddingValues ->
@@ -189,11 +188,6 @@ fun MainScreen(
}
}
data class Booking(
val date: String,
val place: String
)
@Composable
fun BookCard(
date: String,
@@ -201,16 +195,9 @@ fun BookCard(
modifier: Modifier = Modifier
) {
val formattedDate = remember(date) {
try {
val parts = date.split("-")
if (parts.size == 3) {
"${parts[2]}.${parts[1]}.${parts[0]}"
} else {
date
}
} catch (_: Exception) {
date
}
runCatching {
java.time.LocalDate.parse(date).format(java.time.format.DateTimeFormatter.ofPattern("dd.MM.yyyy"))
}.getOrElse { date }
}
Card(
@@ -243,4 +230,9 @@ 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")
}
}
}
}