release #13

Open
student-33689 wants to merge 4 commits from student-33689/NTO-2025-Android-TeamTask:main into main
12 changed files with 462 additions and 15 deletions

View File

@@ -8,7 +8,9 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.lifecycle.viewmodel.compose.viewModel
import ru.myitschool.work.ui.screen.AppNavHost import ru.myitschool.work.ui.screen.AppNavHost
import ru.myitschool.work.ui.screen.auth.AuthViewModel
import ru.myitschool.work.ui.theme.WorkTheme import ru.myitschool.work.ui.theme.WorkTheme
class RootActivity : ComponentActivity() { class RootActivity : ComponentActivity() {
@@ -18,7 +20,9 @@ class RootActivity : ComponentActivity() {
setContent { setContent {
WorkTheme { WorkTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
val viewModel: AuthViewModel = viewModel()
AppNavHost( AppNavHost(
viewModel,
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(innerPadding) .padding(innerPadding)

View File

@@ -1,26 +1,63 @@
package ru.myitschool.work.ui.screen package ru.myitschool.work.ui.screen
import androidx.compose.animation.AnimatedContentScope
import androidx.compose.animation.EnterTransition import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition import androidx.compose.animation.ExitTransition
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.text.input.TextFieldLineLimits
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.material3.Button
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTag
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import ru.myitschool.work.core.TestIds
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
import ru.myitschool.work.ui.nav.MainScreenDestination import ru.myitschool.work.ui.nav.MainScreenDestination
import ru.myitschool.work.ui.screen.auth.AuthIntent
import ru.myitschool.work.ui.screen.auth.AuthScreen import ru.myitschool.work.ui.screen.auth.AuthScreen
import ru.myitschool.work.ui.screen.auth.AuthViewModel
import ru.myitschool.work.ui.screen.book.BookScreen
import ru.myitschool.work.ui.screen.main.MainScreen
@Composable @Composable
fun AppNavHost( fun AppNavHost(
viewModel: AuthViewModel,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
navController: NavHostController = rememberNavController() navController: NavHostController = rememberNavController(),
) {
) {
NavHost( NavHost(
modifier = modifier, modifier = modifier,
enterTransition = { EnterTransition.None }, enterTransition = { EnterTransition.None },
@@ -32,18 +69,13 @@ fun AppNavHost(
AuthScreen(navController = navController) AuthScreen(navController = navController)
} }
composable<MainScreenDestination> { composable<MainScreenDestination> {
Box( MainScreen(navController = navController)
contentAlignment = Alignment.Center
) {
Text(text = "Hello")
}
} }
composable<BookScreenDestination> { composable<BookScreenDestination> {
Box( BookScreen(navController = navController)
contentAlignment = Alignment.Center
) {
Text(text = "Hello")
}
} }
} }
} }

View File

@@ -76,7 +76,9 @@ private fun Content(
var inputText by remember { mutableStateOf("") } var inputText by remember { mutableStateOf("") }
Spacer(modifier = Modifier.size(16.dp)) Spacer(modifier = Modifier.size(16.dp))
TextField( TextField(
modifier = Modifier.testTag(TestIds.Auth.CODE_INPUT).fillMaxWidth(), modifier = Modifier
.testTag(TestIds.Auth.CODE_INPUT)
.fillMaxWidth(),
value = inputText, value = inputText,
onValueChange = { onValueChange = {
inputText = it inputText = it
@@ -86,7 +88,9 @@ private fun Content(
) )
Spacer(modifier = Modifier.size(16.dp)) Spacer(modifier = Modifier.size(16.dp))
Button( Button(
modifier = Modifier.testTag(TestIds.Auth.SIGN_BUTTON).fillMaxWidth(), modifier = Modifier
.testTag(TestIds.Auth.SIGN_BUTTON)
.fillMaxWidth(),
onClick = { onClick = {
viewModel.onIntent(AuthIntent.Send(inputText)) viewModel.onIntent(AuthIntent.Send(inputText))
}, },

View File

@@ -0,0 +1,7 @@
package ru.myitschool.work.ui.screen.book
interface BookIntent {
data class Send(val text: String): BookIntent
data class TextInput(val text: String): BookIntent
}

View File

@@ -0,0 +1,132 @@
package ru.myitschool.work.ui.screen.book
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.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.selection.selectable
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.OutlinedButton
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.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTag
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import ru.myitschool.work.ui.nav.MainScreenDestination
@Composable
fun BookScreen(
viewModel: BookViewModel = viewModel(),
navController: NavController
) {
val state by viewModel.uiState.collectAsState()
LaunchedEffect(Unit) {
viewModel.actionFlow.collect {
navController.navigate(MainScreenDestination)
}
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(all = 24.dp),
) {
when (val currentState = state) {
is BookState.Data -> Content(viewModel, currentState)
is BookState.Loading -> {
CircularProgressIndicator(
modifier = Modifier.size(64.dp)
)
}
}
}
}
@Composable
private fun Content(
viewModel: BookViewModel,
state: BookState.Data
) {
val error = false
var selected = false
var input by remember { mutableStateOf("") }
Column() { Row (horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth().padding(8.dp)) {
OutlinedButton(onClick = { }) {
Text("19.04", Modifier.testTag("main_item_place"))
Modifier.semantics { testTag = "main_add_button"}.selectable(selected = selected, onClick = {selected = true }, role = Role.Button)
}
OutlinedButton(onClick = { }) {
Text("20.04", Modifier.testTag("main_item_place"))
Modifier.semantics { testTag = "main_add_button"}.selectable(selected = selected, onClick = {selected = true }, role = Role.Button)
}
OutlinedButton(onClick = { }) {
Text("21.04", Modifier.testTag("main_item_place"))
Modifier.semantics { testTag = "main_add_button"}.selectable(selected = selected, onClick = {selected = true }, role = Role.Button)
}
OutlinedButton(onClick = { }) {
Text("22.04", Modifier.testTag("main_item_place"))
Modifier.semantics { testTag = "main_add_button" }.selectable(selected = selected, onClick = {selected = true }, role = Role.Button)
}
}
Spacer(modifier = Modifier.size(20.dp))
Text("Рабочее место у окна")
Spacer(modifier = Modifier.size(20.dp))
Text("Переговорная комната №1")
Spacer(modifier = Modifier.size(20.dp))
Text("Коворкинг A", Modifier.selectable(selected = selected, onClick = {selected = true}, role = Role.Button))
Spacer(modifier = Modifier.size(20.dp))
Row (horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth().padding(8.dp)){
Button(onClick = { viewModel.onIntent(BookIntent.Send(input)) }) {
Text(text = "Назад")
Modifier
.padding(20.dp)
.fillMaxWidth()
.semantics { testTag = "book_back_button" }
}
Button(onClick = { }) {
Text(text = "Бронировать")
Modifier
.padding(20.dp)
.fillMaxWidth()
.semantics { testTag = "book_book_button" }
}
}
if (error){
Spacer(modifier = Modifier.size(20.dp))
Text("Ошибка", Modifier.testTag("book_error"))
Button(onClick = { }) {
Text(text = "Обновить")
Modifier
.padding(20.dp)
.fillMaxWidth()
.semantics { testTag = "book_refresh_button" }
}
Text("Всё забронировано", Modifier.testTag("book_empty"))
}
}
}

View File

@@ -0,0 +1,8 @@
package ru.myitschool.work.ui.screen.book
import ru.myitschool.work.ui.screen.auth.AuthState
interface BookState {
object Loading: BookState
object Data: BookState
}

View File

@@ -0,0 +1,43 @@
package ru.myitschool.work.ui.screen.book
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
class BookViewModel: ViewModel() {
private val checkAndSaveAuthCodeUseCase by lazy { CheckAndSaveAuthCodeUseCase(AuthRepository) }
private val _uiState = MutableStateFlow<BookState>(BookState.Data)
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.Send -> {
viewModelScope.launch(Dispatchers.Default) {
_uiState.update { BookState.Loading }
checkAndSaveAuthCodeUseCase.invoke("9999").fold(
onSuccess = {
_actionFlow.emit(Unit)
},
onFailure = { error ->
error.printStackTrace()
_actionFlow.emit(Unit)
}
)
}
}
is BookIntent.TextInput -> Unit
}
}
}

View File

@@ -0,0 +1,4 @@
package ru.myitschool.work.ui.screen.book
interface MainState {
}

View File

@@ -0,0 +1,7 @@
package ru.myitschool.work.ui.screen.main
interface MainIntent {
data class Send(val text: String): MainIntent
data class TextInput(val text: String): MainIntent
}

View File

@@ -0,0 +1,154 @@
package ru.myitschool.work.ui.screen.main
import androidx.compose.foundation.Image
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.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
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.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Color.Companion.Gray
import androidx.compose.ui.graphics.painter.ColorPainter
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTag
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import ru.myitschool.work.R
import ru.myitschool.work.core.TestIds
import ru.myitschool.work.ui.nav.BookScreenDestination
import ru.myitschool.work.ui.nav.MainScreenDestination
import ru.myitschool.work.ui.screen.auth.AuthIntent
import ru.myitschool.work.ui.screen.auth.AuthState
import ru.myitschool.work.ui.screen.auth.AuthViewModel
import ru.myitschool.work.ui.screen.book.BookIntent
@Composable
fun MainScreen(
viewModel: MainViewModel = viewModel(),
navController: NavController
) {
val state by viewModel.uiState.collectAsState()
LaunchedEffect(Unit) {
viewModel.actionFlow.collect {
navController.navigate(BookScreenDestination)
}
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(all = 24.dp)
) {
when (val currentState = state) {
is MainState.Data -> Content(viewModel, currentState)
is MainState.Loading -> {
CircularProgressIndicator(
modifier = Modifier.size(64.dp)
)
}
}
}
}
@Composable
private fun Content(
viewModel: MainViewModel,
state: MainState.Data
) {
var expanded by remember{ mutableStateOf(false)}
var inputUsername by remember { mutableStateOf("Введите имя") }
var input by remember { mutableStateOf("") }
val date:String = "dd.MM.yyyy"
val username:String = "test"
var place:String = "test"
val error = false
Spacer(modifier = Modifier.size(16.dp))
Column() {
Spacer(modifier = Modifier.size(16.dp))
Text(
modifier = Modifier.testTag("main_name").fillMaxWidth(),
text = username
)
Spacer(modifier = Modifier.size(10.dp))
Image(painter = ColorPainter(Color.DarkGray), contentDescription = "main_photo", modifier = Modifier.size(50.dp))
Spacer(modifier = Modifier.size(10.dp))
Row (horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth().padding(8.dp)) {
Button(onClick = { }) {
Text(text = "Выход")
Modifier
.padding(20.dp)
.fillMaxWidth()
.semantics { testTag = "main_logout_button" }
}
Spacer(modifier = Modifier.size(10.dp))
Button(onClick = { }) {
Text(text = "Обновить")
Modifier
.padding(20.dp)
.fillMaxWidth()
.semantics { testTag = "main_refresh_button" }
}
Spacer(modifier = Modifier.size(10.dp))
Button(onClick = {
viewModel.onIntent(MainIntent.Send(input))
}) {
Text(text = "Бронировать")
Modifier
.padding(20.dp)
.fillMaxWidth()
.semantics { testTag = "main_add_button" }
}
Spacer(modifier = Modifier.size(10.dp))
}
Spacer(modifier = Modifier.size(10.dp))
Text(
modifier = Modifier.testTag("main_item_date").fillMaxWidth(),
text = date
)
Spacer(modifier = Modifier.size(10.dp))
Text(
modifier = Modifier.testTag("main_item_place").fillMaxWidth(),
text = place
)
Text(text = "ERROR",
modifier = Modifier.testTag("main_error").fillMaxWidth().alpha(0f))
}
var inputText by remember { mutableStateOf("") }
Spacer(modifier = Modifier.size(16.dp))
}

View File

@@ -0,0 +1,9 @@
package ru.myitschool.work.ui.screen.main
import ru.myitschool.work.ui.screen.auth.AuthState
interface MainState {
object Loading: MainState
object Data: MainState
}

View File

@@ -0,0 +1,43 @@
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
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
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
}
}
}