First request #5
@@ -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,6 @@ 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("androidx.datastore:datastore-preferences:1.2.0")
|
||||
implementation("androidx.compose.material:material-icons-extended:1.7.8")
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ object TestIds {
|
||||
const val SIGN_BUTTON = "auth_sign_button"
|
||||
const val CODE_INPUT = "auth_code_input"
|
||||
}
|
||||
|
||||
object Main {
|
||||
const val ERROR = "main_error"
|
||||
const val ADD_BUTTON = "main_add_button"
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package ru.myitschool.work.data.models
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class BookBody (
|
||||
val date: String,
|
||||
val placeId: Int
|
||||
)
|
||||
@@ -0,0 +1,9 @@
|
||||
package ru.myitschool.work.data.models
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Booking(
|
||||
val id: Int,
|
||||
val place: String
|
||||
)
|
||||
@@ -0,0 +1,3 @@
|
||||
package ru.myitschool.work.data.models
|
||||
|
||||
typealias BookingInfo = Map<String, List<Booking>>
|
||||
10
app/src/main/java/ru/myitschool/work/data/models/UserInfo.kt
Normal file
10
app/src/main/java/ru/myitschool/work/data/models/UserInfo.kt
Normal 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, Booking>
|
||||
)
|
||||
@@ -1,15 +1,21 @@
|
||||
package ru.myitschool.work.data.repo
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import ru.myitschool.work.data.source.LocalDataSource
|
||||
import ru.myitschool.work.data.source.NetworkDataSource
|
||||
|
||||
object AuthRepository {
|
||||
suspend fun clearCode() {
|
||||
LocalDataSource.setCode("")
|
||||
}
|
||||
suspend fun getCode(): String {
|
||||
return LocalDataSource.getCode()
|
||||
}
|
||||
|
||||
private var codeCache: String? = null
|
||||
|
||||
val isCodePresentFlow: Flow<Boolean> = LocalDataSource.isCodePresentFlow
|
||||
suspend fun checkAndSave(text: String): Result<Boolean> {
|
||||
return NetworkDataSource.checkAuth(text).onSuccess { success ->
|
||||
if (success) {
|
||||
codeCache = text
|
||||
LocalDataSource.setCode(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package ru.myitschool.work.data.repo
|
||||
|
||||
import ru.myitschool.work.data.models.BookingInfo
|
||||
import ru.myitschool.work.data.repo.AuthRepository.getCode
|
||||
import ru.myitschool.work.data.source.NetworkDataSource
|
||||
|
||||
object BookRepository {
|
||||
suspend fun fetch(): Result<BookingInfo> {
|
||||
return NetworkDataSource.bookInfo(getCode()).onSuccess { data -> data }
|
||||
}
|
||||
|
||||
suspend fun book(date: String, placeId: Int): Result<Boolean> {
|
||||
return NetworkDataSource.book(getCode(), date, placeId).onSuccess { success -> success }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package ru.myitschool.work.data.repo
|
||||
|
||||
import ru.myitschool.work.data.models.UserInfo
|
||||
import ru.myitschool.work.data.repo.AuthRepository.getCode
|
||||
import ru.myitschool.work.data.source.NetworkDataSource
|
||||
|
||||
object MainRepository {
|
||||
suspend fun fetch(): Result<UserInfo> {
|
||||
return NetworkDataSource.info(getCode()).onSuccess { data -> data }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package ru.myitschool.work.data.source
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import ru.myitschool.work.App
|
||||
|
||||
object LocalDataSource {
|
||||
private val Context.dataStore by preferencesDataStore("user_data")
|
||||
|
||||
object Keys {
|
||||
val CODE = stringPreferencesKey("Username")
|
||||
}
|
||||
|
||||
private val appContext get() = App.context
|
||||
|
||||
suspend fun getCode(): String {
|
||||
return appContext.dataStore.data.map { it[Keys.CODE] ?: "" }.first()
|
||||
}
|
||||
|
||||
suspend fun setCode(code: String) {
|
||||
appContext.dataStore.edit { it[Keys.CODE] = code }
|
||||
}
|
||||
|
||||
val isCodePresentFlow: Flow<Boolean> = appContext.dataStore.data.map { it[Keys.CODE] != "" && it[Keys.CODE] != null }
|
||||
}
|
||||
@@ -1,16 +1,24 @@
|
||||
package ru.myitschool.work.data.source
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.engine.cio.CIO
|
||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.request.post
|
||||
import io.ktor.client.request.setBody
|
||||
import io.ktor.client.statement.bodyAsText
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.http.contentType
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
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.BookBody
|
||||
import ru.myitschool.work.data.models.BookingInfo
|
||||
import ru.myitschool.work.data.models.UserInfo
|
||||
|
||||
object NetworkDataSource {
|
||||
private val client by lazy {
|
||||
@@ -38,5 +46,39 @@ object NetworkDataSource {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun info(code: String): Result<UserInfo> = withContext(Dispatchers.IO) {
|
||||
return@withContext runCatching {
|
||||
val response = client.get(getUrl(code, Constants.INFO_URL))
|
||||
when (response.status) {
|
||||
HttpStatusCode.OK -> Json.decodeFromString<UserInfo>(response.body())
|
||||
else -> error(response.bodyAsText())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun bookInfo(code: String): Result<BookingInfo> = withContext(Dispatchers.IO) {
|
||||
return@withContext runCatching {
|
||||
val response = client.get(getUrl(code, Constants.BOOKING_URL))
|
||||
when (response.status) {
|
||||
HttpStatusCode.OK -> Json.decodeFromString<BookingInfo>(response.body())
|
||||
else -> error(response.bodyAsText())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun book(code: String, date: String, placeId: Int): Result<Boolean> = withContext(Dispatchers.IO) {
|
||||
return@withContext runCatching {
|
||||
// val requestBodyString = Json.encodeToString(BookBody(date, placeId))
|
||||
val response = client.post((getUrl(code, Constants.BOOK_URL))) {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(BookBody(date, placeId))
|
||||
}
|
||||
when(response.status) {
|
||||
HttpStatusCode.Created -> true
|
||||
else -> error(response.bodyAsText())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getUrl(code: String, targetUrl: String) = "${Constants.HOST}/api/$code$targetUrl"
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package ru.myitschool.work.domain.auth
|
||||
|
||||
import ru.myitschool.work.data.repo.AuthRepository
|
||||
import ru.myitschool.work.App
|
||||
|
||||
class CheckAndSaveAuthCodeUseCase(
|
||||
private val repository: AuthRepository
|
||||
|
||||
16
app/src/main/java/ru/myitschool/work/domain/book/Book.kt
Normal file
16
app/src/main/java/ru/myitschool/work/domain/book/Book.kt
Normal file
@@ -0,0 +1,16 @@
|
||||
package ru.myitschool.work.domain.book
|
||||
|
||||
import ru.myitschool.work.data.repo.BookRepository
|
||||
|
||||
class Book (
|
||||
private val repository: BookRepository
|
||||
) {
|
||||
suspend operator fun invoke(
|
||||
date: String,
|
||||
placeId: Int
|
||||
): Result<Unit> {
|
||||
return repository.book(date, placeId).mapCatching { success ->
|
||||
if (!success) error("Code is incorrect")
|
||||
}
|
||||
}
|
||||
}
|
||||
12
app/src/main/java/ru/myitschool/work/domain/book/Fetch.kt
Normal file
12
app/src/main/java/ru/myitschool/work/domain/book/Fetch.kt
Normal file
@@ -0,0 +1,12 @@
|
||||
package ru.myitschool.work.domain.book
|
||||
|
||||
import ru.myitschool.work.data.models.BookingInfo
|
||||
import ru.myitschool.work.data.repo.BookRepository
|
||||
|
||||
class Fetch (
|
||||
private val repository: BookRepository
|
||||
) {
|
||||
suspend operator fun invoke(): Result<BookingInfo> {
|
||||
return repository.fetch().mapCatching { success -> success.filter { it.value.isNotEmpty() } as BookingInfo }
|
||||
}
|
||||
}
|
||||
12
app/src/main/java/ru/myitschool/work/domain/main/Fetch.kt
Normal file
12
app/src/main/java/ru/myitschool/work/domain/main/Fetch.kt
Normal file
@@ -0,0 +1,12 @@
|
||||
package ru.myitschool.work.domain.main
|
||||
|
||||
import ru.myitschool.work.data.models.UserInfo
|
||||
import ru.myitschool.work.data.repo.MainRepository
|
||||
|
||||
class Fetch(
|
||||
private val repository: MainRepository
|
||||
) {
|
||||
suspend operator fun invoke(): Result<UserInfo> {
|
||||
return repository.fetch().mapCatching { success -> success }
|
||||
}
|
||||
}
|
||||
12
app/src/main/java/ru/myitschool/work/domain/main/Logout.kt
Normal file
12
app/src/main/java/ru/myitschool/work/domain/main/Logout.kt
Normal file
@@ -0,0 +1,12 @@
|
||||
package ru.myitschool.work.domain.main
|
||||
|
||||
import ru.myitschool.work.data.repo.AuthRepository
|
||||
import kotlin.mapCatching
|
||||
|
||||
class Logout (
|
||||
private val repository: AuthRepository
|
||||
) {
|
||||
suspend operator fun invoke(): Unit {
|
||||
return repository.clearCode()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package ru.myitschool.work.ui.components
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
@Composable
|
||||
fun Modifier.conditionalImePadding(): Modifier {
|
||||
return if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
|
||||
this.then(Modifier.imePadding())
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package ru.myitschool.work.ui.nav
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data object SplashScreenDestination: AppDestination
|
||||
@@ -7,18 +7,28 @@ import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import ru.myitschool.work.data.repo.AuthRepository
|
||||
import ru.myitschool.work.ui.screen.AppNavHost
|
||||
import ru.myitschool.work.ui.theme.WorkTheme
|
||||
|
||||
class RootActivity : ComponentActivity() {
|
||||
class RootActivity() : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
actionBar?.hide()
|
||||
setContent {
|
||||
WorkTheme {
|
||||
val codePresence by AuthRepository.isCodePresentFlow.collectAsState(initial = null)
|
||||
|
||||
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
|
||||
AppNavHost(
|
||||
codePresence = codePresence,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(innerPadding)
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package ru.myitschool.work.ui.root
|
||||
|
||||
sealed interface RootState {
|
||||
object Loading: RootState
|
||||
|
||||
object CodePresent: RootState
|
||||
|
||||
object CodeAbsent: RootState
|
||||
}
|
||||
@@ -5,45 +5,56 @@ import androidx.compose.animation.ExitTransition
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import ru.myitschool.work.data.repo.AuthRepository
|
||||
import ru.myitschool.work.ui.nav.AuthScreenDestination
|
||||
import ru.myitschool.work.ui.nav.BookScreenDestination
|
||||
import ru.myitschool.work.ui.nav.MainScreenDestination
|
||||
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
|
||||
|
||||
@Composable
|
||||
fun AppNavHost(
|
||||
modifier: Modifier = Modifier,
|
||||
navController: NavHostController = rememberNavController()
|
||||
navController: NavHostController = rememberNavController(),
|
||||
codePresence: Boolean?
|
||||
) {
|
||||
val startDestination = if (codePresence == null) SplashScreenDestination
|
||||
else if (codePresence) MainScreenDestination
|
||||
else AuthScreenDestination
|
||||
|
||||
NavHost(
|
||||
modifier = modifier,
|
||||
enterTransition = { EnterTransition.None },
|
||||
exitTransition = { ExitTransition.None },
|
||||
// enterTransition = { EnterTransition.None },
|
||||
// exitTransition = { ExitTransition.None },
|
||||
navController = navController,
|
||||
startDestination = AuthScreenDestination,
|
||||
startDestination = startDestination,
|
||||
) {
|
||||
composable<AuthScreenDestination> {
|
||||
AuthScreen(navController = navController)
|
||||
}
|
||||
composable<SplashScreenDestination> {
|
||||
SplashScreen()
|
||||
}
|
||||
composable<MainScreenDestination> {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(text = "Hello")
|
||||
}
|
||||
MainScreen(navController = navController)
|
||||
}
|
||||
composable<BookScreenDestination> {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(text = "Hello")
|
||||
}
|
||||
BookScreen(navController = navController)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,22 @@
|
||||
package ru.myitschool.work.ui.screen.auth
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
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.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Surface
|
||||
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
|
||||
@@ -30,6 +35,7 @@ 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.components.conditionalImePadding
|
||||
import ru.myitschool.work.ui.nav.MainScreenDestination
|
||||
|
||||
@Composable
|
||||
@@ -48,22 +54,18 @@ fun AuthScreen(
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(all = 24.dp),
|
||||
.padding(all = 32.dp)
|
||||
.conditionalImePadding(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.auth_title),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
when (val currentState = state) {
|
||||
is AuthState.Data -> Content(viewModel, currentState)
|
||||
is AuthState.Loading -> {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(64.dp)
|
||||
)
|
||||
}
|
||||
else -> Content(viewModel, currentState)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -71,27 +73,53 @@ fun AuthScreen(
|
||||
@Composable
|
||||
private fun Content(
|
||||
viewModel: AuthViewModel,
|
||||
state: AuthState.Data
|
||||
state: AuthState,
|
||||
) {
|
||||
var inputText by remember { mutableStateOf("") }
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
TextField(
|
||||
val err by viewModel.errorFlow.collectAsState()
|
||||
val isButtonEnabled by viewModel.isButtonEnabled.collectAsState()
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.auth_title),
|
||||
style = MaterialTheme.typography.headlineLarge,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
Spacer(modifier = Modifier.size(48.dp))
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.testTag(TestIds.Auth.CODE_INPUT).fillMaxWidth(),
|
||||
value = inputText,
|
||||
onValueChange = {
|
||||
inputText = it
|
||||
viewModel.onIntent(AuthIntent.TextInput(it))
|
||||
},
|
||||
label = { Text(stringResource(R.string.auth_label)) }
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
label = { Text(stringResource(R.string.auth_label)) },
|
||||
placeholder = { Text(stringResource(R.string.auth_label)) }
|
||||
|
||||
)
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
Spacer(modifier = Modifier.size(24.dp))
|
||||
if (state == AuthState.Error) {
|
||||
Text(
|
||||
text = err,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.testTag(TestIds.Auth.ERROR)
|
||||
)
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
}
|
||||
Button(
|
||||
modifier = Modifier.testTag(TestIds.Auth.SIGN_BUTTON).fillMaxWidth(),
|
||||
modifier = Modifier
|
||||
.testTag(TestIds.Auth.SIGN_BUTTON)
|
||||
.fillMaxWidth()
|
||||
.height(56.dp),
|
||||
onClick = {
|
||||
viewModel.onIntent(AuthIntent.Send(inputText))
|
||||
},
|
||||
enabled = true
|
||||
shape = MaterialTheme.shapes.large,
|
||||
enabled = isButtonEnabled
|
||||
) {
|
||||
Text(stringResource(R.string.auth_sign_in))
|
||||
Text(
|
||||
text = stringResource(R.string.auth_sign_in),
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -3,4 +3,5 @@ package ru.myitschool.work.ui.screen.auth
|
||||
sealed interface AuthState {
|
||||
object Loading: AuthState
|
||||
object Data: AuthState
|
||||
object Error: AuthState
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package ru.myitschool.work.ui.screen.auth
|
||||
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -13,31 +14,45 @@ import kotlinx.coroutines.launch
|
||||
import ru.myitschool.work.data.repo.AuthRepository
|
||||
import ru.myitschool.work.domain.auth.CheckAndSaveAuthCodeUseCase
|
||||
|
||||
class AuthViewModel : ViewModel() {
|
||||
class AuthViewModel() : ViewModel() {
|
||||
private val checkAndSaveAuthCodeUseCase by lazy { CheckAndSaveAuthCodeUseCase(AuthRepository) }
|
||||
private val _uiState = MutableStateFlow<AuthState>(AuthState.Data)
|
||||
val uiState: StateFlow<AuthState> = _uiState.asStateFlow()
|
||||
|
||||
private val _errorFlow = MutableStateFlow<String>("")
|
||||
val errorFlow: StateFlow<String> = _errorFlow.asStateFlow()
|
||||
|
||||
private val _isButtonEnabled = MutableStateFlow<Boolean>(false)
|
||||
val isButtonEnabled: StateFlow<Boolean> = _isButtonEnabled.asStateFlow()
|
||||
|
||||
private val _actionFlow: MutableSharedFlow<Unit> = MutableSharedFlow()
|
||||
val actionFlow: SharedFlow<Unit> = _actionFlow
|
||||
|
||||
fun onIntent(intent: AuthIntent) {
|
||||
when (intent) {
|
||||
is AuthIntent.Send -> {
|
||||
onIntent(AuthIntent.TextInput(""))
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
_uiState.update { AuthState.Loading }
|
||||
checkAndSaveAuthCodeUseCase.invoke("9999").fold(
|
||||
checkAndSaveAuthCodeUseCase.invoke(intent.text).fold(
|
||||
onSuccess = {
|
||||
_actionFlow.emit(Unit)
|
||||
},
|
||||
onFailure = { error ->
|
||||
error.printStackTrace()
|
||||
_actionFlow.emit(Unit)
|
||||
_errorFlow.update { error.message.toString() }
|
||||
_uiState.update { AuthState.Error }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
is AuthIntent.TextInput -> Unit
|
||||
|
||||
is AuthIntent.TextInput -> {
|
||||
if (_uiState.value == AuthState.Error) _uiState.update { AuthState.Data }
|
||||
if (intent.text.matches("[a-zA-Z0-9]{4}".toRegex())) {
|
||||
_isButtonEnabled.update { true }
|
||||
} else _isButtonEnabled.update { false }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package ru.myitschool.work.ui.screen.book
|
||||
|
||||
sealed interface BookIntent {
|
||||
data object Fetch: BookIntent
|
||||
data class Book(val date: String, val placeId: Int): BookIntent
|
||||
data object GoBack: BookIntent
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
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.data.models.Booking
|
||||
import ru.myitschool.work.ui.nav.MainScreenDestination
|
||||
import java.time.LocalDate
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
@Composable
|
||||
fun BookScreen(
|
||||
navController: NavController,
|
||||
viewModel: BookViewModel = viewModel()
|
||||
) {
|
||||
val state by viewModel.uiState.collectAsState()
|
||||
val info by viewModel.infoFlow.collectAsState()
|
||||
val err by viewModel.errorFlow.collectAsState()
|
||||
var selectedTabIndex by remember { mutableStateOf(0) }
|
||||
var selectedPlaceIndex 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 = err,
|
||||
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 -> {
|
||||
val entriesList = info!!.entries.toList()
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
|
||||
PrimaryScrollableTabRow(
|
||||
selectedTabIndex = selectedTabIndex,
|
||||
edgePadding = 16.dp,
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
contentColor = MaterialTheme.colorScheme.primary,
|
||||
) {
|
||||
entriesList.forEachIndexed { index, entry ->
|
||||
Tab(
|
||||
selected = selectedTabIndex == index,
|
||||
onClick = {
|
||||
selectedTabIndex = index
|
||||
selectedPlaceIndex = 0
|
||||
},
|
||||
text = {
|
||||
Text(
|
||||
text = LocalDate.parse(entry.key).format(DateTimeFormatter.ofPattern("dd.MM")),
|
||||
modifier = Modifier.testTag(TestIds.Book.ITEM_DATE)
|
||||
)
|
||||
},
|
||||
modifier = Modifier.testTag(TestIds.Book.getIdDateItemByPosition(index))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
itemsIndexed(entriesList[selectedTabIndex].value) { index, booking ->
|
||||
Booking(
|
||||
booking = booking,
|
||||
index = index,
|
||||
selected = selectedPlaceIndex,
|
||||
onRadioChange = { selectedPlaceIndex = index }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ExtendedFloatingActionButton(
|
||||
onClick = { viewModel.onIntent(BookIntent.Book(
|
||||
date = entriesList[selectedTabIndex].key,
|
||||
placeId = entriesList[selectedTabIndex].value[selectedPlaceIndex].id
|
||||
)) },
|
||||
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.onIntent(BookIntent.Fetch)
|
||||
viewModel.actionFlow.collect {
|
||||
navController.popBackStack()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Booking(booking: Booking, index: Int, selected: Int, onRadioChange: Function0<Unit>) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
// .clickable { }
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
.testTag(TestIds.Book.getIdPlaceItemByPosition(index)),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
RadioButton(
|
||||
selected = selected == index,
|
||||
onClick = onRadioChange,
|
||||
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)
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package ru.myitschool.work.ui.screen.book
|
||||
|
||||
import android.util.Log
|
||||
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.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import ru.myitschool.work.data.models.BookingInfo
|
||||
import ru.myitschool.work.data.repo.BookRepository
|
||||
import ru.myitschool.work.domain.book.Book
|
||||
import ru.myitschool.work.domain.book.Fetch
|
||||
|
||||
class BookViewModel(): ViewModel() {
|
||||
private val fetch by lazy { Fetch(BookRepository) }
|
||||
private val book by lazy { Book(BookRepository) }
|
||||
private val _uiState = MutableStateFlow<BookState>(BookState.Loading)
|
||||
val uiState: StateFlow<BookState> = _uiState.asStateFlow()
|
||||
private val _actionFlow: MutableSharedFlow<Unit> = MutableSharedFlow()
|
||||
val actionFlow: SharedFlow<Unit> = _actionFlow
|
||||
private val _infoFlow: MutableStateFlow<BookingInfo?> = MutableStateFlow(null)
|
||||
val infoFlow: StateFlow<BookingInfo?> = _infoFlow.asStateFlow()
|
||||
private val _errorFlow = MutableStateFlow<String>("")
|
||||
val errorFlow: StateFlow<String> = _errorFlow.asStateFlow()
|
||||
|
||||
fun onIntent(intent: BookIntent) {
|
||||
when(intent) {
|
||||
is BookIntent.Fetch -> viewModelScope.launch {
|
||||
_uiState.update { BookState.Loading }
|
||||
fetch.invoke().fold(
|
||||
onSuccess = { success ->
|
||||
if (success.isEmpty()) {
|
||||
_uiState.update { BookState.DataAbsent }
|
||||
} else {
|
||||
_infoFlow.update { success }
|
||||
_uiState.update { BookState.DataPresent }
|
||||
}
|
||||
},
|
||||
onFailure = { failure ->
|
||||
Log.d(failure.message, "failure")
|
||||
_uiState.update { BookState.Error }
|
||||
_errorFlow.update { failure.message.toString() }
|
||||
}
|
||||
)
|
||||
}
|
||||
is BookIntent.Book -> viewModelScope.launch {
|
||||
_uiState.update { BookState.Loading }
|
||||
book.invoke(intent.date, intent.placeId).fold(
|
||||
onSuccess = { success ->
|
||||
_actionFlow.emit(Unit)
|
||||
},
|
||||
onFailure = { failure ->
|
||||
Log.d(failure.message, "failure")
|
||||
_uiState.update { BookState.Error }
|
||||
_errorFlow.update { failure.message.toString() }
|
||||
}
|
||||
)
|
||||
}
|
||||
is BookIntent.GoBack -> viewModelScope.launch {
|
||||
_actionFlow.emit(Unit)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package ru.myitschool.work.ui.screen.main
|
||||
|
||||
sealed interface MainIntent {
|
||||
data object Fetch: MainIntent
|
||||
data object Logout: MainIntent
|
||||
data object NewBooking: MainIntent
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
package ru.myitschool.work.ui.screen.main
|
||||
|
||||
import android.graphics.drawable.Icon
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
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.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
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.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
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
|
||||
import androidx.compose.material3.FilledTonalIconButton
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.IconButtonDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
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.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
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 coil3.compose.rememberAsyncImagePainter
|
||||
import ru.myitschool.work.core.TestIds
|
||||
import ru.myitschool.work.data.models.Booking
|
||||
import ru.myitschool.work.ui.nav.BookScreenDestination
|
||||
import ru.myitschool.work.ui.nav.MainScreenDestination
|
||||
import java.time.LocalDate
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
@Composable
|
||||
fun MainScreen(
|
||||
viewModel: MainViewModel = viewModel(),
|
||||
navController: NavController
|
||||
) {
|
||||
val state by viewModel.uiState.collectAsState()
|
||||
val err by viewModel.errorFlow.collectAsState()
|
||||
val info by viewModel.infoFlow.collectAsState()
|
||||
|
||||
when (val currentState = state) {
|
||||
is MainState.Error -> {
|
||||
Column (
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(
|
||||
text = err,
|
||||
modifier = Modifier.testTag(TestIds.Main.ERROR),
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
IconButton(
|
||||
onClick = { viewModel.onIntent(MainIntent.Fetch) },
|
||||
modifier = Modifier
|
||||
.size(32.dp)
|
||||
.aspectRatio(1f)
|
||||
.testTag(TestIds.Main.REFRESH_BUTTON),
|
||||
enabled = true,
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Refresh,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is MainState.Loading -> {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
is MainState.Data -> {
|
||||
info?.let {
|
||||
Column (
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 20.dp),
|
||||
) {
|
||||
FilledTonalIconButton(
|
||||
onClick = { viewModel.onIntent(MainIntent.Logout) },
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.size(40.dp)
|
||||
.aspectRatio(1f)
|
||||
.offset(x = -16.dp)
|
||||
.testTag(TestIds.Main.LOGOUT_BUTTON),
|
||||
enabled = true,
|
||||
shape = MaterialTheme.shapes.extraLarge,
|
||||
colors = IconButtonDefaults.filledTonalIconButtonColors(
|
||||
containerColor = MaterialTheme.colorScheme.errorContainer,
|
||||
contentColor = MaterialTheme.colorScheme.onErrorContainer
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Outlined.Logout,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(20.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.size(120.dp)
|
||||
.aspectRatio(1f)
|
||||
.background(MaterialTheme.colorScheme.inverseOnSurface, CircleShape)
|
||||
) {
|
||||
Image(
|
||||
painter = rememberAsyncImagePainter(info!!.photoUrl),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Fit,
|
||||
modifier = Modifier
|
||||
.size(105.dp)
|
||||
.clip(CircleShape)
|
||||
.testTag(TestIds.Main.PROFILE_IMAGE)
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.size(12.dp))
|
||||
Text(
|
||||
text = info!!.name,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
modifier = Modifier
|
||||
.testTag(TestIds.Main.PROFILE_NAME)
|
||||
)
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.surfaceContainerLow, RoundedCornerShape(topEnd = 24.dp , topStart = 24.dp))
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(vertical = 16.dp, horizontal = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Absolute.SpaceBetween
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.BookmarkBorder,
|
||||
contentDescription = null,
|
||||
)
|
||||
Text(
|
||||
text = "Бронирования",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
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()
|
||||
)
|
||||
}
|
||||
}
|
||||
HorizontalDivider(
|
||||
color = MaterialTheme.colorScheme.outlineVariant,
|
||||
thickness = 1.dp,
|
||||
)
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
itemsIndexed(info!!.booking.entries.toList()) { index, booking ->
|
||||
Booking(booking = booking.value, date = booking.key, index = index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FloatingActionButton(
|
||||
onClick = { viewModel.onIntent(MainIntent.NewBooking) },
|
||||
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.offset(x = -16.dp, y = -16.dp)
|
||||
.testTag(TestIds.Main.ADD_BUTTON)
|
||||
) {
|
||||
Icon(Icons.Default.Add, contentDescription = "Добавить")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.onIntent(MainIntent.Fetch)
|
||||
viewModel.actionFlow.collect {
|
||||
navController.navigate(BookScreenDestination)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Booking(booking: Booking, date: String, index: Int){
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
// .clickable { }
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp)
|
||||
.testTag(TestIds.Main.getIdItemByPosition(index)),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = LocalDate.parse(date).format(DateTimeFormatter.ofPattern("dd.MM.yyyy")),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.testTag(TestIds.Main.ITEM_DATE)
|
||||
)
|
||||
Text(
|
||||
text = booking.place,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.testTag(TestIds.Main.ITEM_PLACE)
|
||||
)
|
||||
}
|
||||
|
||||
Icon(
|
||||
imageVector = Icons.Default.ChevronRight,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
HorizontalDivider(
|
||||
color = MaterialTheme.colorScheme.outlineVariant,
|
||||
thickness = 1.dp,
|
||||
modifier = Modifier.padding(start = 16.dp)
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package ru.myitschool.work.ui.screen.main
|
||||
|
||||
sealed interface MainState {
|
||||
object Loading: MainState
|
||||
|
||||
object Data: MainState
|
||||
|
||||
object Error: MainState
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package ru.myitschool.work.ui.screen.main
|
||||
|
||||
import android.util.Log
|
||||
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.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import ru.myitschool.work.data.models.UserInfo
|
||||
import ru.myitschool.work.data.repo.AuthRepository
|
||||
import ru.myitschool.work.data.repo.MainRepository
|
||||
import ru.myitschool.work.domain.main.Fetch
|
||||
import ru.myitschool.work.domain.main.Logout
|
||||
|
||||
class MainViewModel(): ViewModel() {
|
||||
private val fetch by lazy { Fetch(MainRepository) }
|
||||
private val logout by lazy { Logout(AuthRepository) }
|
||||
private val _uiState = MutableStateFlow<MainState>(MainState.Loading)
|
||||
val uiState: StateFlow<MainState> = _uiState.asStateFlow()
|
||||
private val _actionFlow: MutableSharedFlow<Unit> = MutableSharedFlow()
|
||||
val actionFlow: SharedFlow<Unit> = _actionFlow
|
||||
private val _infoFlow: MutableStateFlow<UserInfo?> = MutableStateFlow(null)
|
||||
val infoFlow: StateFlow<UserInfo?> = _infoFlow.asStateFlow()
|
||||
private val _errorFlow = MutableStateFlow<String>("")
|
||||
val errorFlow: StateFlow<String> = _errorFlow.asStateFlow()
|
||||
|
||||
fun onIntent(intent: MainIntent) {
|
||||
when (intent) {
|
||||
is MainIntent.Fetch -> viewModelScope.launch {
|
||||
_uiState.update { MainState.Loading }
|
||||
fetch.invoke().fold(
|
||||
onSuccess = { success ->
|
||||
_infoFlow.update { success }
|
||||
_uiState.update { MainState.Data }
|
||||
},
|
||||
onFailure = { failure ->
|
||||
Log.d(failure.message, "failure")
|
||||
_uiState.update { MainState.Error }
|
||||
_errorFlow.update { failure.message.toString() }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
is MainIntent.Logout -> {
|
||||
viewModelScope.launch {
|
||||
logout.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
is MainIntent.NewBooking -> {
|
||||
viewModelScope.launch {
|
||||
_actionFlow.emit(Unit)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package ru.myitschool.work.ui.screen.splash
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
@Composable
|
||||
fun SplashScreen() {
|
||||
|
||||
}
|
||||
@@ -52,6 +52,6 @@ fun WorkTheme(
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = Typography,
|
||||
content = content
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<resources>
|
||||
<string name="app_name">Work</string>
|
||||
<string name="title_activity_root">RootActivity</string>
|
||||
<string name="auth_title">Привет! Введи код для авторизации</string>
|
||||
<string name="auth_title">Авторизируйтесь при помощи кода</string>
|
||||
<string name="auth_label">Код</string>
|
||||
<string name="auth_sign_in">Войти</string>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user