diff --git a/app/src/main/java/ru/myitschool/work/App.kt b/app/src/main/java/ru/myitschool/work/App.kt index 72e003b..b505ed4 100644 --- a/app/src/main/java/ru/myitschool/work/App.kt +++ b/app/src/main/java/ru/myitschool/work/App.kt @@ -2,6 +2,7 @@ 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() { @@ -16,9 +17,13 @@ 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 new file mode 100644 index 0000000..5022805 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/data/DataStoreManager.kt @@ -0,0 +1,36 @@ +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 d0d17e2..cb97fb8 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,9 +2,13 @@ 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) { @@ -46,7 +50,9 @@ class AuthRepository private constructor(context: Context) { userCache = UserCache(name, photo) _isAuthorized.value = true } else { - clear() + CoroutineScope(Dispatchers.IO).launch { + clear() + } } } @@ -60,9 +66,10 @@ 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}") } } @@ -78,13 +85,16 @@ class AuthRepository private constructor(context: Context) { fun getUserInfo(): UserCache? = userCache - fun clear() { + suspend 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 new file mode 100644 index 0000000..675020e --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/domain/LoadDataUseCase.kt @@ -0,0 +1,33 @@ +// 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 new file mode 100644 index 0000000..0497f45 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/domain/main/entities/BookingInfo.kt @@ -0,0 +1,7 @@ +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 new file mode 100644 index 0000000..417edd5 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/domain/main/entities/UserEntity.kt @@ -0,0 +1,32 @@ +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 354c37d..084c0cc 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 ee30e3c..c7ecb91 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,6 +1,7 @@ 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 @@ -11,29 +12,30 @@ 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.Card -import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ButtonDefaults 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.graphics.Color +import androidx.compose.ui.draw.clip 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 @@ -46,235 +48,272 @@ import ru.myitschool.work.ui.nav.BookScreenDestination @Composable fun MainScreen( - viewModel: MainViewModel = viewModel(factory = MainViewModelFactory(LocalContext.current)), - navController: NavController + navController: NavController, + viewModel: MainViewModel = viewModel() ) { 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.NavigateToAuth -> { + when(action) { + is MainAction.Auth -> { navController.navigate(AuthScreenDestination) { popUpTo(0) } } - is MainAction.NavigateToBooking -> { - navController.navigate(BookScreenDestination) - } + is MainAction.Booking -> navController.navigate(BookScreenDestination) } } } - Box( - modifier = Modifier.fillMaxSize() - ) { - when (val currentState = state) { - is MainState.Loading -> { + when(state) { + is MainState.Loading -> { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxSize() + ) { CircularProgressIndicator( - modifier = Modifier.align(Alignment.Center) + modifier = Modifier.size(64.dp) ) } - 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) - ) - } + } + is MainState.Error -> { + ErrorContent(viewModel) + } + is MainState.Data -> { + DataContent( + viewModel, + userData = (state as MainState.Data).userData + ) + } + } +} - Spacer(modifier = Modifier.size(16.dp)) +@Composable +fun ErrorContent(viewModel: MainViewModel){ + val configuration = LocalConfiguration.current - 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 - ) - } - } - } - } + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxSize() + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .padding(15.dp) + .fillMaxSize() + ) { + Spacer(modifier = Modifier.height(80.dp)) - 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)) - } + 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 + ) - Button( - onClick = { viewModel.onIntent(MainIntent.Refresh) }, - modifier = Modifier - .weight(1f) - .testTag(TestIds.Main.REFRESH_BUTTON) - ) { - Text(stringResource(R.string.main_refresh)) - } + Spacer(modifier = Modifier.height(20.dp)) - 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 - ) - } - } - } - } + 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 + ) + ) { + Text(stringResource(R.string.main_refresh)) } } } } @Composable -private fun BookingItem( - booking: BookingItem, - position: Int, - modifier: Modifier = Modifier +fun DataContent( + viewModel: MainViewModel, + userData: ru.myitschool.work.domain.main.entities.UserEntity ) { - Box( - modifier = modifier + val configuration = LocalConfiguration.current + + Column ( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.surfaceVariant) ) { - Card( - modifier = Modifier.fillMaxWidth(), - elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .clip(RoundedCornerShape(bottomStart = 16.dp, bottomEnd = 16.dp)) + .background(MaterialTheme.colorScheme.primary) + .fillMaxWidth() + .padding(16.dp) ) { - Column( - modifier = Modifier.padding(16.dp) + Row( + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth() ) { - 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) + 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)) + } + + 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)) + } + } + + 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 +) { + 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 + ) { + Text( + text = "У вас нет активных бронирований", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } } \ 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 7ccb5be..c1532e3 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,12 +5,8 @@ import java.time.format.DateTimeFormatter sealed interface MainState { object Loading : MainState - data class Data( - val userName: String = "", - val userPhotoUrl: String? = null, - val bookings: List = emptyList(), - val error: String? = null - ) : MainState + data class Data(val userData: ru.myitschool.work.domain.main.entities.UserEntity) : MainState + object Error : 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 a680393..e144e94 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,112 +1,97 @@ package ru.myitschool.work.ui.screen.main -import android.content.Context -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider +import android.app.Application +import androidx.lifecycle.AndroidViewModel 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 java.time.LocalDate -import java.time.format.DateTimeFormatter +import ru.myitschool.work.App 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() } - fun onIntent(intent: MainIntent) { - when (intent) { - MainIntent.Logout -> { - authRepo.clear() - viewModelScope.launch { - _actionFlow.emit(MainAction.NavigateToAuth) - } - } - MainIntent.Refresh -> { - loadData() - } - MainIntent.AddBooking -> { - viewModelScope.launch { - _actionFlow.emit(MainAction.NavigateToBooking) - } - } - is MainIntent.ItemClick -> { - } - } - } - private fun loadData() { - viewModelScope.launch { + viewModelScope.launch(Dispatchers.IO) { _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) + try { + val userCode = dataStoreManager.getUserCode().first() - _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 ?: "Ошибка загрузки данных" - ) - } + if (userCode.isEmpty()) { + _actionFlow.emit(MainAction.Auth) + return@launch } - ) + + 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 } + } } } -} -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 + fun onIntent(intent: MainIntent) { + when(intent) { + is MainIntent.LoadData -> loadData() + is MainIntent.Booking -> { + viewModelScope.launch(Dispatchers.Default) { + _actionFlow.emit(MainAction.Booking) + } + } + is MainIntent.Logout -> { + viewModelScope.launch(Dispatchers.IO) { + authRepository.clear() + _actionFlow.emit(MainAction.Auth) + } + } } - throw IllegalArgumentException("Unknown ViewModel class") } } sealed interface MainAction { - object NavigateToAuth : MainAction - object NavigateToBooking : MainAction + object Auth : MainAction + object Booking : 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 e2ff56b..13ff09d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -13,4 +13,10 @@ Забронировать Повторить Всё забронировано + Ошибка загрузки данных + Обновить + Мои бронирования + У вас нет активных бронирований + Аватар пользователя + Добавить \ No newline at end of file