diff --git a/app/src/main/java/ru/myitschool/work/App.kt b/app/src/main/java/ru/myitschool/work/App.kt index b505ed4..72e003b 100644 --- a/app/src/main/java/ru/myitschool/work/App.kt +++ b/app/src/main/java/ru/myitschool/work/App.kt @@ -2,7 +2,6 @@ package ru.myitschool.work import android.app.Application import android.content.Context -import ru.myitschool.work.data.DataStoreManager import ru.myitschool.work.data.repo.AuthRepository class App : Application() { @@ -17,13 +16,9 @@ class App : Application() { } } - lateinit var dataStoreManager: DataStoreManager - private set - override fun onCreate() { super.onCreate() instance = this - dataStoreManager = DataStoreManager(applicationContext) AuthRepository.getInstance(applicationContext) } diff --git a/app/src/main/java/ru/myitschool/work/data/DataStoreManager.kt b/app/src/main/java/ru/myitschool/work/data/DataStoreManager.kt deleted file mode 100644 index 5022805..0000000 --- a/app/src/main/java/ru/myitschool/work/data/DataStoreManager.kt +++ /dev/null @@ -1,36 +0,0 @@ -package ru.myitschool.work.data - -import android.content.Context -import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.Preferences -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.map - -private val Context.dataStore: DataStore by preferencesDataStore(name = "user_prefs") - -class DataStoreManager(context: Context) { - private val dataStore = context.dataStore - - private object Keys { - val USER_CODE = stringPreferencesKey("user_code") - } - - suspend fun saveUserCode(code: String) { - dataStore.edit { prefs -> - prefs[Keys.USER_CODE] = code - } - } - - suspend fun clearUserCode() { - dataStore.edit { prefs -> - prefs.remove(Keys.USER_CODE) - } - } - - fun getUserCode(): Flow = dataStore.data.map { prefs -> - prefs[Keys.USER_CODE] ?: "" - } -} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/data/repo/AuthRepository.kt b/app/src/main/java/ru/myitschool/work/data/repo/AuthRepository.kt index cb97fb8..d0d17e2 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 @@ -2,13 +2,9 @@ package ru.myitschool.work.data.repo import android.content.Context import android.content.Context.MODE_PRIVATE -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.launch -import ru.myitschool.work.App import ru.myitschool.work.data.source.NetworkDataSource class AuthRepository private constructor(context: Context) { @@ -50,9 +46,7 @@ class AuthRepository private constructor(context: Context) { userCache = UserCache(name, photo) _isAuthorized.value = true } else { - CoroutineScope(Dispatchers.IO).launch { - clear() - } + clear() } } @@ -66,10 +60,9 @@ class AuthRepository private constructor(context: Context) { getPrefs().edit() .putString(KEY_CODE, text) .apply() - - val app = context.applicationContext as App - app.dataStoreManager.saveUserCode(text) } + }.onFailure { exception -> + println("Auth error: ${exception.message}") } } @@ -85,16 +78,13 @@ class AuthRepository private constructor(context: Context) { fun getUserInfo(): UserCache? = userCache - suspend fun clear() { + fun clear() { codeCache = null userCache = null _isAuthorized.value = false getPrefs().edit() .clear() .apply() - - val app = context.applicationContext as App - app.dataStoreManager.clearUserCode() } } diff --git a/app/src/main/java/ru/myitschool/work/domain/LoadDataUseCase.kt b/app/src/main/java/ru/myitschool/work/domain/LoadDataUseCase.kt deleted file mode 100644 index 675020e..0000000 --- a/app/src/main/java/ru/myitschool/work/domain/LoadDataUseCase.kt +++ /dev/null @@ -1,33 +0,0 @@ -// domain/main/LoadDataUseCase.kt -package ru.myitschool.work.domain.main - -import ru.myitschool.work.data.model.UserInfoResponse -import ru.myitschool.work.data.repo.MainRepository -import ru.myitschool.work.domain.main.entities.BookingInfo -import ru.myitschool.work.domain.main.entities.UserEntity - -class LoadDataUseCase( - private val repository: ru.myitschool.work.data.repo.MainRepository -) { - suspend operator fun invoke(userCode: String): Result { - return repository.getUserInfo().map { userInfoResponse -> - mapToUserEntity(userInfoResponse) - } - } - - private fun mapToUserEntity(response: UserInfoResponse): UserEntity { - val bookings = response.bookings.map { bookingResponse -> - BookingInfo( - date = bookingResponse.date, - place = bookingResponse.place, - id = bookingResponse.bookingId - ) - } - - return UserEntity( - name = response.name, - photoUrl = response.photoUrl, - booking = bookings - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/domain/main/entities/BookingInfo.kt b/app/src/main/java/ru/myitschool/work/domain/main/entities/BookingInfo.kt deleted file mode 100644 index 0497f45..0000000 --- a/app/src/main/java/ru/myitschool/work/domain/main/entities/BookingInfo.kt +++ /dev/null @@ -1,7 +0,0 @@ -package ru.myitschool.work.domain.main.entities - -data class BookingInfo( - val date: String, - val place: String, - val id: Int -) \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/domain/main/entities/UserEntity.kt b/app/src/main/java/ru/myitschool/work/domain/main/entities/UserEntity.kt deleted file mode 100644 index 417edd5..0000000 --- a/app/src/main/java/ru/myitschool/work/domain/main/entities/UserEntity.kt +++ /dev/null @@ -1,32 +0,0 @@ -package ru.myitschool.work.domain.main.entities - -import java.time.LocalDate -import java.time.format.DateTimeFormatter - -data class UserEntity( - val name: String, - val photoUrl: String?, - val booking: List -) { - fun hasBookings(): Boolean = booking.isNotEmpty() - - fun getSortedBookingsWithFormattedDate(): List>? { - if (booking.isEmpty()) return null - - return booking.sortedBy { it.date } - .map { booking -> - val originalDate = booking.date - val formattedDate = formatDate(originalDate) - Triple(originalDate, formattedDate, booking) - } - } - - private fun formatDate(dateStr: String): String { - return try { - val date = LocalDate.parse(dateStr, DateTimeFormatter.ISO_DATE) - date.format(DateTimeFormatter.ofPattern("dd.MM.yyyy")) - } catch (e: Exception) { - dateStr - } - } -} \ 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 084c0cc..354c37d 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 @@ -1,8 +1,8 @@ package ru.myitschool.work.ui.screen.main - sealed interface MainIntent { - object LoadData : MainIntent - object Booking : MainIntent object Logout : MainIntent + object Refresh : MainIntent + object AddBooking : MainIntent + data class ItemClick(val position: Int) : 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 c7ecb91..ee30e3c 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,7 +1,6 @@ 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 @@ -12,30 +11,29 @@ 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.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource 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 @@ -48,272 +46,235 @@ import ru.myitschool.work.ui.nav.BookScreenDestination @Composable fun MainScreen( - navController: NavController, - viewModel: MainViewModel = viewModel() + viewModel: MainViewModel = viewModel(factory = MainViewModelFactory(LocalContext.current)), + navController: NavController ) { val state by viewModel.uiState.collectAsState() + val shouldRefresh by navController.currentBackStackEntry + ?.savedStateHandle + ?.getStateFlow("shouldRefresh", false) + ?.collectAsState() ?: remember { mutableStateOf(false) } + + LaunchedEffect(shouldRefresh) { + if (shouldRefresh) { + viewModel.onIntent(MainIntent.Refresh) + navController.currentBackStackEntry?.savedStateHandle?.remove("shouldRefresh") + } + } + LaunchedEffect(Unit) { viewModel.actionFlow.collect { action -> - when(action) { - is MainAction.Auth -> { + when (action) { + is MainAction.NavigateToAuth -> { navController.navigate(AuthScreenDestination) { popUpTo(0) } } - is MainAction.Booking -> navController.navigate(BookScreenDestination) + is MainAction.NavigateToBooking -> { + navController.navigate(BookScreenDestination) + } } } } - when(state) { - is MainState.Loading -> { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier.fillMaxSize() - ) { - CircularProgressIndicator( - modifier = Modifier.size(64.dp) - ) - } - } - is MainState.Error -> { - ErrorContent(viewModel) - } - is MainState.Data -> { - DataContent( - viewModel, - userData = (state as MainState.Data).userData - ) - } - } -} - -@Composable -fun ErrorContent(viewModel: MainViewModel){ - val configuration = LocalConfiguration.current - Box( - contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize() ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier - .padding(15.dp) - .fillMaxSize() - ) { - Spacer(modifier = Modifier.height(80.dp)) - - Text( - text = stringResource(R.string.data_error_message), - modifier = Modifier.testTag(TestIds.Main.ERROR), - textAlign = TextAlign.Center, - style = MaterialTheme.typography.headlineSmall, - color = MaterialTheme.colorScheme.error - ) - - Spacer(modifier = Modifier.height(20.dp)) - - Button( - onClick = { viewModel.onIntent(MainIntent.LoadData) }, - modifier = Modifier - .fillMaxWidth() - .testTag(TestIds.Main.REFRESH_BUTTON), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.onPrimary + when (val currentState = state) { + is MainState.Loading -> { + CircularProgressIndicator( + modifier = Modifier.align(Alignment.Center) ) - ) { - Text(stringResource(R.string.main_refresh)) } - } - } -} + is MainState.Data -> { + if (currentState.error != null) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = currentState.error, + color = Color.Red, + modifier = Modifier + .testTag(TestIds.Main.ERROR) + .padding(bottom = 16.dp) + ) + Button( + onClick = { viewModel.onIntent(MainIntent.Refresh) }, + modifier = Modifier.testTag(TestIds.Main.REFRESH_BUTTON) + ) { + Text(stringResource(R.string.main_refresh)) + } + } + } else { + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 16.dp) + ) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + if (!currentState.userPhotoUrl.isNullOrEmpty()) { + Image( + painter = rememberAsyncImagePainter( + ImageRequest.Builder(LocalContext.current) + .data(currentState.userPhotoUrl) + .build() + ), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .size(64.dp) + .testTag(TestIds.Main.PROFILE_IMAGE) + ) + } else { + Icon( + painter = painterResource(id = R.drawable.github), + contentDescription = null, + modifier = Modifier + .size(64.dp) + .testTag(TestIds.Main.PROFILE_IMAGE) + ) + } -@Composable -fun DataContent( - viewModel: MainViewModel, - userData: ru.myitschool.work.domain.main.entities.UserEntity -) { - val configuration = LocalConfiguration.current + Spacer(modifier = Modifier.size(16.dp)) - Column ( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.surfaceVariant) - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier - .clip(RoundedCornerShape(bottomStart = 16.dp, bottomEnd = 16.dp)) - .background(MaterialTheme.colorScheme.primary) - .fillMaxWidth() - .padding(16.dp) - ) { - Row( - horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier.fillMaxWidth() - ) { - Button( - onClick = { viewModel.onIntent(MainIntent.LoadData) }, - modifier = Modifier.testTag(TestIds.Main.REFRESH_BUTTON), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.onPrimary - ) - ) { - Text(stringResource(R.string.main_refresh)) - } + Column { + Text( + text = currentState.userName, + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.testTag(TestIds.Main.PROFILE_NAME) + ) + if (currentState.bookings.isNotEmpty()) { + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = "Забронировано мест: ${currentState.bookings.size}", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } + } - Button( - onClick = { viewModel.onIntent(MainIntent.Logout) }, - modifier = Modifier.testTag(TestIds.Main.LOGOUT_BUTTON), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.onPrimary - ) - ) { - Text(stringResource(R.string.main_logout)) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 16.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Button( + onClick = { viewModel.onIntent(MainIntent.Logout) }, + modifier = Modifier + .weight(1f) + .testTag(TestIds.Main.LOGOUT_BUTTON) + ) { + Text(stringResource(R.string.main_logout)) + } + + Button( + onClick = { viewModel.onIntent(MainIntent.Refresh) }, + modifier = Modifier + .weight(1f) + .testTag(TestIds.Main.REFRESH_BUTTON) + ) { + Text(stringResource(R.string.main_refresh)) + } + + Button( + onClick = { viewModel.onIntent(MainIntent.AddBooking) }, + modifier = Modifier + .weight(1f) + .testTag(TestIds.Main.ADD_BUTTON) + ) { + Text(stringResource(R.string.main_add_booking)) + } + } + + if (currentState.bookings.isNotEmpty()) { + Text( + text = "Мои бронирования:", + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(bottom = 8.dp) + ) + + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + ) { + itemsIndexed(currentState.bookings) { index, booking -> + BookingItem( + booking = booking, + position = index, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp) + .testTag(TestIds.Main.getIdItemByPosition(index)) + ) + } + } + } else { + Box( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + contentAlignment = Alignment.Center + ) { + Text( + text = "У вас нет активных бронирований", + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } } } - - Image( - painter = rememberAsyncImagePainter( - model = ImageRequest.Builder(LocalContext.current) - .data(userData.photoUrl) - .build() - ), - contentDescription = stringResource(R.string.main_avatar_description), - modifier = Modifier - .clip(RoundedCornerShape(999.dp)) - .testTag(TestIds.Main.PROFILE_IMAGE) - .size(150.dp) - .padding(20.dp), - contentScale = ContentScale.Crop - ) - - Text( - text = userData.name, - color = MaterialTheme.colorScheme.onPrimary, - textAlign = TextAlign.Center, - modifier = Modifier - .testTag(TestIds.Main.PROFILE_NAME) - .padding(horizontal = 20.dp), - style = MaterialTheme.typography.headlineSmall - ) - - Spacer(modifier = Modifier.height(20.dp)) - } - - Column( - modifier = Modifier - .fillMaxSize() - .padding(20.dp) - .clip(RoundedCornerShape(16.dp)) - .background(MaterialTheme.colorScheme.surface) - .padding(16.dp) - ) { - Text( - text = "Мои бронирования:", - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.padding(bottom = 16.dp) - ) - - if (userData.hasBookings()) { - SortedBookingList(userData = userData) - } else { - EmptyBookings() - } - - Spacer(modifier = Modifier.weight(1f)) - - Button( - onClick = { viewModel.onIntent(MainIntent.Booking) }, - modifier = Modifier - .testTag(TestIds.Main.ADD_BUTTON) - .fillMaxWidth(), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.onPrimary - ) - ) { - Text(stringResource(R.string.main_add_booking)) - } } } } @Composable -fun SortedBookingList(userData: ru.myitschool.work.domain.main.entities.UserEntity) { - val sortedBookings = remember(userData.booking) { - userData.getSortedBookingsWithFormattedDate()?.sortedBy { (originalDate, _, _) -> - originalDate - } - } - - LazyColumn( - modifier = Modifier - .fillMaxWidth() - ) { - itemsIndexed( - items = sortedBookings ?: emptyList() - ) { index, (originalDate, formattedDate, bookingInfo) -> - Box( - modifier = Modifier.testTag(TestIds.Main.getIdItemByPosition(index)) - ) { - BookingItem( - originalDate = originalDate, - formattedDate = formattedDate, - bookingInfo = bookingInfo, - index = index - ) - } - Spacer(modifier = Modifier.height(8.dp)) - } - } -} - -@Composable -fun BookingItem( - originalDate: String, - formattedDate: String, - bookingInfo: ru.myitschool.work.domain.main.entities.BookingInfo, - index: Int +private fun BookingItem( + booking: BookingItem, + position: Int, + modifier: Modifier = Modifier ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 8.dp), - horizontalArrangement = Arrangement.SpaceBetween - ) { - Text( - text = bookingInfo.place, - modifier = Modifier.testTag(TestIds.Main.ITEM_PLACE), - style = MaterialTheme.typography.bodyMedium - ) - Text( - text = formattedDate, - modifier = Modifier.testTag(TestIds.Main.ITEM_DATE), - style = MaterialTheme.typography.bodyMedium - ) - } -} - -@Composable -fun EmptyBookings() { Box( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - contentAlignment = Alignment.Center + modifier = modifier ) { - Text( - text = "У вас нет активных бронирований", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) + Card( + modifier = Modifier.fillMaxWidth(), + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + ) { + Column( + modifier = Modifier.padding(16.dp) + ) { + Text( + text = booking.place, + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.testTag(TestIds.Main.ITEM_PLACE) + ) + Text( + text = booking.getFormattedDate(), + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.testTag(TestIds.Main.ITEM_DATE) + ) + } + } } } \ 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 index c1532e3..7ccb5be 100644 --- 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 @@ -5,8 +5,12 @@ import java.time.format.DateTimeFormatter sealed interface MainState { object Loading : MainState - data class Data(val userData: ru.myitschool.work.domain.main.entities.UserEntity) : MainState - object Error : MainState + data class Data( + val userName: String = "", + val userPhotoUrl: String? = null, + val bookings: List = emptyList(), + val error: String? = null + ) : MainState } data class BookingItem( 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 e144e94..a680393 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,97 +1,112 @@ package ru.myitschool.work.ui.screen.main -import android.app.Application -import androidx.lifecycle.AndroidViewModel +import android.content.Context +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider 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.first import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import ru.myitschool.work.App +import java.time.LocalDate +import java.time.format.DateTimeFormatter import ru.myitschool.work.data.repo.AuthRepository import ru.myitschool.work.data.repo.MainRepository -import ru.myitschool.work.domain.main.LoadDataUseCase - -class MainViewModel(application: Application) : AndroidViewModel(application) { - - private val dataStoreManager by lazy { - (getApplication() as App).dataStoreManager - } - - private val authRepository by lazy { - AuthRepository.getInstance(getApplication()) - } - - private val mainRepository by lazy { - MainRepository(authRepository) - } - - private val loadDataUseCase by lazy { - LoadDataUseCase(mainRepository) - } +class MainViewModel( + private val authRepo: AuthRepository, + private val mainRepo: MainRepository +) : ViewModel() { private val _uiState = MutableStateFlow(MainState.Loading) val uiState: StateFlow = _uiState.asStateFlow() private val _actionFlow: MutableSharedFlow = MutableSharedFlow() val actionFlow: SharedFlow = _actionFlow + private val dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") + init { loadData() } - private fun loadData() { - viewModelScope.launch(Dispatchers.IO) { - _uiState.update { MainState.Loading } - - try { - val userCode = dataStoreManager.getUserCode().first() - - if (userCode.isEmpty()) { - _actionFlow.emit(MainAction.Auth) - return@launch + fun onIntent(intent: MainIntent) { + when (intent) { + MainIntent.Logout -> { + authRepo.clear() + viewModelScope.launch { + _actionFlow.emit(MainAction.NavigateToAuth) } - - loadDataUseCase.invoke(userCode).fold( - onSuccess = { data -> - _uiState.update { MainState.Data(data) } - }, - onFailure = { error -> - error.printStackTrace() - _uiState.update { MainState.Error } - } - ) - } catch (error: Exception) { - error.printStackTrace() - _uiState.update { MainState.Error } + } + MainIntent.Refresh -> { + loadData() + } + MainIntent.AddBooking -> { + viewModelScope.launch { + _actionFlow.emit(MainAction.NavigateToBooking) + } + } + is MainIntent.ItemClick -> { } } } - fun onIntent(intent: MainIntent) { - when(intent) { - is MainIntent.LoadData -> loadData() - is MainIntent.Booking -> { - viewModelScope.launch(Dispatchers.Default) { - _actionFlow.emit(MainAction.Booking) + private fun loadData() { + viewModelScope.launch { + _uiState.update { MainState.Loading } + mainRepo.getUserInfo().fold( + onSuccess = { userInfo -> + val bookings = userInfo.bookings.mapNotNull { bookingResponse -> + try { + BookingItem( + id = bookingResponse.bookingId.toString(), + date = LocalDate.parse(bookingResponse.date, dateFormatter), + place = bookingResponse.place + ) + } catch (e: Exception) { + null + } + }.sortedBy { it.date } + + authRepo.saveUserInfo(userInfo.name, userInfo.photoUrl) + + _uiState.update { + MainState.Data( + userName = userInfo.name, + userPhotoUrl = userInfo.photoUrl, + bookings = bookings + ) + } + }, + onFailure = { error -> + _uiState.update { + MainState.Data( + userName = "", + userPhotoUrl = null, + bookings = emptyList(), + error = error.message ?: "Ошибка загрузки данных" + ) + } } - } - is MainIntent.Logout -> { - viewModelScope.launch(Dispatchers.IO) { - authRepository.clear() - _actionFlow.emit(MainAction.Auth) - } - } + ) } } } +class MainViewModelFactory(private val context: Context) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + if (modelClass.isAssignableFrom(MainViewModel::class.java)) { + val authRepository = AuthRepository.getInstance(context) + val mainRepository = MainRepository(authRepository) + return MainViewModel(authRepository, mainRepository) as T + } + throw IllegalArgumentException("Unknown ViewModel class") + } +} + sealed interface MainAction { - object Auth : MainAction - object Booking : MainAction + object NavigateToAuth : MainAction + object NavigateToBooking : MainAction } \ 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 13ff09d..e2ff56b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -13,10 +13,4 @@ Забронировать Повторить Всё забронировано - Ошибка загрузки данных - Обновить - Мои бронирования - У вас нет активных бронирований - Аватар пользователя - Добавить \ No newline at end of file