diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a5ccda1..53550bd 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,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") } diff --git a/app/src/main/java/ru/myitschool/work/core/TestIds.kt b/app/src/main/java/ru/myitschool/work/core/TestIds.kt index d67b884..748f7b2 100644 --- a/app/src/main/java/ru/myitschool/work/core/TestIds.kt +++ b/app/src/main/java/ru/myitschool/work/core/TestIds.kt @@ -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" diff --git a/app/src/main/java/ru/myitschool/work/data/models/BookBody.kt b/app/src/main/java/ru/myitschool/work/data/models/BookBody.kt new file mode 100644 index 0000000..893b3a3 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/data/models/BookBody.kt @@ -0,0 +1,9 @@ +package ru.myitschool.work.data.models + +import kotlinx.serialization.Serializable + +@Serializable +data class BookBody ( + val date: String, + val placeId: Int +) \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/data/models/Booking.kt b/app/src/main/java/ru/myitschool/work/data/models/Booking.kt new file mode 100644 index 0000000..a71aad5 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/data/models/Booking.kt @@ -0,0 +1,9 @@ +package ru.myitschool.work.data.models + +import kotlinx.serialization.Serializable + +@Serializable +data class Booking( + val id: Int, + val place: String +) \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/data/models/BookingInfo.kt b/app/src/main/java/ru/myitschool/work/data/models/BookingInfo.kt new file mode 100644 index 0000000..d60ffbf --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/data/models/BookingInfo.kt @@ -0,0 +1,3 @@ +package ru.myitschool.work.data.models + +typealias BookingInfo = Map> \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/data/models/UserInfo.kt b/app/src/main/java/ru/myitschool/work/data/models/UserInfo.kt new file mode 100644 index 0000000..7728fc3 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/data/models/UserInfo.kt @@ -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 +) \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/data/repo/AuthRepository.kt b/app/src/main/java/ru/myitschool/work/data/repo/AuthRepository.kt index 3ef28f1..7331433 100644 --- a/app/src/main/java/ru/myitschool/work/data/repo/AuthRepository.kt +++ b/app/src/main/java/ru/myitschool/work/data/repo/AuthRepository.kt @@ -1,16 +1,22 @@ 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 = LocalDataSource.isCodePresentFlow suspend fun checkAndSave(text: String): Result { return NetworkDataSource.checkAuth(text).onSuccess { success -> if (success) { - codeCache = text + LocalDataSource.setCode(text) } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/ru/myitschool/work/data/repo/BookRepository.kt b/app/src/main/java/ru/myitschool/work/data/repo/BookRepository.kt new file mode 100644 index 0000000..0105d29 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/data/repo/BookRepository.kt @@ -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 { + return NetworkDataSource.bookInfo(getCode()).onSuccess { data -> data } + } + + suspend fun book(date: String, placeId: Int): Result { + return NetworkDataSource.book(getCode(), date, placeId).onSuccess { success -> success } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/data/repo/MainRepository.kt b/app/src/main/java/ru/myitschool/work/data/repo/MainRepository.kt new file mode 100644 index 0000000..3d49bfa --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/data/repo/MainRepository.kt @@ -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 { + return NetworkDataSource.info(getCode()).onSuccess { data -> data } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/data/source/LocalDataSource.kt b/app/src/main/java/ru/myitschool/work/data/source/LocalDataSource.kt new file mode 100644 index 0000000..2714061 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/data/source/LocalDataSource.kt @@ -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 = appContext.dataStore.data.map { it[Keys.CODE] != "" && it[Keys.CODE] != null } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt b/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt index fbdfef5..af5ca67 100644 --- a/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt +++ b/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt @@ -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 = withContext(Dispatchers.IO) { + return@withContext runCatching { + val response = client.get(getUrl(code, Constants.INFO_URL)) + when (response.status) { + HttpStatusCode.OK -> Json.decodeFromString(response.body()) + else -> error(response.bodyAsText()) + } + } + } + + suspend fun bookInfo(code: String): Result = withContext(Dispatchers.IO) { + return@withContext runCatching { + val response = client.get(getUrl(code, Constants.BOOKING_URL)) + when (response.status) { + HttpStatusCode.OK -> Json.decodeFromString(response.body()) + else -> error(response.bodyAsText()) + } + } + } + + suspend fun book(code: String, date: String, placeId: Int): Result = 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" } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/domain/auth/CheckAndSaveAuthCodeUseCase.kt b/app/src/main/java/ru/myitschool/work/domain/auth/CheckAndSaveAuthCodeUseCase.kt index 012fb6f..e76a418 100644 --- a/app/src/main/java/ru/myitschool/work/domain/auth/CheckAndSaveAuthCodeUseCase.kt +++ b/app/src/main/java/ru/myitschool/work/domain/auth/CheckAndSaveAuthCodeUseCase.kt @@ -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 diff --git a/app/src/main/java/ru/myitschool/work/domain/book/Book.kt b/app/src/main/java/ru/myitschool/work/domain/book/Book.kt new file mode 100644 index 0000000..3f9c9e3 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/domain/book/Book.kt @@ -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 { + return repository.book(date, placeId).mapCatching { success -> + if (!success) error("Code is incorrect") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/domain/book/Fetch.kt b/app/src/main/java/ru/myitschool/work/domain/book/Fetch.kt new file mode 100644 index 0000000..6a78817 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/domain/book/Fetch.kt @@ -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 { + return repository.fetch().mapCatching { success -> success.filter { it.value.isNotEmpty() } as BookingInfo } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/domain/main/Fetch.kt b/app/src/main/java/ru/myitschool/work/domain/main/Fetch.kt new file mode 100644 index 0000000..b2496d9 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/domain/main/Fetch.kt @@ -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 { + return repository.fetch().mapCatching { success -> success } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/domain/main/Logout.kt b/app/src/main/java/ru/myitschool/work/domain/main/Logout.kt new file mode 100644 index 0000000..af54108 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/domain/main/Logout.kt @@ -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() + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/components/ConditionalImePadding.kt b/app/src/main/java/ru/myitschool/work/ui/components/ConditionalImePadding.kt new file mode 100644 index 0000000..7ce6759 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/components/ConditionalImePadding.kt @@ -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 + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/nav/SplashScreenDestination.kt b/app/src/main/java/ru/myitschool/work/ui/nav/SplashScreenDestination.kt new file mode 100644 index 0000000..4fc41c7 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/nav/SplashScreenDestination.kt @@ -0,0 +1,6 @@ +package ru.myitschool.work.ui.nav + +import kotlinx.serialization.Serializable + +@Serializable +data object SplashScreenDestination: AppDestination \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/root/RootActivity.kt b/app/src/main/java/ru/myitschool/work/ui/root/RootActivity.kt index 54b156d..32539f9 100644 --- a/app/src/main/java/ru/myitschool/work/ui/root/RootActivity.kt +++ b/app/src/main/java/ru/myitschool/work/ui/root/RootActivity.kt @@ -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) diff --git a/app/src/main/java/ru/myitschool/work/ui/root/RootState.kt b/app/src/main/java/ru/myitschool/work/ui/root/RootState.kt new file mode 100644 index 0000000..b1d4dd9 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/root/RootState.kt @@ -0,0 +1,9 @@ +package ru.myitschool.work.ui.root + +sealed interface RootState { + object Loading: RootState + + object CodePresent: RootState + + object CodeAbsent: RootState +} \ No newline at end of file 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 01b0f32..65f3745 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 @@ -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 { AuthScreen(navController = navController) } + composable { + SplashScreen() + } composable { - Box( - contentAlignment = Alignment.Center - ) { - Text(text = "Hello") - } + MainScreen(navController = navController) } composable { - Box( - contentAlignment = Alignment.Center - ) { - Text(text = "Hello") - } + BookScreen(navController = navController) } } } \ No newline at end of file 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 f99978e..99ca6a2 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 @@ -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 + ) } } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthState.kt b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthState.kt index a06ba76..84aa7e3 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthState.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthState.kt @@ -3,4 +3,5 @@ package ru.myitschool.work.ui.screen.auth sealed interface AuthState { object Loading: AuthState object Data: AuthState + object Error: AuthState } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthViewModel.kt b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthViewModel.kt index 3153640..1bbb431 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthViewModel.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthViewModel.kt @@ -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.Data) val uiState: StateFlow = _uiState.asStateFlow() + private val _errorFlow = MutableStateFlow("") + val errorFlow: StateFlow = _errorFlow.asStateFlow() + + private val _isButtonEnabled = MutableStateFlow(false) + val isButtonEnabled: StateFlow = _isButtonEnabled.asStateFlow() + private val _actionFlow: MutableSharedFlow = MutableSharedFlow() val actionFlow: SharedFlow = _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 } + } } } } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookIntent.kt b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookIntent.kt new file mode 100644 index 0000000..1c5be79 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookIntent.kt @@ -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 +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt new file mode 100644 index 0000000..bfc230d --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt @@ -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) { + 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) + ) +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookState.kt b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookState.kt new file mode 100644 index 0000000..8edecf7 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookState.kt @@ -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 +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookViewModel.kt b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookViewModel.kt new file mode 100644 index 0000000..959d4a3 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookViewModel.kt @@ -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.Loading) + val uiState: StateFlow = _uiState.asStateFlow() + private val _actionFlow: MutableSharedFlow = MutableSharedFlow() + val actionFlow: SharedFlow = _actionFlow + private val _infoFlow: MutableStateFlow = MutableStateFlow(null) + val infoFlow: StateFlow = _infoFlow.asStateFlow() + private val _errorFlow = MutableStateFlow("") + val errorFlow: StateFlow = _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) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/main/MainIntent.kt b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainIntent.kt new file mode 100644 index 0000000..e892f17 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainIntent.kt @@ -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 +} \ No newline at end of file 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 new file mode 100644 index 0000000..c4f133c --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainScreen.kt @@ -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) + ) +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/main/MainState.kt b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainState.kt new file mode 100644 index 0000000..c4dc7dd --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainState.kt @@ -0,0 +1,9 @@ +package ru.myitschool.work.ui.screen.main + +sealed interface MainState { + object Loading: MainState + + object Data: MainState + + object Error: MainState +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/main/MainViewModel.kt b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainViewModel.kt new file mode 100644 index 0000000..5a63a6d --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainViewModel.kt @@ -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.Loading) + val uiState: StateFlow = _uiState.asStateFlow() + private val _actionFlow: MutableSharedFlow = MutableSharedFlow() + val actionFlow: SharedFlow = _actionFlow + private val _infoFlow: MutableStateFlow = MutableStateFlow(null) + val infoFlow: StateFlow = _infoFlow.asStateFlow() + private val _errorFlow = MutableStateFlow("") + val errorFlow: StateFlow = _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) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/splash/SplashScreen.kt b/app/src/main/java/ru/myitschool/work/ui/screen/splash/SplashScreen.kt new file mode 100644 index 0000000..381b4be --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/splash/SplashScreen.kt @@ -0,0 +1,8 @@ +package ru.myitschool.work.ui.screen.splash + +import androidx.compose.runtime.Composable + +@Composable +fun SplashScreen() { + +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/theme/Theme.kt b/app/src/main/java/ru/myitschool/work/ui/theme/Theme.kt index d9cc58f..86ede1e 100644 --- a/app/src/main/java/ru/myitschool/work/ui/theme/Theme.kt +++ b/app/src/main/java/ru/myitschool/work/ui/theme/Theme.kt @@ -52,6 +52,6 @@ fun WorkTheme( MaterialTheme( colorScheme = colorScheme, typography = Typography, - content = content + content = content, ) } \ 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..6e0569b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,7 +1,7 @@ Work RootActivity - Привет! Введи код для авторизации + Авторизируйтесь при помощи кода Код Войти \ No newline at end of file