First request #5

Closed
student-21892 wants to merge 14 commits from student-21892/NTO-2025-Android-minipigs:main into main
10 changed files with 305 additions and 12 deletions
Showing only changes of commit 053a916b55 - 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")

View File

@@ -23,6 +23,7 @@ import ru.myitschool.work.ui.nav.SplashScreenDestination
import ru.myitschool.work.ui.root.RootState
import ru.myitschool.work.ui.screen.auth.AuthIntent
import ru.myitschool.work.ui.screen.auth.AuthScreen
import ru.myitschool.work.ui.screen.book.BookScreen
import ru.myitschool.work.ui.screen.main.MainScreen
import ru.myitschool.work.ui.screen.splash.SplashScreen
@@ -38,8 +39,8 @@ fun AppNavHost(
NavHost(
modifier = modifier,
enterTransition = { EnterTransition.None },
exitTransition = { ExitTransition.None },
// enterTransition = { EnterTransition.None },
// exitTransition = { ExitTransition.None },
navController = navController,
startDestination = startDestination,
) {
@@ -53,11 +54,7 @@ fun AppNavHost(
MainScreen(navController = navController)
}
composable<BookScreenDestination> {
Box(
contentAlignment = Alignment.Center
) {
Text(text = "BOOK")
}
BookScreen(navController = navController)
}
}
}

View File

@@ -3,6 +3,5 @@ package ru.myitschool.work.ui.screen.auth
sealed interface AuthState {
object Loading: AuthState
object Data: AuthState
object Error: AuthState
}

View File

@@ -0,0 +1,7 @@
package ru.myitschool.work.ui.screen.book
sealed interface BookIntent {
data object Fetch: BookIntent
data object Book: BookIntent
data object GoBack: BookIntent
}

View File

@@ -0,0 +1,221 @@
package ru.myitschool.work.ui.screen.book
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.selection.selectable
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBackIosNew
import androidx.compose.material.icons.filled.BookmarkAdd
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PrimaryScrollableTabRow
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Tab
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.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import ru.myitschool.work.core.TestIds
import ru.myitschool.work.ui.nav.MainScreenDestination
@Composable
fun BookScreen(
navController: NavController,
viewModel: BookViewModel = viewModel()
) {
val state by viewModel.uiState.collectAsState()
val dates = remember { bookingsByDate.keys.sorted() }
var selectedTabIndex by remember { mutableStateOf(0) }
Box(
modifier = Modifier
.fillMaxSize()
){
when(val currentState = state) {
is BookState.Loading -> {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
CircularProgressIndicator()
}
}
is BookState.Error -> {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
) {
Text(
text = "TEST_ERROR",
modifier = Modifier.testTag(TestIds.Book.ERROR),
color = MaterialTheme.colorScheme.error
)
}
FloatingActionButton(
onClick = { viewModel.onIntent(BookIntent.Fetch) },
containerColor = MaterialTheme.colorScheme.secondaryContainer,
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
modifier = Modifier
.align(Alignment.BottomEnd)
.offset(x = -16.dp, y = -16.dp)
.testTag(TestIds.Book.REFRESH_BUTTON)
) {
Icon(Icons.Default.Refresh, contentDescription = "Обновить")
}
}
is BookState.DataPresent -> {
Column(modifier = Modifier.fillMaxSize()) {
PrimaryScrollableTabRow(
selectedTabIndex = selectedTabIndex,
edgePadding = 16.dp,
containerColor = MaterialTheme.colorScheme.surface,
contentColor = MaterialTheme.colorScheme.primary,
) {
dates.forEachIndexed { index, date ->
Tab(
selected = selectedTabIndex == index,
onClick = { selectedTabIndex = index },
text = {
Text(
text = date,
modifier = Modifier.testTag(TestIds.Book.ITEM_DATE)
)
},
modifier = Modifier.testTag(TestIds.Book.getIdDateItemByPosition(index))
)
}
}
val selectedDate = dates[selectedTabIndex]
val bookings = bookingsByDate[selectedDate] ?: emptyList()
LazyColumn(
modifier = Modifier.fillMaxSize(),
) {
itemsIndexed(bookings) { index, booking ->
Booking(booking, index)
}
}
}
ExtendedFloatingActionButton(
onClick = { viewModel.onIntent(BookIntent.Book) },
containerColor = MaterialTheme.colorScheme.secondaryContainer,
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
text = {
Text("Бронировать")
},
icon = {
Icon(Icons.Default.BookmarkAdd, contentDescription = "Бронировать")
},
modifier = Modifier
.align(Alignment.BottomEnd)
.offset(x = -16.dp, y = -16.dp)
.testTag(TestIds.Book.BOOK_BUTTON)
)
}
is BookState.DataAbsent -> {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
) {
Text(text = "Всё забронировано", modifier = Modifier.testTag(TestIds.Book.EMPTY))
}
}
}
FloatingActionButton(
onClick = { viewModel.onIntent(BookIntent.GoBack) },
containerColor = MaterialTheme.colorScheme.secondaryContainer,
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
modifier = Modifier
.align(Alignment.BottomStart)
.offset(x = 16.dp, y = -16.dp)
.testTag(TestIds.Book.BACK_BUTTON)
) {
Icon(Icons.Default.ArrowBackIosNew, contentDescription = "Назад")
}
}
LaunchedEffect(Unit) {
viewModel.actionFlow.collect {
navController.navigate(MainScreenDestination)
}
}
}
@Composable
private fun Booking(booking: Booking, index: Int){
Row(
modifier = Modifier
.fillMaxWidth()
// .clickable { }
.padding(horizontal = 16.dp, vertical = 8.dp)
.testTag(TestIds.Book.getIdPlaceItemByPosition(index)),
verticalAlignment = Alignment.CenterVertically,
) {
RadioButton(
selected = false,
onClick = {},
modifier = Modifier
.testTag(TestIds.Book.ITEM_PLACE_SELECTOR)
// .selectable(
// selected = false,
// onClick = {}
// )
)
Text(
text = booking.place,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.testTag(TestIds.Book.ITEM_PLACE_TEXT)
)
}
HorizontalDivider(
color = MaterialTheme.colorScheme.outlineVariant,
thickness = 1.dp,
modifier = Modifier.padding(start = 16.dp)
)
}
data class Booking(val id: Int, val place: String)
typealias BookingsByDate = Map<String, List<Booking>>
val bookingsByDate: BookingsByDate = mapOf(
"2025-01-05" to listOf(Booking(1, "102"), Booking(2, "209.13")),
"2025-01-06" to listOf(Booking(3, "Зона 51. 50")),
"2025-01-07" to listOf(Booking(1, "102"), Booking(2, "209.13")),
"2025-01-08" to listOf(Booking(2, "209.13"))
)

View File

@@ -0,0 +1,8 @@
package ru.myitschool.work.ui.screen.book
sealed interface BookState {
data object Loading: BookState
data object DataPresent: BookState
data object DataAbsent: BookState
data object Error: BookState
}

View File

@@ -0,0 +1,27 @@
package ru.myitschool.work.ui.screen.book
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
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.launch
class BookViewModel(): ViewModel() {
private val _uiState = MutableStateFlow<BookState>(BookState.DataPresent)
val uiState: StateFlow<BookState> = _uiState.asStateFlow()
private val _actionFlow: MutableSharedFlow<Unit> = MutableSharedFlow()
val actionFlow: SharedFlow<Unit> = _actionFlow
fun onIntent(intent: BookIntent) {
when(intent) {
is BookIntent.Fetch -> Unit
is BookIntent.Book -> Unit
is BookIntent.GoBack -> viewModelScope.launch {
_actionFlow.emit(Unit)
}
}
}
}

View File

@@ -3,4 +3,5 @@ package ru.myitschool.work.ui.screen.main
sealed interface MainIntent {
data object Fetch: MainIntent
data object Logout: MainIntent
data object NewBooking: MainIntent
}

View File

@@ -23,6 +23,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.Logout
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.BookmarkBorder
import androidx.compose.material.icons.filled.Bookmarks
import androidx.compose.material.icons.filled.ChevronRight
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.CircularProgressIndicator
@@ -49,6 +50,8 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import coil3.compose.rememberAsyncImagePainter
import ru.myitschool.work.core.TestIds
import ru.myitschool.work.ui.nav.BookScreenDestination
import ru.myitschool.work.ui.nav.MainScreenDestination
@Composable
fun MainScreen(
@@ -82,7 +85,26 @@ fun MainScreen(
when (val currentState = state) {
is MainState.Error -> {
Text("TEST_ERROR", modifier = Modifier.testTag(TestIds.Main.ERROR))
Text(
text = "TEST_ERROR",
modifier = Modifier.testTag(TestIds.Main.ERROR),
color = MaterialTheme.colorScheme.error
)
IconButton(
onClick = { viewModel.onIntent(MainIntent.Fetch) },
modifier = Modifier
.size(24.dp)
.aspectRatio(1f)
.testTag(TestIds.Main.REFRESH_BUTTON),
enabled = true,
) {
Icon(
Icons.Default.Refresh,
contentDescription = null,
modifier = Modifier
.fillMaxSize()
)
}
}
is MainState.Loading -> {
@@ -176,7 +198,6 @@ fun MainScreen(
Icon(
imageVector = Icons.Default.BookmarkBorder,
contentDescription = null,
tint = MaterialTheme.colorScheme.secondaryFixed
)
Text(
text = "Бронирования",
@@ -213,7 +234,7 @@ fun MainScreen(
}
FloatingActionButton(
onClick = { },
onClick = { viewModel.onIntent(MainIntent.NewBooking) },
containerColor = MaterialTheme.colorScheme.secondaryContainer,
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
modifier = Modifier
@@ -223,7 +244,6 @@ fun MainScreen(
) {
Icon(Icons.Default.Add, contentDescription = "Добавить")
}
}
}
}
@@ -231,6 +251,9 @@ fun MainScreen(
LaunchedEffect(Unit) {
viewModel.onIntent(MainIntent.Fetch)
viewModel.actionFlow.collect {
navController.navigate(BookScreenDestination)
}
}
}

View File

@@ -2,7 +2,9 @@ package ru.myitschool.work.ui.screen.main
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
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.launch
@@ -13,6 +15,8 @@ class MainViewModel(): ViewModel() {
private val logout by lazy { Logout(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
fun onIntent(intent: MainIntent) {
when (intent) {
@@ -22,6 +26,11 @@ class MainViewModel(): ViewModel() {
logout.invoke()
}
}
is MainIntent.NewBooking -> {
viewModelScope.launch {
_actionFlow.emit(Unit)
}
}
}
}
}