forked from Olympic/NTO-2025-Android-TeamTask
@@ -2,7 +2,6 @@ package ru.myitschool.work
|
|||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import ru.myitschool.work.data.DataStoreManager
|
|
||||||
import ru.myitschool.work.data.repo.AuthRepository
|
import ru.myitschool.work.data.repo.AuthRepository
|
||||||
|
|
||||||
class App : Application() {
|
class App : Application() {
|
||||||
@@ -17,13 +16,9 @@ class App : Application() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lateinit var dataStoreManager: DataStoreManager
|
|
||||||
private set
|
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
instance = this
|
instance = this
|
||||||
dataStoreManager = DataStoreManager(applicationContext)
|
|
||||||
|
|
||||||
AuthRepository.getInstance(applicationContext)
|
AuthRepository.getInstance(applicationContext)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<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,13 +2,9 @@ package ru.myitschool.work.data.repo
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Context.MODE_PRIVATE
|
import android.content.Context.MODE_PRIVATE
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import ru.myitschool.work.App
|
|
||||||
import ru.myitschool.work.data.source.NetworkDataSource
|
import ru.myitschool.work.data.source.NetworkDataSource
|
||||||
|
|
||||||
class AuthRepository private constructor(context: Context) {
|
class AuthRepository private constructor(context: Context) {
|
||||||
@@ -50,11 +46,9 @@ class AuthRepository private constructor(context: Context) {
|
|||||||
userCache = UserCache(name, photo)
|
userCache = UserCache(name, photo)
|
||||||
_isAuthorized.value = true
|
_isAuthorized.value = true
|
||||||
} else {
|
} else {
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
clear()
|
clear()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun getPrefs() = context.getSharedPreferences(PREFS_NAME, MODE_PRIVATE)
|
private fun getPrefs() = context.getSharedPreferences(PREFS_NAME, MODE_PRIVATE)
|
||||||
|
|
||||||
@@ -66,10 +60,9 @@ class AuthRepository private constructor(context: Context) {
|
|||||||
getPrefs().edit()
|
getPrefs().edit()
|
||||||
.putString(KEY_CODE, text)
|
.putString(KEY_CODE, text)
|
||||||
.apply()
|
.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
|
fun getUserInfo(): UserCache? = userCache
|
||||||
|
|
||||||
suspend fun clear() {
|
fun clear() {
|
||||||
codeCache = null
|
codeCache = null
|
||||||
userCache = null
|
userCache = null
|
||||||
_isAuthorized.value = false
|
_isAuthorized.value = false
|
||||||
getPrefs().edit()
|
getPrefs().edit()
|
||||||
.clear()
|
.clear()
|
||||||
.apply()
|
.apply()
|
||||||
|
|
||||||
val app = context.applicationContext as App
|
|
||||||
app.dataStoreManager.clearUserCode()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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<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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package ru.myitschool.work.domain.main.entities
|
|
||||||
|
|
||||||
data class BookingInfo(
|
|
||||||
val date: String,
|
|
||||||
val place: String,
|
|
||||||
val id: Int
|
|
||||||
)
|
|
||||||
@@ -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<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
|
package ru.myitschool.work.ui.screen.main
|
||||||
|
|
||||||
|
|
||||||
sealed interface MainIntent {
|
sealed interface MainIntent {
|
||||||
object LoadData : MainIntent
|
|
||||||
object Booking : MainIntent
|
|
||||||
object Logout : MainIntent
|
object Logout : MainIntent
|
||||||
|
object Refresh : MainIntent
|
||||||
|
object AddBooking : MainIntent
|
||||||
|
data class ItemClick(val position: Int) : MainIntent
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package ru.myitschool.work.ui.screen.main
|
package ru.myitschool.work.ui.screen.main
|
||||||
|
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
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.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material3.Button
|
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.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
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.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.testTag
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
@@ -48,272 +46,235 @@ import ru.myitschool.work.ui.nav.BookScreenDestination
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MainScreen(
|
fun MainScreen(
|
||||||
navController: NavController,
|
viewModel: MainViewModel = viewModel(factory = MainViewModelFactory(LocalContext.current)),
|
||||||
viewModel: MainViewModel = viewModel()
|
navController: NavController
|
||||||
) {
|
) {
|
||||||
val state by viewModel.uiState.collectAsState()
|
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) {
|
LaunchedEffect(Unit) {
|
||||||
viewModel.actionFlow.collect { action ->
|
viewModel.actionFlow.collect { action ->
|
||||||
when(action) {
|
when (action) {
|
||||||
is MainAction.Auth -> {
|
is MainAction.NavigateToAuth -> {
|
||||||
navController.navigate(AuthScreenDestination) {
|
navController.navigate(AuthScreenDestination) {
|
||||||
popUpTo(0)
|
popUpTo(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is MainAction.Booking -> navController.navigate(BookScreenDestination)
|
is MainAction.NavigateToBooking -> {
|
||||||
|
navController.navigate(BookScreenDestination)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
when(state) {
|
|
||||||
is MainState.Loading -> {
|
|
||||||
Box(
|
Box(
|
||||||
contentAlignment = Alignment.Center,
|
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
|
when (val currentState = state) {
|
||||||
|
is MainState.Loading -> {
|
||||||
CircularProgressIndicator(
|
CircularProgressIndicator(
|
||||||
modifier = Modifier.size(64.dp)
|
modifier = Modifier.align(Alignment.Center)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
is MainState.Error -> {
|
|
||||||
ErrorContent(viewModel)
|
|
||||||
}
|
|
||||||
is MainState.Data -> {
|
is MainState.Data -> {
|
||||||
DataContent(
|
if (currentState.error != null) {
|
||||||
viewModel,
|
|
||||||
userData = (state as MainState.Data).userData
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ErrorContent(viewModel: MainViewModel){
|
|
||||||
val configuration = LocalConfiguration.current
|
|
||||||
|
|
||||||
Box(
|
|
||||||
contentAlignment = Alignment.Center,
|
|
||||||
modifier = Modifier.fillMaxSize()
|
|
||||||
) {
|
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(15.dp)
|
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
.padding(16.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
Spacer(modifier = Modifier.height(80.dp))
|
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.data_error_message),
|
text = currentState.error,
|
||||||
modifier = Modifier.testTag(TestIds.Main.ERROR),
|
color = Color.Red,
|
||||||
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
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.testTag(TestIds.Main.ERROR)
|
||||||
.testTag(TestIds.Main.REFRESH_BUTTON),
|
.padding(bottom = 16.dp)
|
||||||
colors = ButtonDefaults.buttonColors(
|
|
||||||
containerColor = MaterialTheme.colorScheme.primary,
|
|
||||||
contentColor = MaterialTheme.colorScheme.onPrimary
|
|
||||||
)
|
)
|
||||||
|
Button(
|
||||||
|
onClick = { viewModel.onIntent(MainIntent.Refresh) },
|
||||||
|
modifier = Modifier.testTag(TestIds.Main.REFRESH_BUTTON)
|
||||||
) {
|
) {
|
||||||
Text(stringResource(R.string.main_refresh))
|
Text(stringResource(R.string.main_refresh))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun DataContent(
|
|
||||||
viewModel: MainViewModel,
|
|
||||||
userData: ru.myitschool.work.domain.main.entities.UserEntity
|
|
||||||
) {
|
|
||||||
val configuration = LocalConfiguration.current
|
|
||||||
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
} else {
|
||||||
EmptyBookings()
|
Column(
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
|
||||||
|
|
||||||
Button(
|
|
||||||
onClick = { viewModel.onIntent(MainIntent.Booking) },
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.testTag(TestIds.Main.ADD_BUTTON)
|
.fillMaxSize()
|
||||||
.fillMaxWidth(),
|
.padding(horizontal = 16.dp)
|
||||||
colors = ButtonDefaults.buttonColors(
|
|
||||||
containerColor = MaterialTheme.colorScheme.primary,
|
|
||||||
contentColor = MaterialTheme.colorScheme.onPrimary
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
Text(stringResource(R.string.main_add_booking))
|
Card(
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun SortedBookingList(userData: ru.myitschool.work.domain.main.entities.UserEntity) {
|
|
||||||
val sortedBookings = remember(userData.booking) {
|
|
||||||
userData.getSortedBookingsWithFormattedDate()?.sortedBy { (originalDate, _, _) ->
|
|
||||||
originalDate
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LazyColumn(
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 16.dp),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
|
||||||
) {
|
) {
|
||||||
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(
|
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
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
contentAlignment = Alignment.Center
|
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(
|
||||||
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,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun BookingItem(
|
||||||
|
booking: BookingItem,
|
||||||
|
position: Int,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
) {
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -5,8 +5,12 @@ import java.time.format.DateTimeFormatter
|
|||||||
|
|
||||||
sealed interface MainState {
|
sealed interface MainState {
|
||||||
object Loading : MainState
|
object Loading : MainState
|
||||||
data class Data(val userData: ru.myitschool.work.domain.main.entities.UserEntity) : MainState
|
data class Data(
|
||||||
object Error : MainState
|
val userName: String = "",
|
||||||
|
val userPhotoUrl: String? = null,
|
||||||
|
val bookings: List<BookingItem> = emptyList(),
|
||||||
|
val error: String? = null
|
||||||
|
) : MainState
|
||||||
}
|
}
|
||||||
|
|
||||||
data class BookingItem(
|
data class BookingItem(
|
||||||
|
|||||||
@@ -1,97 +1,112 @@
|
|||||||
package ru.myitschool.work.ui.screen.main
|
package ru.myitschool.work.ui.screen.main
|
||||||
|
|
||||||
import android.app.Application
|
import android.content.Context
|
||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
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.AuthRepository
|
||||||
import ru.myitschool.work.data.repo.MainRepository
|
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)
|
private val _uiState = MutableStateFlow<MainState>(MainState.Loading)
|
||||||
val uiState: StateFlow<MainState> = _uiState.asStateFlow()
|
val uiState: StateFlow<MainState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
private val _actionFlow: MutableSharedFlow<MainAction> = MutableSharedFlow()
|
private val _actionFlow: MutableSharedFlow<MainAction> = MutableSharedFlow()
|
||||||
val actionFlow: SharedFlow<MainAction> = _actionFlow
|
val actionFlow: SharedFlow<MainAction> = _actionFlow
|
||||||
|
|
||||||
|
private val dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")
|
||||||
|
|
||||||
init {
|
init {
|
||||||
loadData()
|
loadData()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadData() {
|
fun onIntent(intent: MainIntent) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
when (intent) {
|
||||||
_uiState.update { MainState.Loading }
|
MainIntent.Logout -> {
|
||||||
|
authRepo.clear()
|
||||||
try {
|
viewModelScope.launch {
|
||||||
val userCode = dataStoreManager.getUserCode().first()
|
_actionFlow.emit(MainAction.NavigateToAuth)
|
||||||
|
}
|
||||||
if (userCode.isEmpty()) {
|
}
|
||||||
_actionFlow.emit(MainAction.Auth)
|
MainIntent.Refresh -> {
|
||||||
return@launch
|
loadData()
|
||||||
|
}
|
||||||
|
MainIntent.AddBooking -> {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_actionFlow.emit(MainAction.NavigateToBooking)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is MainIntent.ItemClick -> {
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loadDataUseCase.invoke(userCode).fold(
|
private fun loadData() {
|
||||||
onSuccess = { data ->
|
viewModelScope.launch {
|
||||||
_uiState.update { MainState.Data(data) }
|
_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 ->
|
onFailure = { error ->
|
||||||
error.printStackTrace()
|
_uiState.update {
|
||||||
_uiState.update { MainState.Error }
|
MainState.Data(
|
||||||
|
userName = "",
|
||||||
|
userPhotoUrl = null,
|
||||||
|
bookings = emptyList(),
|
||||||
|
error = error.message ?: "Ошибка загрузки данных"
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
} catch (error: Exception) {
|
|
||||||
error.printStackTrace()
|
|
||||||
_uiState.update { MainState.Error }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun onIntent(intent: MainIntent) {
|
class MainViewModelFactory(private val context: Context) : ViewModelProvider.Factory {
|
||||||
when(intent) {
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||||
is MainIntent.LoadData -> loadData()
|
if (modelClass.isAssignableFrom(MainViewModel::class.java)) {
|
||||||
is MainIntent.Booking -> {
|
val authRepository = AuthRepository.getInstance(context)
|
||||||
viewModelScope.launch(Dispatchers.Default) {
|
val mainRepository = MainRepository(authRepository)
|
||||||
_actionFlow.emit(MainAction.Booking)
|
return MainViewModel(authRepository, mainRepository) as T
|
||||||
}
|
|
||||||
}
|
|
||||||
is MainIntent.Logout -> {
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
authRepository.clear()
|
|
||||||
_actionFlow.emit(MainAction.Auth)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
throw IllegalArgumentException("Unknown ViewModel class")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface MainAction {
|
sealed interface MainAction {
|
||||||
object Auth : MainAction
|
object NavigateToAuth : MainAction
|
||||||
object Booking : MainAction
|
object NavigateToBooking : MainAction
|
||||||
}
|
}
|
||||||
@@ -13,10 +13,4 @@
|
|||||||
<string name="book_book">Забронировать</string>
|
<string name="book_book">Забронировать</string>
|
||||||
<string name="book_refresh">Повторить</string>
|
<string name="book_refresh">Повторить</string>
|
||||||
<string name="book_empty">Всё забронировано</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>
|
</resources>
|
||||||
Reference in New Issue
Block a user