main #15

Open
student-33039 wants to merge 9 commits from student-33039/NTO-2025-Android-TeamTask:main into main
7 changed files with 212 additions and 31 deletions
Showing only changes of commit 72e8b946c0 - Show all commits

View File

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

View File

@@ -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">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

View File

@@ -28,7 +28,8 @@ fun AppNavHost(
enterTransition = { EnterTransition.None },
exitTransition = { ExitTransition.None },
navController = navController,
startDestination = AuthScreenDestination,
// startDestination = AuthScreenDestination,
startDestination = MainScreenDestination,
) {
composable<AuthScreenDestination> {
AuthScreen(navController = navController)

View File

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

View File

@@ -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
) {
Text(
text = "главная страница...",
style = MaterialTheme.typography.headlineSmall,
textAlign = TextAlign.Center
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)
)
}
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)
)
}
}
}

View File

@@ -1,7 +1,9 @@
<resources>
<string name="app_name">Work</string>
<string name="title_activity_root">RootActivity</string>
<string name="auth_title">Привет! Введи код для авторизации</string>
<string name="auth_label">Код</string>
<string name="auth_sign_in">Войти</string>
<string name="logout">Выйти</string>
<string name="refresh">Обновить данные</string>
<string name="book_new">Новая бронь</string>
</resources>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.Work" parent="Theme.Material3.DayNight.NoActionBar">
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>
</resources>