diff --git a/app/src/main/java/ru/sweetbread/unn/ui/composes/Schedule.kt b/app/src/main/java/ru/sweetbread/unn/ui/composes/Schedule.kt index 4ce8afc..fe314d2 100644 --- a/app/src/main/java/ru/sweetbread/unn/ui/composes/Schedule.kt +++ b/app/src/main/java/ru/sweetbread/unn/ui/composes/Schedule.kt @@ -2,7 +2,6 @@ package ru.sweetbread.unn.ui.composes -import android.util.Log import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateColorAsState import androidx.compose.foundation.Canvas @@ -24,12 +23,11 @@ import androidx.compose.material3.Surface 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.mutableFloatStateOf import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -43,9 +41,22 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.compose.viewModel import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay -import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.withContext import ru.sweetbread.unn.R import ru.sweetbread.unn.api.Auditorium import ru.sweetbread.unn.api.Building @@ -55,6 +66,8 @@ import ru.sweetbread.unn.api.Lecturer import ru.sweetbread.unn.api.LecturerRank import ru.sweetbread.unn.api.ScheduleUnit import ru.sweetbread.unn.api.getScheduleDay +import ru.sweetbread.unn.db.cacheDb +import ru.sweetbread.unn.db.toModel import ru.sweetbread.unn.ui.theme.UNNTheme import splitties.resources.appStr import java.time.LocalDate @@ -64,58 +77,105 @@ import java.time.ZoneId import java.time.format.DateTimeFormatter import java.util.Calendar + +class ScheduleRepository { + fun getFromDb(date: LocalDate): List = + cacheDb.scheduleDao() + .getSchedule(date.format(DateTimeFormatter.ISO_DATE)) + .mapNotNull { it.toModel() } + suspend fun getFromApi(date: LocalDate): List = getScheduleDay(date) +} + +sealed class ScheduleState { + object Loading : ScheduleState() + data class Success(val data: List) : ScheduleState() + data class Error(val message: String) : ScheduleState() +} + +class ScheduleViewModel : ViewModel() { + private val repository = ScheduleRepository() + + private val _date = MutableStateFlow(LocalDate.now()) + val date: StateFlow = _date.asStateFlow() + + @OptIn(ExperimentalCoroutinesApi::class) + val scheduleState: StateFlow = _date + .flatMapLatest { date -> + flow { + emit(ScheduleState.Loading) + val cached = withContext(Dispatchers.IO) { + repository.getFromDb(date) + } + emit(ScheduleState.Success(cached)) + + try { + val fresh = repository.getFromApi(date) + if (fresh != cached) { + emit(ScheduleState.Success(fresh)) + } + } catch (e: Exception) { + emit(ScheduleState.Error("Ошибка загрузки: ${e.message}")) + } + }.flowOn(Dispatchers.IO) + } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5000), + initialValue = ScheduleState.Loading + ) + + fun updateDate(newDate: LocalDate) { + _date.update { newDate } + } +} + @Composable fun Schedule() { - var selectedDate by remember { mutableStateOf(LocalDate.now()) } + val viewModel: ScheduleViewModel = viewModel() Column { BoundedWeekPicker( modifier = Modifier .fillMaxWidth() .padding(vertical = 8.dp), - selectedDate = selectedDate, - onDateSelected = { selectedDate = it } + selectedDate = viewModel.date.collectAsState().value, + onDateSelected = { viewModel.updateDate(it) } ) - ScheduleDay(date = selectedDate) + ScheduleDay(viewModel = viewModel) } } @Composable -fun ScheduleDay(modifier: Modifier = Modifier, date: LocalDate) { - val scope = rememberCoroutineScope() - var loadedDate by remember { mutableStateOf(LocalDate.MIN) } - val lessons = remember { mutableListOf() } +fun ScheduleDay(modifier: Modifier = Modifier, viewModel: ScheduleViewModel) { var expanded by remember { mutableIntStateOf(0) } + val state by viewModel.scheduleState.collectAsState() - if (loadedDate == date) { - Log.d("Loaded", "${date.format(DateTimeFormatter.ISO_DATE)} ${lessons.size}") - LazyColumn (modifier) { - items(lessons) { - ScheduleItem(unit = it, modifier = Modifier.clickable { - expanded = if (it.oid == expanded) 0 - else it.oid - }, expanded = expanded == it.oid) - } + when (state) { + is ScheduleState.Success -> { + val lessons = (state as ScheduleState.Success).data + LazyColumn(modifier) { + items(lessons) { + ScheduleItem(unit = it, modifier = Modifier.clickable { + expanded = if (it.oid == expanded) 0 + else it.oid + }, expanded = expanded == it.oid) + } - if (lessons.isEmpty()) - item { Text(appStr(R.string.noData)) } - } - } else { - LinearProgressIndicator( - modifier = Modifier - .fillMaxWidth() - .padding(8.dp), - color = MaterialTheme.colorScheme.surfaceVariant, - trackColor = MaterialTheme.colorScheme.secondary, - ) - LaunchedEffect(date != loadedDate) { - scope.launch(Dispatchers.IO) { - lessons.clear() - lessons.addAll(getScheduleDay(date = date)) - loadedDate = date + if (lessons.isEmpty()) + item { Text(appStr(R.string.noData)) } } } + is ScheduleState.Loading -> + LinearProgressIndicator( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + color = MaterialTheme.colorScheme.surfaceVariant, + trackColor = MaterialTheme.colorScheme.secondary, + ) + is ScheduleState.Error -> + Text((state as ScheduleState.Error).message, color = MaterialTheme.colorScheme.error) } }