authscreen-changed

This commit is contained in:
v3less11
2025-12-10 17:26:04 +03:00
parent 945b9d347d
commit 5a79376482
9 changed files with 314 additions and 67 deletions

View File

@@ -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 {

View File

@@ -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<Boolean> {
return NetworkDataSource.checkAuth(text).onSuccess { success ->
return withContext(Dispatchers.IO) {
NetworkDataSource.checkAuth(code = text).onSuccess { success ->
if (success) {
codeCache = text
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()
}
}
}

View File

@@ -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
)
}
}

View File

@@ -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<AuthScreenDestination> {
AuthScreen(navController = navController)
}
composable<MainScreenDestination> {
Box(
contentAlignment = Alignment.Center
) {
Text(text = "Hello")
}
MainScreen(navController = navController)
}
composable<BookScreenDestination> {
Box(
contentAlignment = Alignment.Center
) {
Text(text = "Hello")
Text(text = "Экран бронирования (будет реализован позже)")
}
}
}

View File

@@ -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 -> {
// Исправление: 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))
}

View File

@@ -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
}

View File

@@ -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>(AuthState.Data)
private val checkAndSaveAuthCodeUseCase by lazy {
CheckAndSaveAuthCodeUseCase(AuthRepository)
}
private val _uiState = MutableStateFlow<AuthState>(
AuthState.Data(inputText = "", isValid = false, isLoading = false)
)
val uiState: StateFlow<AuthState> = _uiState.asStateFlow()
private val _actionFlow: MutableSharedFlow<Unit> = MutableSharedFlow()
val actionFlow: SharedFlow<Unit> = _actionFlow
val actionFlow: SharedFlow<Unit> = _actionFlow.asSharedFlow()
private val _error = MutableStateFlow<String?>(null)
val error: StateFlow<String?> = _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'
}
}
}

View File

@@ -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)
)
}
}
}
}
}
}

View File

@@ -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>(MainState.Loading)
val uiState: StateFlow<MainState> = _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<Booking>
) : MainState
}
// Временные модели (потом перенесем в data/model)
data class UserInfo(
val name: String,
val photoUrl: String
)
data class Booking(
val date: String,
val place: String
)