From acb65d63cb08e2d2817f5c807c3a4f2758d5082c Mon Sep 17 00:00:00 2001 From: nicktun Date: Sun, 30 Nov 2025 16:55:10 +0300 Subject: [PATCH 01/14] Login screen implemented (without saving data) --- .../java/ru/myitschool/work/core/TestIds.kt | 1 + .../work/data/repo/AuthRepository.kt | 1 + .../myitschool/work/ui/root/RootActivity.kt | 1 + .../work/ui/screen/NavigationGraph.kt | 4 +-- .../work/ui/screen/auth/AuthScreen.kt | 28 +++++++++++++++---- .../work/ui/screen/auth/AuthState.kt | 2 ++ .../work/ui/screen/auth/AuthViewModel.kt | 21 ++++++++++++-- .../java/ru/myitschool/work/ui/theme/Theme.kt | 2 +- 8 files changed, 48 insertions(+), 12 deletions(-) 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/repo/AuthRepository.kt b/app/src/main/java/ru/myitschool/work/data/repo/AuthRepository.kt index 3ef28f1..db1293e 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,5 +1,6 @@ package ru.myitschool.work.data.repo +import android.util.Log import ru.myitschool.work.data.source.NetworkDataSource object AuthRepository { 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..d373d0c 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 @@ -15,6 +15,7 @@ class RootActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() + actionBar?.hide() setContent { WorkTheme { Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> 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..c83595e 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 @@ -35,14 +35,14 @@ fun AppNavHost( Box( contentAlignment = Alignment.Center ) { - Text(text = "Hello") + Text(text = "MAIN") } } composable { Box( contentAlignment = Alignment.Center ) { - Text(text = "Hello") + Text(text = "BOOK") } } } 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..de6661d 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,5 +1,6 @@ package ru.myitschool.work.ui.screen.auth +import android.util.Log import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -10,6 +11,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.runtime.Composable @@ -22,6 +24,7 @@ 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.layout.onGloballyPositioned import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign @@ -54,16 +57,16 @@ fun AuthScreen( ) { Text( text = stringResource(R.string.auth_title), - style = MaterialTheme.typography.headlineSmall, + style = MaterialTheme.typography.headlineMedium, 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,11 +74,14 @@ fun AuthScreen( @Composable private fun Content( viewModel: AuthViewModel, - state: AuthState.Data + state: AuthState, ) { var inputText by remember { mutableStateOf("") } + val err by viewModel.errorFlow.collectAsState() + val isButtonEnabled by viewModel.isButtonEnabled.collectAsState() + Spacer(modifier = Modifier.size(16.dp)) - TextField( + OutlinedTextField( modifier = Modifier.testTag(TestIds.Auth.CODE_INPUT).fillMaxWidth(), value = inputText, onValueChange = { @@ -85,12 +91,22 @@ private fun Content( label = { Text(stringResource(R.string.auth_label)) } ) Spacer(modifier = Modifier.size(16.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(), onClick = { viewModel.onIntent(AuthIntent.Send(inputText)) }, - enabled = true + enabled = isButtonEnabled ) { Text(stringResource(R.string.auth_sign_in)) } 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..6770dc1 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,6 @@ 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..9896bd9 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 @@ -18,26 +19,40 @@ class AuthViewModel : ViewModel() { 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/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 -- 2.34.1 From 0bd59b81e3134a7de9f6ae405183a16752dbc436 Mon Sep 17 00:00:00 2001 From: nicktun Date: Sun, 30 Nov 2025 17:02:49 +0300 Subject: [PATCH 02/14] placeholder fix --- .../main/java/ru/myitschool/work/ui/screen/auth/AuthScreen.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 de6661d..a1bd9e9 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 @@ -88,7 +88,9 @@ private fun Content( inputText = it viewModel.onIntent(AuthIntent.TextInput(it)) }, - label = { Text(stringResource(R.string.auth_label)) } + label = { Text(stringResource(R.string.auth_label)) }, + placeholder = { Text(stringResource(R.string.auth_label)) } + ) Spacer(modifier = Modifier.size(16.dp)) if (state == AuthState.Error) { -- 2.34.1 From e572d2dd1e81a3ddfb2e10d9752a2eff413e4e21 Mon Sep 17 00:00:00 2001 From: nicktun Date: Sun, 30 Nov 2025 19:13:55 +0300 Subject: [PATCH 03/14] Implemented auth with login state saved --- app/build.gradle.kts | 1 + .../work/data/repo/AuthRepository.kt | 14 +++++---- .../work/data/source/LocalDataSource.kt | 31 +++++++++++++++++++ .../auth/CheckAndSaveAuthCodeUseCase.kt | 1 + .../myitschool/work/ui/root/RootActivity.kt | 10 +++++- .../work/ui/screen/NavigationGraph.kt | 11 +++++-- .../work/ui/screen/auth/AuthViewModel.kt | 2 +- 7 files changed, 60 insertions(+), 10 deletions(-) create mode 100644 app/src/main/java/ru/myitschool/work/data/source/LocalDataSource.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a5ccda1..d725b1f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -48,4 +48,5 @@ dependencies { implementation("io.ktor:ktor-client-content-negotiation:$ktor") implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0") + implementation("androidx.datastore:datastore-preferences:1.2.0") } 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 db1293e..ce5d6e5 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,17 +1,19 @@ package ru.myitschool.work.data.repo -import android.util.Log +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("") + } - 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/source/LocalDataSource.kt b/app/src/main/java/ru/myitschool/work/data/source/LocalDataSource.kt new file mode 100644 index 0000000..b6e84e8 --- /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] != "" } +} \ 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/ui/root/RootActivity.kt b/app/src/main/java/ru/myitschool/work/ui/root/RootActivity.kt index d373d0c..67de67e 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,19 +7,27 @@ 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 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(false) + Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> AppNavHost( + codePresence = codePresence, modifier = Modifier .fillMaxSize() .padding(innerPadding) 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 c83595e..58f52ef 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,12 +5,16 @@ 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.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 @@ -19,14 +23,15 @@ import ru.myitschool.work.ui.screen.auth.AuthScreen @Composable fun AppNavHost( modifier: Modifier = Modifier, - navController: NavHostController = rememberNavController() + navController: NavHostController = rememberNavController(), + codePresence: Boolean ) { NavHost( modifier = modifier, enterTransition = { EnterTransition.None }, exitTransition = { ExitTransition.None }, navController = navController, - startDestination = AuthScreenDestination, + startDestination = if (codePresence) MainScreenDestination else AuthScreenDestination, ) { composable { AuthScreen(navController = navController) @@ -36,6 +41,8 @@ fun AppNavHost( contentAlignment = Alignment.Center ) { Text(text = "MAIN") +// LaunchedEffect(Unit) { AuthRepository.clearCode() } + } } composable { 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 9896bd9..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 @@ -14,7 +14,7 @@ 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() -- 2.34.1 From 011804aa61bfdce836f3aa9cc0fa7057e8ba01d9 Mon Sep 17 00:00:00 2001 From: nicktun Date: Sun, 30 Nov 2025 22:13:55 +0300 Subject: [PATCH 04/14] Splash screen fix applied; Started work on main page --- app/build.gradle.kts | 1 + .../work/data/repo/AuthRepository.kt | 3 + .../ru/myitschool/work/domain/main/Logout.kt | 12 ++ .../work/ui/nav/SplashScreenDestination.kt | 6 + .../myitschool/work/ui/root/RootActivity.kt | 3 +- .../ru/myitschool/work/ui/root/RootState.kt | 9 + .../work/ui/screen/NavigationGraph.kt | 25 ++- .../work/ui/screen/main/MainIntent.kt | 6 + .../work/ui/screen/main/MainScreen.kt | 160 ++++++++++++++++++ .../work/ui/screen/main/MainState.kt | 9 + .../work/ui/screen/main/MainViewModel.kt | 27 +++ .../work/ui/screen/splash/SplashScreen.kt | 8 + 12 files changed, 259 insertions(+), 10 deletions(-) create mode 100644 app/src/main/java/ru/myitschool/work/domain/main/Logout.kt create mode 100644 app/src/main/java/ru/myitschool/work/ui/nav/SplashScreenDestination.kt create mode 100644 app/src/main/java/ru/myitschool/work/ui/root/RootState.kt create mode 100644 app/src/main/java/ru/myitschool/work/ui/screen/main/MainIntent.kt create mode 100644 app/src/main/java/ru/myitschool/work/ui/screen/main/MainScreen.kt create mode 100644 app/src/main/java/ru/myitschool/work/ui/screen/main/MainState.kt create mode 100644 app/src/main/java/ru/myitschool/work/ui/screen/main/MainViewModel.kt create mode 100644 app/src/main/java/ru/myitschool/work/ui/screen/splash/SplashScreen.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d725b1f..9f82afb 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -49,4 +49,5 @@ dependencies { 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/data/repo/AuthRepository.kt b/app/src/main/java/ru/myitschool/work/data/repo/AuthRepository.kt index ce5d6e5..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 @@ -7,6 +7,9 @@ object AuthRepository { suspend fun clearCode() { LocalDataSource.setCode("") } + suspend fun getCode(): String { + return LocalDataSource.getCode() + } val isCodePresentFlow: Flow = LocalDataSource.isCodePresentFlow suspend fun checkAndSave(text: String): Result { 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/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 67de67e..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 @@ -10,6 +10,7 @@ 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 @@ -23,7 +24,7 @@ class RootActivity() : ComponentActivity() { actionBar?.hide() setContent { WorkTheme { - val codePresence by AuthRepository.isCodePresentFlow.collectAsState(false) + val codePresence by AuthRepository.isCodePresentFlow.collectAsState(initial = null) Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> AppNavHost( 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 58f52ef..0df5e00 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 @@ -6,6 +6,7 @@ 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 @@ -18,32 +19,38 @@ 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.main.MainScreen +import ru.myitschool.work.ui.screen.splash.SplashScreen @Composable fun AppNavHost( modifier: Modifier = Modifier, navController: NavHostController = rememberNavController(), - codePresence: Boolean + codePresence: Boolean? ) { + val startDestination = if (codePresence == null) SplashScreenDestination + else if (codePresence == true) MainScreenDestination + else AuthScreenDestination + NavHost( modifier = modifier, enterTransition = { EnterTransition.None }, exitTransition = { ExitTransition.None }, navController = navController, - startDestination = if (codePresence) MainScreenDestination else AuthScreenDestination, + startDestination = startDestination, ) { composable { AuthScreen(navController = navController) } + composable { + SplashScreen() + } composable { - Box( - contentAlignment = Alignment.Center - ) { - Text(text = "MAIN") -// LaunchedEffect(Unit) { AuthRepository.clearCode() } - - } + MainScreen(navController = navController) } composable { Box( 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..32a5f41 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainIntent.kt @@ -0,0 +1,6 @@ +package ru.myitschool.work.ui.screen.main + +sealed interface MainIntent { + data object Fetch: MainIntent + data object Logout: 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..8e09c8d --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainScreen.kt @@ -0,0 +1,160 @@ +package ru.myitschool.work.ui.screen.main + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +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.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +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.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.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard +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.draw.shadow +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import coil3.compose.rememberAsyncImagePainter + +@Composable +fun MainScreen( + viewModel: MainViewModel = viewModel(), + navController: NavController +) { + val state by viewModel.uiState.collectAsState() + + when (val currentState = state) { + is MainState.Error -> { + Text("ИДИ НАХУЙ") + } + + is MainState.Loading -> { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + CircularProgressIndicator() + } + } + is MainState.Data -> { + Column ( + modifier = Modifier.fillMaxSize() + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(top = 40.dp) + .height(200.dp), + ) { + IconButton( + onClick = { viewModel.onIntent(MainIntent.Logout) }, + modifier = Modifier + .align(Alignment.TopEnd) + .size(20.dp) + .aspectRatio(1f) + .offset(x = -30.dp, y = 40.dp) + ) { + Icon( + Icons.AutoMirrored.Outlined.Logout, + contentDescription = null, + tint = MaterialTheme.colorScheme.error, + modifier = Modifier + .fillMaxSize() + .shadow(7.dp) + ) } + + + Row( + modifier = Modifier.fillMaxSize(), + verticalAlignment = Alignment.CenterVertically + ) { + Column ( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .padding(start = 30.dp) + .size(120.dp) + .aspectRatio(1f) + .background(MaterialTheme.colorScheme.primaryContainer, CircleShape) + ) { + Image( + painter = rememberAsyncImagePainter("https://catalog-cdn.detmir.st/media/2fe02057f9915e72a378795d32c79ea9.jpeg"), + contentDescription = null, + contentScale = ContentScale.Fit, + modifier = Modifier.size(105.dp).clip(CircleShape) + ) + } + } + } + +// Box( +// modifier = Modifier +// .fillMaxSize() +// .background(MaterialTheme.colorScheme.surfaceContainerLow, RoundedCornerShape(topEnd = 24.dp , topStart = 24.dp)) +// ) { +// LazyColumn( +// modifier = Modifier +// .fillMaxWidth() +// .padding(horizontal = 20.dp) +// .padding(top = 8.dp) +// ) { +// items( +// items = [], +// key = { item -> item.id } +// ) { entry -> +// OutlinedCard( +// modifier = Modifier +// .fillMaxWidth() +// .padding(top = 12.dp) +// ) { +// Text( +// text = entry.time, +// style = MaterialTheme.typography.titleMedium, +// fontWeight = FontWeight.Light, +// modifier = Modifier +// .padding(start = 16.dp) +// .padding(top = 12.dp) +// ) +// Text( +// text = entry.place.name, +// style = MaterialTheme.typography.titleMedium, +// fontWeight = FontWeight.Normal, +// modifier = Modifier +// .padding(start = 16.dp) +// .padding(bottom = 12.dp) +// ) +// } +// } +// } + } + } + } + + LaunchedEffect(Unit) { + viewModel.onIntent(MainIntent.Fetch) + } + +} \ 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..dfa3af0 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainViewModel.kt @@ -0,0 +1,27 @@ +package ru.myitschool.work.ui.screen.main + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import ru.myitschool.work.data.repo.AuthRepository +import ru.myitschool.work.domain.main.Logout + +class MainViewModel(): ViewModel() { + private val logout by lazy { Logout(AuthRepository) } + private val _uiState = MutableStateFlow(MainState.Data) + val uiState: StateFlow = _uiState.asStateFlow() + + fun onIntent(intent: MainIntent) { + when (intent) { + is MainIntent.Fetch -> Unit + is MainIntent.Logout -> { + viewModelScope.launch { + logout.invoke() + } + } + } + } +} \ 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 -- 2.34.1 From 8a6430f20e5eba1027ec6899092303c077589c02 Mon Sep 17 00:00:00 2001 From: nicktun Date: Sun, 30 Nov 2025 23:06:36 +0300 Subject: [PATCH 05/14] Visual fixes --- .../ui/components/ConditionalImePadding.kt | 15 ++++++++ .../work/ui/screen/auth/AuthScreen.kt | 36 ++++++++++++------- app/src/main/res/values/strings.xml | 2 +- 3 files changed, 39 insertions(+), 14 deletions(-) create mode 100644 app/src/main/java/ru/myitschool/work/ui/components/ConditionalImePadding.kt 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/screen/auth/AuthScreen.kt b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthScreen.kt index a1bd9e9..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,19 +1,22 @@ package ru.myitschool.work.ui.screen.auth -import android.util.Log +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 @@ -24,7 +27,6 @@ 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.layout.onGloballyPositioned import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign @@ -33,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 @@ -51,15 +54,11 @@ 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.headlineMedium, - textAlign = TextAlign.Center - ) when (val currentState = state) { is AuthState.Loading -> { CircularProgressIndicator( @@ -80,7 +79,12 @@ private fun Content( val err by viewModel.errorFlow.collectAsState() val isButtonEnabled by viewModel.isButtonEnabled.collectAsState() - Spacer(modifier = Modifier.size(16.dp)) + 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, @@ -88,11 +92,12 @@ private fun Content( inputText = it viewModel.onIntent(AuthIntent.TextInput(it)) }, + 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, @@ -104,12 +109,17 @@ private fun Content( Button( modifier = Modifier .testTag(TestIds.Auth.SIGN_BUTTON) - .fillMaxWidth(), + .fillMaxWidth() + .height(56.dp), onClick = { viewModel.onIntent(AuthIntent.Send(inputText)) }, + 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/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 -- 2.34.1 From 100a3adbb2879f9b0537a633380e799ec58d73c7 Mon Sep 17 00:00:00 2001 From: nicktun Date: Mon, 1 Dec 2025 14:57:12 +0300 Subject: [PATCH 06/14] Main Screen Front implemented --- .../work/ui/screen/main/MainScreen.kt | 242 +++++++++++++----- 1 file changed, 181 insertions(+), 61 deletions(-) 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 index 8e09c8d..e02e484 100644 --- 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 @@ -1,28 +1,38 @@ 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.height 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.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.OutlinedCard import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -31,12 +41,14 @@ 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.draw.shadow 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 @Composable fun MainScreen( @@ -45,9 +57,32 @@ fun MainScreen( ) { val state by viewModel.uiState.collectAsState() + val bookings = listOf( + Booking(time = "08:00", place = "Meeting Room A"), + Booking(time = "09:30", place = "Conference Hall 1"), + Booking(time = "10:00", place = "CEO Office"), + Booking(time = "11:15", place = "Training Room B"), + Booking(time = "12:00", place = "Cafeteria (Lunch Meeting)"), + Booking(time = "13:30", place = "Boardroom"), + Booking(time = "14:00", place = "Design Studio"), + Booking(time = "15:00", place = "Meeting Room C"), + Booking(time = "16:15", place = "HR Office"), + Booking(time = "17:00", place = "Auditorium"), + Booking(time = "09:00", place = "Meeting Room B"), + Booking(time = "10:30", place = "Client Lounge"), + Booking(time = "11:00", place = "Server Room"), + Booking(time = "13:00", place = "Rooftop Terrace"), + Booking(time = "14:30", place = "Lab 3"), + Booking(time = "15:45", place = "Reception Area"), + Booking(time = "18:00", place = "Parking Lot (Team Event)"), + Booking(time = "19:30", place = "Offsite - Sky Bar"), + Booking(time = "08:30", place = "Video Conference Room"), + Booking(time = "16:30", place = "Gym (Wellness Session)") + ) + when (val currentState = state) { is MainState.Error -> { - Text("ИДИ НАХУЙ") + Text("TEST_ERROR", modifier = Modifier.testTag(TestIds.Main.ERROR)) } is MainState.Loading -> { @@ -66,95 +101,180 @@ fun MainScreen( Box( modifier = Modifier .fillMaxWidth() - .padding(top = 40.dp) - .height(200.dp), + .padding(top = 20.dp), ) { - IconButton( + FilledTonalIconButton( onClick = { viewModel.onIntent(MainIntent.Logout) }, modifier = Modifier .align(Alignment.TopEnd) - .size(20.dp) + .size(40.dp) .aspectRatio(1f) - .offset(x = -30.dp, y = 40.dp) + .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, - tint = MaterialTheme.colorScheme.error, modifier = Modifier - .fillMaxSize() - .shadow(7.dp) - ) } + .size(20.dp) + ) + } - - Row( - modifier = Modifier.fillMaxSize(), - verticalAlignment = Alignment.CenterVertically + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally ) { - Column ( + Column( verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier - .padding(start = 30.dp) .size(120.dp) .aspectRatio(1f) - .background(MaterialTheme.colorScheme.primaryContainer, CircleShape) + .background(MaterialTheme.colorScheme.inverseOnSurface, CircleShape) ) { Image( painter = rememberAsyncImagePainter("https://catalog-cdn.detmir.st/media/2fe02057f9915e72a378795d32c79ea9.jpeg"), contentDescription = null, contentScale = ContentScale.Fit, - modifier = Modifier.size(105.dp).clip(CircleShape) + modifier = Modifier + .size(105.dp) + .clip(CircleShape) + .testTag(TestIds.Main.PROFILE_IMAGE) ) } + Spacer(modifier = Modifier.size(12.dp)) + Text( + text = "Smirnova Anna", + 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)) -// ) { -// LazyColumn( -// modifier = Modifier -// .fillMaxWidth() -// .padding(horizontal = 20.dp) -// .padding(top = 8.dp) -// ) { -// items( -// items = [], -// key = { item -> item.id } -// ) { entry -> -// OutlinedCard( -// modifier = Modifier -// .fillMaxWidth() -// .padding(top = 12.dp) -// ) { -// Text( -// text = entry.time, -// style = MaterialTheme.typography.titleMedium, -// fontWeight = FontWeight.Light, -// modifier = Modifier -// .padding(start = 16.dp) -// .padding(top = 12.dp) -// ) -// Text( -// text = entry.place.name, -// style = MaterialTheme.typography.titleMedium, -// fontWeight = FontWeight.Normal, -// modifier = Modifier -// .padding(start = 16.dp) -// .padding(bottom = 12.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, + tint = MaterialTheme.colorScheme.secondaryFixed + ) + 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(bookings) { index, booking -> + Booking(booking = booking, index = index) + } + } + } + + FloatingActionButton( + onClick = { }, + 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) } -} \ No newline at end of file +} + +@Composable +private fun Booking(booking: Booking, 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 = booking.time, + 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) + ) +} + +data class Booking( + val time: String, + val place: String +) \ No newline at end of file -- 2.34.1 From 053a916b55eb7e45004cb6eb7d316e3dcb3389d7 Mon Sep 17 00:00:00 2001 From: nicktun Date: Mon, 1 Dec 2025 17:59:10 +0300 Subject: [PATCH 07/14] Book screen frontend partially implemented --- app/build.gradle.kts | 1 + .../work/ui/screen/NavigationGraph.kt | 11 +- .../work/ui/screen/auth/AuthState.kt | 1 - .../work/ui/screen/book/BookIntent.kt | 7 + .../work/ui/screen/book/BookScreen.kt | 221 ++++++++++++++++++ .../work/ui/screen/book/BookState.kt | 8 + .../work/ui/screen/book/BookViewModel.kt | 27 +++ .../work/ui/screen/main/MainIntent.kt | 1 + .../work/ui/screen/main/MainScreen.kt | 31 ++- .../work/ui/screen/main/MainViewModel.kt | 9 + 10 files changed, 305 insertions(+), 12 deletions(-) create mode 100644 app/src/main/java/ru/myitschool/work/ui/screen/book/BookIntent.kt create mode 100644 app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt create mode 100644 app/src/main/java/ru/myitschool/work/ui/screen/book/BookState.kt create mode 100644 app/src/main/java/ru/myitschool/work/ui/screen/book/BookViewModel.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9f82afb..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") 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 0df5e00..a87d511 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 @@ -23,6 +23,7 @@ import ru.myitschool.work.ui.nav.SplashScreenDestination import ru.myitschool.work.ui.root.RootState import ru.myitschool.work.ui.screen.auth.AuthIntent import ru.myitschool.work.ui.screen.auth.AuthScreen +import ru.myitschool.work.ui.screen.book.BookScreen import ru.myitschool.work.ui.screen.main.MainScreen import ru.myitschool.work.ui.screen.splash.SplashScreen @@ -38,8 +39,8 @@ fun AppNavHost( NavHost( modifier = modifier, - enterTransition = { EnterTransition.None }, - exitTransition = { ExitTransition.None }, +// enterTransition = { EnterTransition.None }, +// exitTransition = { ExitTransition.None }, navController = navController, startDestination = startDestination, ) { @@ -53,11 +54,7 @@ fun AppNavHost( MainScreen(navController = navController) } composable { - Box( - contentAlignment = Alignment.Center - ) { - Text(text = "BOOK") - } + BookScreen(navController = navController) } } } \ 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 6770dc1..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,6 +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/book/BookIntent.kt b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookIntent.kt new file mode 100644 index 0000000..f947f3e --- /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 object Book: 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..f306382 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt @@ -0,0 +1,221 @@ +package ru.myitschool.work.ui.screen.book + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.selection.selectable +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBackIosNew +import androidx.compose.material.icons.filled.BookmarkAdd +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExtendedFloatingActionButton +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.PrimaryScrollableTabRow +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Tab +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import ru.myitschool.work.core.TestIds +import ru.myitschool.work.ui.nav.MainScreenDestination + +@Composable +fun BookScreen( + navController: NavController, + viewModel: BookViewModel = viewModel() +) { + val state by viewModel.uiState.collectAsState() + val dates = remember { bookingsByDate.keys.sorted() } + var selectedTabIndex by remember { mutableStateOf(0) } + + Box( + modifier = Modifier + .fillMaxSize() + ){ + when(val currentState = state) { + is BookState.Loading -> { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + CircularProgressIndicator() + } + } + + is BookState.Error -> { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxSize() + ) { + Text( + text = "TEST_ERROR", + modifier = Modifier.testTag(TestIds.Book.ERROR), + color = MaterialTheme.colorScheme.error + ) + } + + FloatingActionButton( + onClick = { viewModel.onIntent(BookIntent.Fetch) }, + containerColor = MaterialTheme.colorScheme.secondaryContainer, + contentColor = MaterialTheme.colorScheme.onSecondaryContainer, + modifier = Modifier + .align(Alignment.BottomEnd) + .offset(x = -16.dp, y = -16.dp) + .testTag(TestIds.Book.REFRESH_BUTTON) + ) { + Icon(Icons.Default.Refresh, contentDescription = "Обновить") + } + } + + is BookState.DataPresent -> { + Column(modifier = Modifier.fillMaxSize()) { + PrimaryScrollableTabRow( + selectedTabIndex = selectedTabIndex, + edgePadding = 16.dp, + containerColor = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.primary, + ) { + dates.forEachIndexed { index, date -> + Tab( + selected = selectedTabIndex == index, + onClick = { selectedTabIndex = index }, + text = { + Text( + text = date, + modifier = Modifier.testTag(TestIds.Book.ITEM_DATE) + ) + }, + modifier = Modifier.testTag(TestIds.Book.getIdDateItemByPosition(index)) + ) + } + } + + val selectedDate = dates[selectedTabIndex] + val bookings = bookingsByDate[selectedDate] ?: emptyList() + + LazyColumn( + modifier = Modifier.fillMaxSize(), + ) { + itemsIndexed(bookings) { index, booking -> + Booking(booking, index) + } + } + } + + ExtendedFloatingActionButton( + onClick = { viewModel.onIntent(BookIntent.Book) }, + containerColor = MaterialTheme.colorScheme.secondaryContainer, + contentColor = MaterialTheme.colorScheme.onSecondaryContainer, + text = { + Text("Бронировать") + }, + icon = { + Icon(Icons.Default.BookmarkAdd, contentDescription = "Бронировать") + }, + modifier = Modifier + .align(Alignment.BottomEnd) + .offset(x = -16.dp, y = -16.dp) + .testTag(TestIds.Book.BOOK_BUTTON) + ) + } + + is BookState.DataAbsent -> { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxSize() + ) { + Text(text = "Всё забронировано", modifier = Modifier.testTag(TestIds.Book.EMPTY)) + } + } + } + + + FloatingActionButton( + onClick = { viewModel.onIntent(BookIntent.GoBack) }, + containerColor = MaterialTheme.colorScheme.secondaryContainer, + contentColor = MaterialTheme.colorScheme.onSecondaryContainer, + modifier = Modifier + .align(Alignment.BottomStart) + .offset(x = 16.dp, y = -16.dp) + .testTag(TestIds.Book.BACK_BUTTON) + ) { + Icon(Icons.Default.ArrowBackIosNew, contentDescription = "Назад") + } + } + + LaunchedEffect(Unit) { + viewModel.actionFlow.collect { + navController.navigate(MainScreenDestination) + } + } +} + +@Composable +private fun Booking(booking: Booking, index: Int){ + Row( + modifier = Modifier + .fillMaxWidth() +// .clickable { } + .padding(horizontal = 16.dp, vertical = 8.dp) + .testTag(TestIds.Book.getIdPlaceItemByPosition(index)), + verticalAlignment = Alignment.CenterVertically, + ) { + RadioButton( + selected = false, + onClick = {}, + modifier = Modifier + .testTag(TestIds.Book.ITEM_PLACE_SELECTOR) +// .selectable( +// selected = false, +// onClick = {} +// ) + ) + Text( + text = booking.place, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.testTag(TestIds.Book.ITEM_PLACE_TEXT) + ) + } + HorizontalDivider( + color = MaterialTheme.colorScheme.outlineVariant, + thickness = 1.dp, + modifier = Modifier.padding(start = 16.dp) + ) +} + +data class Booking(val id: Int, val place: String) +typealias BookingsByDate = Map> + +val bookingsByDate: BookingsByDate = mapOf( + "2025-01-05" to listOf(Booking(1, "102"), Booking(2, "209.13")), + "2025-01-06" to listOf(Booking(3, "Зона 51. 50")), + "2025-01-07" to listOf(Booking(1, "102"), Booking(2, "209.13")), + "2025-01-08" to listOf(Booking(2, "209.13")) +) \ 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..f1e46f6 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookViewModel.kt @@ -0,0 +1,27 @@ +package ru.myitschool.work.ui.screen.book + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +class BookViewModel(): ViewModel() { + private val _uiState = MutableStateFlow(BookState.DataPresent) + val uiState: StateFlow = _uiState.asStateFlow() + private val _actionFlow: MutableSharedFlow = MutableSharedFlow() + val actionFlow: SharedFlow = _actionFlow + + fun onIntent(intent: BookIntent) { + when(intent) { + is BookIntent.Fetch -> Unit + is BookIntent.Book -> Unit + 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 index 32a5f41..e892f17 100644 --- 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 @@ -3,4 +3,5 @@ package ru.myitschool.work.ui.screen.main sealed interface MainIntent { data object Fetch: MainIntent data object Logout: MainIntent + data object NewBooking: MainIntent } \ 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 index e02e484..5aa3d78 100644 --- 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 @@ -23,6 +23,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.Logout import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.BookmarkBorder +import androidx.compose.material.icons.filled.Bookmarks import androidx.compose.material.icons.filled.ChevronRight import androidx.compose.material.icons.filled.Refresh import androidx.compose.material3.CircularProgressIndicator @@ -49,6 +50,8 @@ import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import coil3.compose.rememberAsyncImagePainter import ru.myitschool.work.core.TestIds +import ru.myitschool.work.ui.nav.BookScreenDestination +import ru.myitschool.work.ui.nav.MainScreenDestination @Composable fun MainScreen( @@ -82,7 +85,26 @@ fun MainScreen( when (val currentState = state) { is MainState.Error -> { - Text("TEST_ERROR", modifier = Modifier.testTag(TestIds.Main.ERROR)) + Text( + text = "TEST_ERROR", + modifier = Modifier.testTag(TestIds.Main.ERROR), + color = MaterialTheme.colorScheme.error + ) + IconButton( + onClick = { viewModel.onIntent(MainIntent.Fetch) }, + modifier = Modifier + .size(24.dp) + .aspectRatio(1f) + .testTag(TestIds.Main.REFRESH_BUTTON), + enabled = true, + ) { + Icon( + Icons.Default.Refresh, + contentDescription = null, + modifier = Modifier + .fillMaxSize() + ) + } } is MainState.Loading -> { @@ -176,7 +198,6 @@ fun MainScreen( Icon( imageVector = Icons.Default.BookmarkBorder, contentDescription = null, - tint = MaterialTheme.colorScheme.secondaryFixed ) Text( text = "Бронирования", @@ -213,7 +234,7 @@ fun MainScreen( } FloatingActionButton( - onClick = { }, + onClick = { viewModel.onIntent(MainIntent.NewBooking) }, containerColor = MaterialTheme.colorScheme.secondaryContainer, contentColor = MaterialTheme.colorScheme.onSecondaryContainer, modifier = Modifier @@ -223,7 +244,6 @@ fun MainScreen( ) { Icon(Icons.Default.Add, contentDescription = "Добавить") } - } } } @@ -231,6 +251,9 @@ fun MainScreen( LaunchedEffect(Unit) { viewModel.onIntent(MainIntent.Fetch) + viewModel.actionFlow.collect { + navController.navigate(BookScreenDestination) + } } } 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 index dfa3af0..41c0e3f 100644 --- 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 @@ -2,7 +2,9 @@ package ru.myitschool.work.ui.screen.main import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch @@ -13,6 +15,8 @@ class MainViewModel(): ViewModel() { private val logout by lazy { Logout(AuthRepository) } private val _uiState = MutableStateFlow(MainState.Data) val uiState: StateFlow = _uiState.asStateFlow() + private val _actionFlow: MutableSharedFlow = MutableSharedFlow() + val actionFlow: SharedFlow = _actionFlow fun onIntent(intent: MainIntent) { when (intent) { @@ -22,6 +26,11 @@ class MainViewModel(): ViewModel() { logout.invoke() } } + is MainIntent.NewBooking -> { + viewModelScope.launch { + _actionFlow.emit(Unit) + } + } } } } \ No newline at end of file -- 2.34.1 From 3e404ed765a5c73fec0b66177d561000eeae9df0 Mon Sep 17 00:00:00 2001 From: nicktun Date: Mon, 1 Dec 2025 21:38:25 +0300 Subject: [PATCH 08/14] Mainpage logic implemented --- .../ru/myitschool/work/data/models/Booking.kt | 9 + .../myitschool/work/data/models/UserData.kt | 10 + .../work/data/repo/BookRepository.kt | 4 + .../work/data/repo/MainRepository.kt | 14 + .../work/data/source/NetworkDataSource.kt | 14 + .../ru/myitschool/work/domain/main/Fetch.kt | 12 + .../work/ui/screen/main/MainScreen.kt | 288 ++++++++---------- .../work/ui/screen/main/MainViewModel.kt | 29 +- 8 files changed, 225 insertions(+), 155 deletions(-) create mode 100644 app/src/main/java/ru/myitschool/work/data/models/Booking.kt create mode 100644 app/src/main/java/ru/myitschool/work/data/models/UserData.kt create mode 100644 app/src/main/java/ru/myitschool/work/data/repo/BookRepository.kt create mode 100644 app/src/main/java/ru/myitschool/work/data/repo/MainRepository.kt create mode 100644 app/src/main/java/ru/myitschool/work/domain/main/Fetch.kt 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/UserData.kt b/app/src/main/java/ru/myitschool/work/data/models/UserData.kt new file mode 100644 index 0000000..039837c --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/data/models/UserData.kt @@ -0,0 +1,10 @@ +package ru.myitschool.work.data.models + +import kotlinx.serialization.Serializable + +@Serializable +data class UserData( + 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/BookRepository.kt b/app/src/main/java/ru/myitschool/work/data/repo/BookRepository.kt new file mode 100644 index 0000000..ead988b --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/data/repo/BookRepository.kt @@ -0,0 +1,4 @@ +package ru.myitschool.work.data.repo + +object BookRepository { +} \ 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..cdfe93d --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/data/repo/MainRepository.kt @@ -0,0 +1,14 @@ +package ru.myitschool.work.data.repo + +import android.util.Log +import ru.myitschool.work.data.models.Booking +import ru.myitschool.work.data.models.UserData +import ru.myitschool.work.data.repo.AuthRepository.getCode +import ru.myitschool.work.data.source.LocalDataSource +import ru.myitschool.work.data.source.NetworkDataSource + +object MainRepository { + suspend fun fetch(): Result { + return NetworkDataSource.Info(getCode()).onSuccess { data -> data.name } + } +} \ 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..091ed82 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,6 +1,8 @@ package ru.myitschool.work.data.source +import android.util.Log 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 @@ -11,6 +13,8 @@ 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.Booking +import ru.myitschool.work.data.models.UserData object NetworkDataSource { private val client by lazy { @@ -39,4 +43,14 @@ object NetworkDataSource { } private fun getUrl(code: String, targetUrl: String) = "${Constants.HOST}/api/$code$targetUrl" + + 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()) + } + } + } } \ 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..540e789 --- /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.UserData +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/ui/screen/main/MainScreen.kt b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainScreen.kt index 5aa3d78..96728ae 100644 --- 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 @@ -50,6 +50,7 @@ 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 @@ -59,51 +60,35 @@ fun MainScreen( navController: NavController ) { val state by viewModel.uiState.collectAsState() - - val bookings = listOf( - Booking(time = "08:00", place = "Meeting Room A"), - Booking(time = "09:30", place = "Conference Hall 1"), - Booking(time = "10:00", place = "CEO Office"), - Booking(time = "11:15", place = "Training Room B"), - Booking(time = "12:00", place = "Cafeteria (Lunch Meeting)"), - Booking(time = "13:30", place = "Boardroom"), - Booking(time = "14:00", place = "Design Studio"), - Booking(time = "15:00", place = "Meeting Room C"), - Booking(time = "16:15", place = "HR Office"), - Booking(time = "17:00", place = "Auditorium"), - Booking(time = "09:00", place = "Meeting Room B"), - Booking(time = "10:30", place = "Client Lounge"), - Booking(time = "11:00", place = "Server Room"), - Booking(time = "13:00", place = "Rooftop Terrace"), - Booking(time = "14:30", place = "Lab 3"), - Booking(time = "15:45", place = "Reception Area"), - Booking(time = "18:00", place = "Parking Lot (Team Event)"), - Booking(time = "19:30", place = "Offsite - Sky Bar"), - Booking(time = "08:30", place = "Video Conference Room"), - Booking(time = "16:30", place = "Gym (Wellness Session)") - ) + val err by viewModel.errorFlow.collectAsState() + val info by viewModel.infoFlow.collectAsState() when (val currentState = state) { is MainState.Error -> { - Text( - text = "TEST_ERROR", - modifier = Modifier.testTag(TestIds.Main.ERROR), - color = MaterialTheme.colorScheme.error - ) - IconButton( - onClick = { viewModel.onIntent(MainIntent.Fetch) }, - modifier = Modifier - .size(24.dp) - .aspectRatio(1f) - .testTag(TestIds.Main.REFRESH_BUTTON), - enabled = true, + Column ( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center ) { - Icon( - Icons.Default.Refresh, - contentDescription = null, - modifier = Modifier - .fillMaxSize() + 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() + ) + } } } @@ -117,132 +102,134 @@ fun MainScreen( } } is MainState.Data -> { - Column ( - modifier = Modifier.fillMaxSize() - ) { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(top = 20.dp), + info?.let { + Column ( + modifier = Modifier.fillMaxSize() ) { - FilledTonalIconButton( - onClick = { viewModel.onIntent(MainIntent.Logout) }, + Box( 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 - ) + .fillMaxWidth() + .padding(top = 20.dp), ) { - Icon( - Icons.AutoMirrored.Outlined.Logout, - contentDescription = null, + FilledTonalIconButton( + onClick = { viewModel.onIntent(MainIntent.Logout) }, modifier = Modifier - .size(20.dp) - ) - } - - Column( - modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier - .size(120.dp) + .align(Alignment.TopEnd) + .size(40.dp) .aspectRatio(1f) - .background(MaterialTheme.colorScheme.inverseOnSurface, CircleShape) - ) { - Image( - painter = rememberAsyncImagePainter("https://catalog-cdn.detmir.st/media/2fe02057f9915e72a378795d32c79ea9.jpeg"), - contentDescription = null, - contentScale = ContentScale.Fit, - modifier = Modifier - .size(105.dp) - .clip(CircleShape) - .testTag(TestIds.Main.PROFILE_IMAGE) + .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 ) - } - Spacer(modifier = Modifier.size(12.dp)) - Text( - text = "Smirnova Anna", - 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, + Icons.AutoMirrored.Outlined.Logout, contentDescription = null, - ) - Text( - text = "Бронирования", - style = MaterialTheme.typography.titleMedium, - ) - IconButton( - onClick = { viewModel.onIntent(MainIntent.Fetch) }, modifier = Modifier - .size(24.dp) + .size(20.dp) + ) + } + + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .size(120.dp) .aspectRatio(1f) - .testTag(TestIds.Main.REFRESH_BUTTON), - enabled = true, + .background(MaterialTheme.colorScheme.inverseOnSurface, CircleShape) ) { - Icon( - Icons.Default.Refresh, + Image( + painter = rememberAsyncImagePainter("https://catalog-cdn.detmir.st/media/2fe02057f9915e72a378795d32c79ea9.jpeg"), contentDescription = null, + contentScale = ContentScale.Fit, modifier = Modifier - .fillMaxSize() + .size(105.dp) + .clip(CircleShape) + .testTag(TestIds.Main.PROFILE_IMAGE) ) } - } - HorizontalDivider( - color = MaterialTheme.colorScheme.outlineVariant, - thickness = 1.dp, - ) - LazyColumn( - modifier = Modifier - .fillMaxWidth() - ) { - itemsIndexed(bookings) { index, booking -> - Booking(booking = booking, index = index) - } + Spacer(modifier = Modifier.size(12.dp)) + Text( + text = "Smirnova Anna", + style = MaterialTheme.typography.titleLarge, + modifier = Modifier + .testTag(TestIds.Main.PROFILE_NAME) + ) + Spacer(modifier = Modifier.size(16.dp)) } } - FloatingActionButton( - onClick = { viewModel.onIntent(MainIntent.NewBooking) }, - containerColor = MaterialTheme.colorScheme.secondaryContainer, - contentColor = MaterialTheme.colorScheme.onSecondaryContainer, + Box( modifier = Modifier - .align(Alignment.BottomEnd) - .offset(x = -16.dp, y = -16.dp) - .testTag(TestIds.Main.ADD_BUTTON) + .fillMaxSize() + .background(MaterialTheme.colorScheme.surfaceContainerLow, RoundedCornerShape(topEnd = 24.dp , topStart = 24.dp)) ) { - Icon(Icons.Default.Add, contentDescription = "Добавить") + 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 = "Добавить") + } } } } @@ -259,7 +246,7 @@ fun MainScreen( } @Composable -private fun Booking(booking: Booking, index: Int){ +private fun Booking(booking: Booking, date: String, index: Int){ Row( modifier = Modifier .fillMaxWidth() @@ -270,7 +257,7 @@ private fun Booking(booking: Booking, index: Int){ ) { Column(modifier = Modifier.weight(1f)) { Text( - text = booking.time, + text = date, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.testTag(TestIds.Main.ITEM_DATE) @@ -295,9 +282,4 @@ private fun Booking(booking: Booking, index: Int){ thickness = 1.dp, modifier = Modifier.padding(start = 16.dp) ) -} - -data class Booking( - val time: String, - val place: String -) \ No newline at end of file +} \ 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 index 41c0e3f..ec65c7d 100644 --- 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 @@ -1,5 +1,6 @@ package ru.myitschool.work.ui.screen.main +import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.MutableSharedFlow @@ -7,25 +8,49 @@ 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.UserData 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.Data) + 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 -> Unit + 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, "") + _uiState.update { MainState.Error } + _errorFlow.update { failure.message.toString() } + } + ) + } + is MainIntent.Logout -> { viewModelScope.launch { logout.invoke() } } + is MainIntent.NewBooking -> { viewModelScope.launch { _actionFlow.emit(Unit) -- 2.34.1 From 7adb05efb919504ccad80d6e7001d5e80ebf9219 Mon Sep 17 00:00:00 2001 From: nicktun Date: Mon, 1 Dec 2025 22:47:22 +0300 Subject: [PATCH 09/14] Book page fetch implemented --- .../work/data/models/BookingInfo.kt | 3 ++ .../data/models/{UserData.kt => UserInfo.kt} | 2 +- .../work/data/repo/BookRepository.kt | 7 ++++ .../work/data/repo/MainRepository.kt | 9 ++---- .../work/data/source/NetworkDataSource.kt | 19 ++++++++--- .../ru/myitschool/work/domain/book/Book.kt | 4 +++ .../ru/myitschool/work/domain/book/Fetch.kt | 12 +++++++ .../ru/myitschool/work/domain/main/Fetch.kt | 4 +-- .../work/ui/screen/book/BookScreen.kt | 28 ++++++---------- .../work/ui/screen/book/BookViewModel.kt | 32 +++++++++++++++++-- .../work/ui/screen/main/MainViewModel.kt | 8 ++--- 11 files changed, 89 insertions(+), 39 deletions(-) create mode 100644 app/src/main/java/ru/myitschool/work/data/models/BookingInfo.kt rename app/src/main/java/ru/myitschool/work/data/models/{UserData.kt => UserInfo.kt} (89%) create mode 100644 app/src/main/java/ru/myitschool/work/domain/book/Book.kt create mode 100644 app/src/main/java/ru/myitschool/work/domain/book/Fetch.kt 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/UserData.kt b/app/src/main/java/ru/myitschool/work/data/models/UserInfo.kt similarity index 89% rename from app/src/main/java/ru/myitschool/work/data/models/UserData.kt rename to app/src/main/java/ru/myitschool/work/data/models/UserInfo.kt index 039837c..7728fc3 100644 --- a/app/src/main/java/ru/myitschool/work/data/models/UserData.kt +++ b/app/src/main/java/ru/myitschool/work/data/models/UserInfo.kt @@ -3,7 +3,7 @@ package ru.myitschool.work.data.models import kotlinx.serialization.Serializable @Serializable -data class UserData( +data class UserInfo( val name: String, val photoUrl: String, val booking: Map 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 index ead988b..f305d95 100644 --- a/app/src/main/java/ru/myitschool/work/data/repo/BookRepository.kt +++ b/app/src/main/java/ru/myitschool/work/data/repo/BookRepository.kt @@ -1,4 +1,11 @@ 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.book(getCode()).onSuccess { data -> data } + } } \ 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 index cdfe93d..3d49bfa 100644 --- a/app/src/main/java/ru/myitschool/work/data/repo/MainRepository.kt +++ b/app/src/main/java/ru/myitschool/work/data/repo/MainRepository.kt @@ -1,14 +1,11 @@ package ru.myitschool.work.data.repo -import android.util.Log -import ru.myitschool.work.data.models.Booking -import ru.myitschool.work.data.models.UserData +import ru.myitschool.work.data.models.UserInfo import ru.myitschool.work.data.repo.AuthRepository.getCode -import ru.myitschool.work.data.source.LocalDataSource import ru.myitschool.work.data.source.NetworkDataSource object MainRepository { - suspend fun fetch(): Result { - return NetworkDataSource.Info(getCode()).onSuccess { data -> data.name } + 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/NetworkDataSource.kt b/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt index 091ed82..426701e 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,6 +1,5 @@ package ru.myitschool.work.data.source -import android.util.Log import io.ktor.client.HttpClient import io.ktor.client.call.body import io.ktor.client.engine.cio.CIO @@ -13,8 +12,8 @@ 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.Booking -import ru.myitschool.work.data.models.UserData +import ru.myitschool.work.data.models.BookingInfo +import ru.myitschool.work.data.models.UserInfo object NetworkDataSource { private val client by lazy { @@ -44,11 +43,21 @@ object NetworkDataSource { private fun getUrl(code: String, targetUrl: String) = "${Constants.HOST}/api/$code$targetUrl" - suspend fun Info(code: String): Result = withContext(Dispatchers.IO) { + 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()) + HttpStatusCode.OK -> Json.decodeFromString(response.body()) + else -> error(response.bodyAsText()) + } + } + } + + suspend fun book(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()) } } 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..2d395bb --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/domain/book/Book.kt @@ -0,0 +1,4 @@ +package ru.myitschool.work.domain.book + +class Book { +} \ 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 index 540e789..b2496d9 100644 --- a/app/src/main/java/ru/myitschool/work/domain/main/Fetch.kt +++ b/app/src/main/java/ru/myitschool/work/domain/main/Fetch.kt @@ -1,12 +1,12 @@ package ru.myitschool.work.domain.main -import ru.myitschool.work.data.models.UserData +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 { + 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/ui/screen/book/BookScreen.kt b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt index f306382..ba46f51 100644 --- 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 @@ -41,6 +41,7 @@ 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 @Composable @@ -49,7 +50,8 @@ fun BookScreen( viewModel: BookViewModel = viewModel() ) { val state by viewModel.uiState.collectAsState() - val dates = remember { bookingsByDate.keys.sorted() } + val info by viewModel.infoFlow.collectAsState() + val err by viewModel.errorFlow.collectAsState() var selectedTabIndex by remember { mutableStateOf(0) } Box( @@ -73,7 +75,7 @@ fun BookScreen( modifier = Modifier.fillMaxSize() ) { Text( - text = "TEST_ERROR", + text = err, modifier = Modifier.testTag(TestIds.Book.ERROR), color = MaterialTheme.colorScheme.error ) @@ -100,13 +102,13 @@ fun BookScreen( containerColor = MaterialTheme.colorScheme.surface, contentColor = MaterialTheme.colorScheme.primary, ) { - dates.forEachIndexed { index, date -> + info!!.entries.toList().forEachIndexed { index, entry -> Tab( selected = selectedTabIndex == index, onClick = { selectedTabIndex = index }, text = { Text( - text = date, + text = entry.key, modifier = Modifier.testTag(TestIds.Book.ITEM_DATE) ) }, @@ -115,13 +117,10 @@ fun BookScreen( } } - val selectedDate = dates[selectedTabIndex] - val bookings = bookingsByDate[selectedDate] ?: emptyList() - LazyColumn( modifier = Modifier.fillMaxSize(), ) { - itemsIndexed(bookings) { index, booking -> + itemsIndexed(info!!.entries.toList()[selectedTabIndex].value) { index, booking -> Booking(booking, index) } } @@ -169,6 +168,7 @@ fun BookScreen( } LaunchedEffect(Unit) { + viewModel.onIntent(BookIntent.Fetch) viewModel.actionFlow.collect { navController.navigate(MainScreenDestination) } @@ -208,14 +208,4 @@ private fun Booking(booking: Booking, index: Int){ thickness = 1.dp, modifier = Modifier.padding(start = 16.dp) ) -} - -data class Booking(val id: Int, val place: String) -typealias BookingsByDate = Map> - -val bookingsByDate: BookingsByDate = mapOf( - "2025-01-05" to listOf(Booking(1, "102"), Booking(2, "209.13")), - "2025-01-06" to listOf(Booking(3, "Зона 51. 50")), - "2025-01-07" to listOf(Booking(1, "102"), Booking(2, "209.13")), - "2025-01-08" to listOf(Booking(2, "209.13")) -) \ No newline at end of file +} \ 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 index f1e46f6..15e843e 100644 --- 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 @@ -1,5 +1,6 @@ package ru.myitschool.work.ui.screen.book +import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.MutableSharedFlow @@ -7,17 +8,44 @@ 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.Fetch +import ru.myitschool.work.ui.screen.main.MainState class BookViewModel(): ViewModel() { - private val _uiState = MutableStateFlow(BookState.DataPresent) + private val fetch by lazy { Fetch(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 -> Unit + 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 -> Unit is BookIntent.GoBack -> viewModelScope.launch { _actionFlow.emit(Unit) 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 index ec65c7d..5a63a6d 100644 --- 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 @@ -10,7 +10,7 @@ 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.UserData +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 @@ -23,8 +23,8 @@ class MainViewModel(): ViewModel() { 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 _infoFlow: MutableStateFlow = MutableStateFlow(null) + val infoFlow: StateFlow = _infoFlow.asStateFlow() private val _errorFlow = MutableStateFlow("") val errorFlow: StateFlow = _errorFlow.asStateFlow() @@ -38,7 +38,7 @@ class MainViewModel(): ViewModel() { _uiState.update { MainState.Data } }, onFailure = { failure -> - Log.d(failure.message, "") + Log.d(failure.message, "failure") _uiState.update { MainState.Error } _errorFlow.update { failure.message.toString() } } -- 2.34.1 From c53bd718c0f8f7969af5a5043e676f38f6bc051a Mon Sep 17 00:00:00 2001 From: nicktun Date: Wed, 3 Dec 2025 21:21:52 +0300 Subject: [PATCH 10/14] Finished book push functions --- .../myitschool/work/data/models/BookBody.kt | 9 ++++++ .../work/data/repo/BookRepository.kt | 6 +++- .../work/data/source/LocalDataSource.kt | 2 +- .../work/data/source/NetworkDataSource.kt | 25 +++++++++++++-- .../ru/myitschool/work/domain/book/Book.kt | 14 +++++++- .../work/ui/screen/book/BookIntent.kt | 2 +- .../work/ui/screen/book/BookScreen.kt | 32 +++++++++++++------ .../work/ui/screen/book/BookViewModel.kt | 17 ++++++++-- 8 files changed, 89 insertions(+), 18 deletions(-) create mode 100644 app/src/main/java/ru/myitschool/work/data/models/BookBody.kt 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/repo/BookRepository.kt b/app/src/main/java/ru/myitschool/work/data/repo/BookRepository.kt index f305d95..0105d29 100644 --- a/app/src/main/java/ru/myitschool/work/data/repo/BookRepository.kt +++ b/app/src/main/java/ru/myitschool/work/data/repo/BookRepository.kt @@ -6,6 +6,10 @@ import ru.myitschool.work.data.source.NetworkDataSource object BookRepository { suspend fun fetch(): Result { - return NetworkDataSource.book(getCode()).onSuccess { data -> data } + 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/source/LocalDataSource.kt b/app/src/main/java/ru/myitschool/work/data/source/LocalDataSource.kt index b6e84e8..c41b351 100644 --- a/app/src/main/java/ru/myitschool/work/data/source/LocalDataSource.kt +++ b/app/src/main/java/ru/myitschool/work/data/source/LocalDataSource.kt @@ -27,5 +27,5 @@ object LocalDataSource { appContext.dataStore.edit { it[Keys.CODE] = code } } - val isCodePresentFlow: Flow = appContext.dataStore.data.map { it[Keys.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 426701e..6b8e72b 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 @@ -5,13 +5,18 @@ 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 @@ -41,8 +46,6 @@ object NetworkDataSource { } } - private fun getUrl(code: String, targetUrl: String) = "${Constants.HOST}/api/$code$targetUrl" - suspend fun info(code: String): Result = withContext(Dispatchers.IO) { return@withContext runCatching { val response = client.get(getUrl(code, Constants.INFO_URL)) @@ -53,7 +56,7 @@ object NetworkDataSource { } } - suspend fun book(code: String): Result = withContext(Dispatchers.IO) { + suspend fun bookInfo(code: String): Result = withContext(Dispatchers.IO) { return@withContext runCatching { val response = client.get(getUrl(code, Constants.BOOKING_URL)) when (response.status) { @@ -62,4 +65,20 @@ object NetworkDataSource { } } } + + 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.OK -> 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/book/Book.kt b/app/src/main/java/ru/myitschool/work/domain/book/Book.kt index 2d395bb..3f9c9e3 100644 --- a/app/src/main/java/ru/myitschool/work/domain/book/Book.kt +++ b/app/src/main/java/ru/myitschool/work/domain/book/Book.kt @@ -1,4 +1,16 @@ package ru.myitschool.work.domain.book -class 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/ui/screen/book/BookIntent.kt b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookIntent.kt index f947f3e..1c5be79 100644 --- 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 @@ -2,6 +2,6 @@ package ru.myitschool.work.ui.screen.book sealed interface BookIntent { data object Fetch: BookIntent - data object Book: 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 index ba46f51..56b622e 100644 --- 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 @@ -53,6 +53,7 @@ fun BookScreen( 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 @@ -95,17 +96,22 @@ fun BookScreen( } 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, ) { - info!!.entries.toList().forEachIndexed { index, entry -> + entriesList.forEachIndexed { index, entry -> Tab( selected = selectedTabIndex == index, - onClick = { selectedTabIndex = index }, + onClick = { + selectedTabIndex = index + selectedPlaceIndex = 0 + }, text = { Text( text = entry.key, @@ -120,14 +126,22 @@ fun BookScreen( LazyColumn( modifier = Modifier.fillMaxSize(), ) { - itemsIndexed(info!!.entries.toList()[selectedTabIndex].value) { index, booking -> - Booking(booking, index) + itemsIndexed(entriesList[selectedTabIndex].value) { index, booking -> + Booking( + booking = booking, + index = index, + selected = selectedPlaceIndex, + onRadioChange = { selectedPlaceIndex = index } + ) } } } ExtendedFloatingActionButton( - onClick = { viewModel.onIntent(BookIntent.Book) }, + onClick = { viewModel.onIntent(BookIntent.Book( + date = entriesList[selectedTabIndex].key, + placeId = entriesList[selectedTabIndex].value[selectedPlaceIndex].id + )) }, containerColor = MaterialTheme.colorScheme.secondaryContainer, contentColor = MaterialTheme.colorScheme.onSecondaryContainer, text = { @@ -170,13 +184,13 @@ fun BookScreen( LaunchedEffect(Unit) { viewModel.onIntent(BookIntent.Fetch) viewModel.actionFlow.collect { - navController.navigate(MainScreenDestination) + navController.popBackStack() } } } @Composable -private fun Booking(booking: Booking, index: Int){ +private fun Booking(booking: Booking, index: Int, selected: Int, onRadioChange: Function0) { Row( modifier = Modifier .fillMaxWidth() @@ -186,8 +200,8 @@ private fun Booking(booking: Booking, index: Int){ verticalAlignment = Alignment.CenterVertically, ) { RadioButton( - selected = false, - onClick = {}, + selected = selected == index, + onClick = onRadioChange, modifier = Modifier .testTag(TestIds.Book.ITEM_PLACE_SELECTOR) // .selectable( 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 index 15e843e..959d4a3 100644 --- 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 @@ -12,11 +12,12 @@ 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 -import ru.myitschool.work.ui.screen.main.MainState 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() @@ -46,7 +47,19 @@ class BookViewModel(): ViewModel() { } ) } - is BookIntent.Book -> Unit + 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) } -- 2.34.1 From f5cab8e87535196819dd47385df0e38ab90fbc78 Mon Sep 17 00:00:00 2001 From: nicktun Date: Wed, 3 Dec 2025 21:26:53 +0300 Subject: [PATCH 11/14] Login screen fixed --- .../main/java/ru/myitschool/work/data/source/LocalDataSource.kt | 2 +- .../main/java/ru/myitschool/work/ui/screen/NavigationGraph.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 index c41b351..2714061 100644 --- a/app/src/main/java/ru/myitschool/work/data/source/LocalDataSource.kt +++ b/app/src/main/java/ru/myitschool/work/data/source/LocalDataSource.kt @@ -27,5 +27,5 @@ object LocalDataSource { appContext.dataStore.edit { it[Keys.CODE] = code } } - val isCodePresentFlow: Flow = appContext.dataStore.data.map { it[Keys.CODE] != "" || it[Keys.CODE] != null } + 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/ui/screen/NavigationGraph.kt b/app/src/main/java/ru/myitschool/work/ui/screen/NavigationGraph.kt index a87d511..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 @@ -34,7 +34,7 @@ fun AppNavHost( codePresence: Boolean? ) { val startDestination = if (codePresence == null) SplashScreenDestination - else if (codePresence == true) MainScreenDestination + else if (codePresence) MainScreenDestination else AuthScreenDestination NavHost( -- 2.34.1 From 03e87ad36c120d10fb8b3aaa497b7edd5e9ff415 Mon Sep 17 00:00:00 2001 From: nicktun Date: Thu, 4 Dec 2025 11:13:08 +0300 Subject: [PATCH 12/14] Fixed dates --- .../main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt | 4 +++- .../main/java/ru/myitschool/work/ui/screen/main/MainScreen.kt | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) 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 index 56b622e..bfc230d 100644 --- 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 @@ -43,6 +43,8 @@ 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( @@ -114,7 +116,7 @@ fun BookScreen( }, text = { Text( - text = entry.key, + text = LocalDate.parse(entry.key).format(DateTimeFormatter.ofPattern("dd.MM")), modifier = Modifier.testTag(TestIds.Book.ITEM_DATE) ) }, 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 index 96728ae..0e9063d 100644 --- 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 @@ -53,6 +53,8 @@ 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( @@ -257,7 +259,7 @@ private fun Booking(booking: Booking, date: String, index: Int){ ) { Column(modifier = Modifier.weight(1f)) { Text( - text = date, + 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) -- 2.34.1 From 85b23119d9894722f91c2ada574e50adbd9dfa33 Mon Sep 17 00:00:00 2001 From: nicktun Date: Thu, 4 Dec 2025 11:56:10 +0300 Subject: [PATCH 13/14] Fixed 201 code --- .../java/ru/myitschool/work/data/source/NetworkDataSource.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 6b8e72b..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 @@ -74,7 +74,7 @@ object NetworkDataSource { setBody(BookBody(date, placeId)) } when(response.status) { - HttpStatusCode.OK -> true + HttpStatusCode.Created -> true else -> error(response.bodyAsText()) } } -- 2.34.1 From cbfdf844ed1e03c125ce9785e76f6d41ef296395 Mon Sep 17 00:00:00 2001 From: nicktun Date: Thu, 4 Dec 2025 12:18:14 +0300 Subject: [PATCH 14/14] inner text values fixed --- .../main/java/ru/myitschool/work/ui/screen/main/MainScreen.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index 0e9063d..c4f133c 100644 --- 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 @@ -149,7 +149,7 @@ fun MainScreen( .background(MaterialTheme.colorScheme.inverseOnSurface, CircleShape) ) { Image( - painter = rememberAsyncImagePainter("https://catalog-cdn.detmir.st/media/2fe02057f9915e72a378795d32c79ea9.jpeg"), + painter = rememberAsyncImagePainter(info!!.photoUrl), contentDescription = null, contentScale = ContentScale.Fit, modifier = Modifier @@ -160,7 +160,7 @@ fun MainScreen( } Spacer(modifier = Modifier.size(12.dp)) Text( - text = "Smirnova Anna", + text = info!!.name, style = MaterialTheme.typography.titleLarge, modifier = Modifier .testTag(TestIds.Main.PROFILE_NAME) -- 2.34.1