From 5a793764821aa1cff65f9d5e615593a1c6f59a59 Mon Sep 17 00:00:00 2001 From: v3less11 Date: Wed, 10 Dec 2025 17:26:04 +0300 Subject: [PATCH] authscreen-changed --- app/src/main/java/ru/myitschool/work/App.kt | 4 +- .../work/data/repo/AuthRepository.kt | 35 ++++++- .../myitschool/work/ui/root/RootActivity.kt | 19 +++- .../work/ui/screen/NavigationGraph.kt | 17 ++-- .../work/ui/screen/auth/AuthScreen.kt | 73 +++++++------- .../work/ui/screen/auth/AuthState.kt | 8 +- .../work/ui/screen/auth/AuthViewModel.kt | 61 +++++++++--- .../work/ui/screen/main/MainScreen.kt | 94 +++++++++++++++++++ .../work/ui/screen/main/MainViewModel.kt | 70 ++++++++++++++ 9 files changed, 314 insertions(+), 67 deletions(-) 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/MainViewModel.kt diff --git a/app/src/main/java/ru/myitschool/work/App.kt b/app/src/main/java/ru/myitschool/work/App.kt index aa33483..cd744dd 100644 --- a/app/src/main/java/ru/myitschool/work/App.kt +++ b/app/src/main/java/ru/myitschool/work/App.kt @@ -2,11 +2,11 @@ package ru.myitschool.work import android.app.Application import android.content.Context - +import ru.myitschool.work.data.repo.AuthRepository class App: Application() { override fun onCreate() { super.onCreate() - context = this + AuthRepository.initialize(this) } companion object { 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..5c3bfbf 100644 --- a/app/src/main/java/ru/myitschool/work/data/repo/AuthRepository.kt +++ b/app/src/main/java/ru/myitschool/work/data/repo/AuthRepository.kt @@ -1,16 +1,45 @@ package ru.myitschool.work.data.repo +import android.content.Context +import android.content.SharedPreferences +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import ru.myitschool.work.data.source.NetworkDataSource object AuthRepository { + private const val PREFS_NAME = "auth_prefs" + private const val KEY_AUTH_CODE = "auth_code" + private var codeCache: String? = null + private lateinit var prefs: SharedPreferences + + fun initialize(context: Context) { + prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + codeCache = prefs.getString(KEY_AUTH_CODE, null) + } suspend fun checkAndSave(text: String): Result { - return NetworkDataSource.checkAuth(text).onSuccess { success -> - if (success) { - codeCache = text + return withContext(Dispatchers.IO) { + NetworkDataSource.checkAuth(code = text).onSuccess { success -> + if (success) { + saveCode(text) + } } } } + + private fun saveCode(code: String) { + codeCache = code + prefs.edit().putString(KEY_AUTH_CODE, code).apply() + } + + fun getSavedCode(): String? = codeCache + + suspend fun clearSavedCode() { + codeCache = null + withContext(Dispatchers.IO) { + prefs.edit().remove(KEY_AUTH_CODE).apply() + } + } } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/root/RootActivity.kt b/app/src/main/java/ru/myitschool/work/ui/root/RootActivity.kt index 54b156d..91732f5 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 @@ -3,25 +3,38 @@ package ru.myitschool.work.ui.root import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.activity.enableEdgeToEdge import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material3.Scaffold import androidx.compose.ui.Modifier +import androidx.core.view.WindowCompat +import ru.myitschool.work.data.repo.AuthRepository +import ru.myitschool.work.ui.nav.AuthScreenDestination +import ru.myitschool.work.ui.nav.MainScreenDestination import ru.myitschool.work.ui.screen.AppNavHost import ru.myitschool.work.ui.theme.WorkTheme class RootActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - enableEdgeToEdge() + + // Определяем стартовый экран + val startDestination = if (AuthRepository.getSavedCode() != null) { + MainScreenDestination + } else { + AuthScreenDestination + } + + WindowCompat.setDecorFitsSystemWindows(window, false) + setContent { WorkTheme { Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> AppNavHost( modifier = Modifier .fillMaxSize() - .padding(innerPadding) + .padding(innerPadding), + startDestination = startDestination ) } } 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..aee4054 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 @@ -11,38 +11,39 @@ import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController +import ru.myitschool.work.ui.nav.AppDestination 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.screen.auth.AuthScreen +import ru.myitschool.work.ui.screen.main.MainScreen @Composable fun AppNavHost( modifier: Modifier = Modifier, - navController: NavHostController = rememberNavController() + navController: NavHostController = rememberNavController(), + startDestination: AppDestination = AuthScreenDestination ) { NavHost( modifier = modifier, enterTransition = { EnterTransition.None }, exitTransition = { ExitTransition.None }, navController = navController, - startDestination = AuthScreenDestination, + startDestination = startDestination, ) { composable { AuthScreen(navController = navController) } + composable { - Box( - contentAlignment = Alignment.Center - ) { - Text(text = "Hello") - } + MainScreen(navController = navController) } + composable { Box( contentAlignment = Alignment.Center ) { - Text(text = "Hello") + Text(text = "Экран бронирования (будет реализован позже)") } } } 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..bd3e2f6 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,29 +1,14 @@ package ru.myitschool.work.ui.screen.auth -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.padding -import androidx.compose.foundation.layout.size -import androidx.compose.material3.Button -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.material3.TextField -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.* +import androidx.compose.runtime.* 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.res.stringResource +import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel @@ -38,6 +23,7 @@ fun AuthScreen( navController: NavController ) { val state by viewModel.uiState.collectAsState() + val error by viewModel.error.collectAsState() LaunchedEffect(Unit) { viewModel.actionFlow.collect { @@ -57,13 +43,14 @@ fun AuthScreen( style = MaterialTheme.typography.headlineSmall, textAlign = TextAlign.Center ) - when (val currentState = state) { - is AuthState.Data -> Content(viewModel, currentState) - is AuthState.Loading -> { - CircularProgressIndicator( - modifier = Modifier.size(64.dp) - ) - } + + // Исправление: Loading теперь внутри AuthState.Data + if (state is AuthState.Data && (state as AuthState.Data).isLoading) { + CircularProgressIndicator( + modifier = Modifier.size(64.dp) + ) + } else if (state is AuthState.Data) { + Content(viewModel, state as AuthState.Data, error) } } } @@ -71,26 +58,44 @@ fun AuthScreen( @Composable private fun Content( viewModel: AuthViewModel, - state: AuthState.Data + state: AuthState.Data, + error: String? ) { - var inputText by remember { mutableStateOf("") } Spacer(modifier = Modifier.size(16.dp)) + + // Показать ошибку, если есть + if (error != null) { + Text( + text = error, + color = MaterialTheme.colorScheme.error, + modifier = Modifier + .testTag(TestIds.Auth.ERROR) + .fillMaxWidth() + .padding(bottom = 8.dp), + textAlign = TextAlign.Center + ) + } + TextField( modifier = Modifier.testTag(TestIds.Auth.CODE_INPUT).fillMaxWidth(), - value = inputText, + value = state.inputText, onValueChange = { - inputText = it viewModel.onIntent(AuthIntent.TextInput(it)) }, - label = { Text(stringResource(R.string.auth_label)) } + label = { Text(stringResource(R.string.auth_label)) }, + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text), + isError = error != null ) + Spacer(modifier = Modifier.size(16.dp)) + Button( modifier = Modifier.testTag(TestIds.Auth.SIGN_BUTTON).fillMaxWidth(), onClick = { - viewModel.onIntent(AuthIntent.Send(inputText)) + viewModel.onIntent(AuthIntent.Send(state.inputText)) }, - enabled = true + enabled = state.isValid && !state.isLoading // Добавил проверку валидности ) { 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..4274e2d 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 @@ -1,6 +1,10 @@ package ru.myitschool.work.ui.screen.auth sealed interface AuthState { - object Loading: AuthState - object Data: AuthState + data class Data( + val inputText: String = "", + val error: String? = null, + val isValid: Boolean = false, + val isLoading: Boolean = false + ) : 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..72d8452 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 @@ -3,41 +3,72 @@ package ru.myitschool.work.ui.screen.auth import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update +import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import ru.myitschool.work.data.repo.AuthRepository import ru.myitschool.work.domain.auth.CheckAndSaveAuthCodeUseCase class AuthViewModel : ViewModel() { - private val checkAndSaveAuthCodeUseCase by lazy { CheckAndSaveAuthCodeUseCase(AuthRepository) } - private val _uiState = MutableStateFlow(AuthState.Data) + + private val checkAndSaveAuthCodeUseCase by lazy { + CheckAndSaveAuthCodeUseCase(AuthRepository) + } + + private val _uiState = MutableStateFlow( + AuthState.Data(inputText = "", isValid = false, isLoading = false) + ) val uiState: StateFlow = _uiState.asStateFlow() private val _actionFlow: MutableSharedFlow = MutableSharedFlow() - val actionFlow: SharedFlow = _actionFlow + val actionFlow: SharedFlow = _actionFlow.asSharedFlow() + + private val _error = MutableStateFlow(null) + val error: StateFlow = _error.asStateFlow() fun onIntent(intent: AuthIntent) { when (intent) { is AuthIntent.Send -> { - viewModelScope.launch(Dispatchers.Default) { - _uiState.update { AuthState.Loading } - checkAndSaveAuthCodeUseCase.invoke("9999").fold( + viewModelScope.launch(Dispatchers.IO) { + _uiState.update { + if (it is AuthState.Data) it.copy(isLoading = true) else it + } + + checkAndSaveAuthCodeUseCase.invoke(intent.text).fold( onSuccess = { _actionFlow.emit(Unit) + _error.value = null }, onFailure = { error -> - error.printStackTrace() - _actionFlow.emit(Unit) + _error.value = error.message ?: "Ошибка авторизации" + _uiState.update { + if (it is AuthState.Data) it.copy(isLoading = false) else it + } } ) } } - is AuthIntent.TextInput -> Unit + + is AuthIntent.TextInput -> { + if (_error.value != null) { + _error.value = null + } + + val isValid = isValidCode(intent.text) + _uiState.update { + if (it is AuthState.Data) it.copy( + inputText = intent.text, + isValid = isValid + ) else it + } + } + } + } + + private fun isValidCode(text: String): Boolean { + if (text.isEmpty() || text.length != 4) return false + + return text.all { char -> + char in 'A'..'Z' || char in 'a'..'z' || char in '0'..'9' } } } \ 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..1ee8ede --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainScreen.kt @@ -0,0 +1,94 @@ +package ru.myitschool.work.ui.screen.main + +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import ru.myitschool.work.R +import ru.myitschool.work.ui.nav.AuthScreenDestination + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MainScreen( + viewModel: MainViewModel = viewModel(), + navController: NavController +) { + val state by viewModel.uiState.collectAsState() + + // При первом открытии загружаем данные + LaunchedEffect(key1 = Unit) { + viewModel.loadUserInfo() + } + + // Проверяем авторизацию + LaunchedEffect(key1 = viewModel.isUserAuthorized) { + if (!viewModel.isUserAuthorized) { + navController.navigate(AuthScreenDestination) { + popUpTo(0) + } + } + } + + Scaffold( + topBar = { + TopAppBar( + title = { Text(text = "Главная") } + ) + } + ) { paddingValues -> + Box( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + ) { + when (state) { + is MainState.Loading -> { + CircularProgressIndicator( + modifier = Modifier.align(Alignment.Center) + ) + } + + is MainState.Error -> { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = (state as MainState.Error).message, + color = MaterialTheme.colorScheme.error, + modifier = Modifier.testTag("main_error"), + textAlign = TextAlign.Center + ) + } + } + + is MainState.Success -> { + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = "Главный экран", + style = MaterialTheme.typography.headlineMedium + ) + Text( + text = "Здесь будет информация о пользователе и бронированиях", + modifier = Modifier.padding(top = 16.dp) + ) + } + } + } + } + } +} \ 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..3c8d3c8 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainViewModel.kt @@ -0,0 +1,70 @@ +package ru.myitschool.work.ui.screen.main + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import ru.myitschool.work.data.repo.AuthRepository + +class MainViewModel : ViewModel() { + + private val _uiState = MutableStateFlow(MainState.Loading) + val uiState: StateFlow = _uiState.asStateFlow() + + val isUserAuthorized: Boolean + get() = AuthRepository.getSavedCode() != null + + fun loadUserInfo() { + viewModelScope.launch { + _uiState.value = MainState.Loading + + try { + val code = AuthRepository.getSavedCode() + if (code == null) { + _uiState.value = MainState.Error("Пользователь не авторизован") + return@launch + } + + // TODO: Реальный запрос к API + // Пока заглушка + _uiState.value = MainState.Success( + userInfo = UserInfo("Иван Иванов", ""), + bookings = emptyList() + ) + + } catch (e: Exception) { + _uiState.value = MainState.Error("Ошибка загрузки: ${e.message}") + } + } + } + + fun logout() { + viewModelScope.launch { + AuthRepository.clearSavedCode() + } + } + + fun refresh() { + loadUserInfo() + } +} + +sealed interface MainState { + object Loading : MainState + data class Error(val message: String) : MainState + data class Success( + val userInfo: UserInfo, + val bookings: List + ) : MainState +} + +// Временные модели (потом перенесем в data/model) +data class UserInfo( + val name: String, + val photoUrl: String +) + +data class Booking( + val date: String, + val place: String +) \ No newline at end of file