forked from Olympic/NTO-2025-Android-TeamTask
fix
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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<Preferences> 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<String> = dataStore.data.map { prefs ->
|
||||
prefs[Keys.USER_CODE] ?: ""
|
||||
}
|
||||
}
|
||||
@@ -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,9 +50,11 @@ class AuthRepository private constructor(context: Context) {
|
||||
userCache = UserCache(name, photo)
|
||||
_isAuthorized.value = true
|
||||
} else {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getPrefs() = context.getSharedPreferences(PREFS_NAME, MODE_PRIVATE)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<UserEntity> {
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package ru.myitschool.work.domain.main.entities
|
||||
|
||||
data class BookingInfo(
|
||||
val date: String,
|
||||
val place: String,
|
||||
val id: Int
|
||||
)
|
||||
@@ -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<BookingInfo>
|
||||
) {
|
||||
fun hasBookings(): Boolean = booking.isNotEmpty()
|
||||
|
||||
fun getSortedBookingsWithFormattedDate(): List<Triple<String, String, BookingInfo>>? {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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<Boolean>("shouldRefresh", false)
|
||||
?.collectAsState() ?: remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(shouldRefresh) {
|
||||
if (shouldRefresh) {
|
||||
viewModel.onIntent(MainIntent.Refresh)
|
||||
navController.currentBackStackEntry?.savedStateHandle?.remove<Boolean>("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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
when(state) {
|
||||
is MainState.Loading -> {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
when (val currentState = state) {
|
||||
is MainState.Loading -> {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
modifier = Modifier.size(64.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
is MainState.Error -> {
|
||||
ErrorContent(viewModel)
|
||||
}
|
||||
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)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
|
||||
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
|
||||
DataContent(
|
||||
viewModel,
|
||||
userData = (state as MainState.Data).userData
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
@Composable
|
||||
fun ErrorContent(viewModel: MainViewModel){
|
||||
val configuration = LocalConfiguration.current
|
||||
|
||||
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(
|
||||
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()
|
||||
.weight(1f),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "У вас нет активных бронирований",
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
.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
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
||||
val configuration = LocalConfiguration.current
|
||||
|
||||
Column (
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp)
|
||||
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))
|
||||
}
|
||||
|
||||
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 = booking.place,
|
||||
text = "Мои бронирования:",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier = Modifier.testTag(TestIds.Main.ITEM_PLACE)
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.padding(bottom = 16.dp)
|
||||
)
|
||||
Text(
|
||||
text = booking.getFormattedDate(),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.testTag(TestIds.Main.ITEM_DATE)
|
||||
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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<BookingItem> = 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(
|
||||
|
||||
@@ -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>(MainState.Loading)
|
||||
val uiState: StateFlow<MainState> = _uiState.asStateFlow()
|
||||
|
||||
private val _actionFlow: MutableSharedFlow<MainAction> = MutableSharedFlow()
|
||||
val actionFlow: SharedFlow<MainAction> = _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 }
|
||||
val userCode = dataStoreManager.getUserCode().first()
|
||||
|
||||
authRepo.saveUserInfo(userInfo.name, userInfo.photoUrl)
|
||||
|
||||
_uiState.update {
|
||||
MainState.Data(
|
||||
userName = userInfo.name,
|
||||
userPhotoUrl = userInfo.photoUrl,
|
||||
bookings = bookings
|
||||
)
|
||||
if (userCode.isEmpty()) {
|
||||
_actionFlow.emit(MainAction.Auth)
|
||||
return@launch
|
||||
}
|
||||
|
||||
loadDataUseCase.invoke(userCode).fold(
|
||||
onSuccess = { data ->
|
||||
_uiState.update { MainState.Data(data) }
|
||||
},
|
||||
onFailure = { error ->
|
||||
_uiState.update {
|
||||
MainState.Data(
|
||||
userName = "",
|
||||
userPhotoUrl = null,
|
||||
bookings = emptyList(),
|
||||
error = error.message ?: "Ошибка загрузки данных"
|
||||
)
|
||||
}
|
||||
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 <T : ViewModel> create(modelClass: Class<T>): 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
|
||||
}
|
||||
@@ -13,4 +13,10 @@
|
||||
<string name="book_book">Забронировать</string>
|
||||
<string name="book_refresh">Повторить</string>
|
||||
<string name="book_empty">Всё забронировано</string>
|
||||
<string name="data_error_message">Ошибка загрузки данных</string>
|
||||
<string name="main_update">Обновить</string>
|
||||
<string name="main_booking_title">Мои бронирования</string>
|
||||
<string name="main_empty_booking">У вас нет активных бронирований</string>
|
||||
<string name="main_avatar_description">Аватар пользователя</string>
|
||||
<string name="add_icon_description">Добавить</string>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user