diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a5ccda1..8b61370 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -35,6 +35,7 @@ android { } dependencies { + implementation("androidx.compose.material3:material3:1.4.0") defaultComposeLibrary() implementation("androidx.datastore:datastore-preferences:1.1.7") implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.4.0") @@ -48,4 +49,5 @@ dependencies { implementation("io.ktor:ktor-client-content-negotiation:$ktor") implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0") + implementation("io.coil-kt:coil-compose:2.6.0") } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a2c02bd..0eb09d5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -19,10 +19,9 @@ android:name=".ui.root.RootActivity" android:exported="true" android:windowSoftInputMode="adjustResize" - android:label="@string/title_activity_root"> + android:theme="@style/Theme.Work"> - diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/NavigationGraph.kt b/app/src/main/java/ru/myitschool/work/ui/screen/NavigationGraph.kt index 7890fab..d843e79 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/NavigationGraph.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/NavigationGraph.kt @@ -28,7 +28,8 @@ fun AppNavHost( enterTransition = { EnterTransition.None }, exitTransition = { ExitTransition.None }, navController = navController, - startDestination = AuthScreenDestination, +// startDestination = AuthScreenDestination, + startDestination = MainScreenDestination, ) { composable { AuthScreen(navController = navController) 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 1d78716..065a0c2 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 @@ -34,6 +34,7 @@ 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.foundation.layout.imePadding import androidx.compose.ui.text.input.ImeAction import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions @@ -54,6 +55,7 @@ fun AuthScreen( Column( modifier = Modifier .fillMaxSize() + .imePadding() .padding(24.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center 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 a4a71c8..0e9895d 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 @@ -2,20 +2,41 @@ package ru.myitschool.work.ui.screen.main import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults 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.text.style.TextAlign +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.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 @Composable @@ -25,27 +46,9 @@ fun MainScreen( ) { val state by viewModel.uiState.collectAsState() - LaunchedEffect(Unit) { - viewModel.actionFlow.collect { - navController.navigate(MainScreenDestination) - } - } - /* - Главный экран - Данный экран содержит информацию о пользователе и его текущие бронирования. Если пользователь авторизован, данный экран должен быть отображен при запуске приложения. - - Элементы, которые должны присутствовать на экране: - - Текстовое поле (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). + Требования к компонентам: В случае любой ошибки необходимо скрыть все элементы, кроме текстового поля с ошибкой и кнопки обновления данных. @@ -56,20 +59,184 @@ fun MainScreen( Список бронирований должен быть отсортирован в порядке увеличения даты (например, 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() - .padding(all = 24.dp), + .imePadding() + .padding( + start = 20.dp, + top = 20.dp, + end = 20.dp, + bottom = 0.dp), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center + verticalArrangement = Arrangement.Top ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ) { + AsyncImage( + model = "https://palyulin.ru/netcat_files/23/21/rabotnik.jpg", + 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( - text = "главная страница...", - style = MaterialTheme.typography.headlineSmall, - textAlign = TextAlign.Center - ) + Spacer(modifier = Modifier.size(8.dp)) + Button( + modifier = Modifier + .fillMaxWidth() + .testTag(TestIds.Main.LOGOUT_BUTTON), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.error, + contentColor = MaterialTheme.colorScheme.onError + ), + onClick = { + }, + ) { + Text(stringResource(R.string.logout)) + } + + 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 = { + }, + ) { + Text(stringResource(R.string.book_new)) + } + + Scaffold( + modifier = Modifier.fillMaxSize() + ) { paddingValues -> + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + ) { + itemsIndexed( + items = bookings, + key = { index, item -> "main_book_pos_$index" } + ) { index, booking -> + BookCard( + date = booking.date, + place = booking.place, + modifier = Modifier.padding( + top = if (index == 0) 0.dp else 8.dp, + bottom = if (index == bookings.lastIndex) 0.dp else 8.dp + ) + ) + } + } + } + } +} + +data class Booking( + val date: String, + val place: String +) + +@Composable +fun BookCard( + date: String, + place: String, + 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 + } + } + + Card( + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant, + ), + modifier = modifier.fillMaxWidth() + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Text( + text = "Бронь на $formattedDate", + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier + .testTag(TestIds.Main.ITEM_DATE) + ) + + Spacer(modifier = Modifier.size(8.dp)) + + Text( + text = place, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier + .testTag(TestIds.Main.ITEM_PLACE) + ) + } } } \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fa8bda6..1a695a3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,7 +1,9 @@ Work - RootActivity Привет! Введи код для авторизации Код Войти + Выйти + Обновить данные + Новая бронь \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..07e5fc2 --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file