fix(teacher): register migration, containment period queries, concurrency guards
- Register AppDatabase.MIGRATION_3_4 so v3 databases upgrade without data loss - Query teacher schedule periods by containment (weekStart<=x AND weekEnd>=x) instead of exact weekStart, matching 2-week catalog windows - Clip buildTeacherSchedule days to the requested week range - Add weeksFor() to enumerate weeks of a catalog window for the UI selector - Serialize syncPeriod body with a Mutex to protect the unique index - Rethrow CancellationException before generic catch; version loadRevision checks so stale observers/loads are cancelled - Sync job tracking in PickerViewModel; keep loading true while empty - Drop dead selectTeacher helper
This commit is contained in:
@@ -40,14 +40,16 @@ interface TeacherScheduleDao {
|
|||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"SELECT teacher_days.* FROM teacher_days INNER JOIN teacher_schedules ON teacher_schedules.id = teacher_days.scheduleId " +
|
"SELECT teacher_days.* FROM teacher_days INNER JOIN teacher_schedules ON teacher_schedules.id = teacher_days.scheduleId " +
|
||||||
"WHERE teacher_schedules.season = :season AND teacher_schedules.weekStart = :weekStart ORDER BY teacher_days.date"
|
"WHERE teacher_schedules.season = :season AND teacher_schedules.weekStart <= :weekStart " +
|
||||||
|
"AND teacher_schedules.weekEnd >= :weekStart ORDER BY teacher_days.date"
|
||||||
)
|
)
|
||||||
fun observeDaysForPeriod(season: String, weekStart: Long): Flow<List<TeacherDayEntity>>
|
fun observeDaysForPeriod(season: String, weekStart: Long): Flow<List<TeacherDayEntity>>
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"SELECT teacher_lessons.* FROM teacher_lessons INNER JOIN teacher_days ON teacher_days.id = teacher_lessons.dayId " +
|
"SELECT teacher_lessons.* FROM teacher_lessons INNER JOIN teacher_days ON teacher_days.id = teacher_lessons.dayId " +
|
||||||
"INNER JOIN teacher_schedules ON teacher_schedules.id = teacher_days.scheduleId " +
|
"INNER JOIN teacher_schedules ON teacher_schedules.id = teacher_days.scheduleId " +
|
||||||
"WHERE teacher_schedules.season = :season AND teacher_schedules.weekStart = :weekStart " +
|
"WHERE teacher_schedules.season = :season AND teacher_schedules.weekStart <= :weekStart " +
|
||||||
|
"AND teacher_schedules.weekEnd >= :weekStart " +
|
||||||
"ORDER BY teacher_lessons.date, teacher_lessons.slotIndex, teacher_lessons.lessonIndex"
|
"ORDER BY teacher_lessons.date, teacher_lessons.slotIndex, teacher_lessons.lessonIndex"
|
||||||
)
|
)
|
||||||
fun observeLessonsForPeriod(season: String, weekStart: Long): Flow<List<TeacherLessonEntity>>
|
fun observeLessonsForPeriod(season: String, weekStart: Long): Flow<List<TeacherLessonEntity>>
|
||||||
@@ -55,12 +57,14 @@ interface TeacherScheduleDao {
|
|||||||
@Query(
|
@Query(
|
||||||
"SELECT DISTINCT teacher FROM teacher_lessons INNER JOIN teacher_days ON teacher_days.id = teacher_lessons.dayId " +
|
"SELECT DISTINCT teacher FROM teacher_lessons INNER JOIN teacher_days ON teacher_days.id = teacher_lessons.dayId " +
|
||||||
"INNER JOIN teacher_schedules ON teacher_schedules.id = teacher_days.scheduleId " +
|
"INNER JOIN teacher_schedules ON teacher_schedules.id = teacher_days.scheduleId " +
|
||||||
"WHERE teacher_schedules.season = :season AND teacher_schedules.weekStart = :weekStart ORDER BY teacher"
|
"WHERE teacher_schedules.season = :season AND teacher_schedules.weekStart <= :weekStart " +
|
||||||
|
"AND teacher_schedules.weekEnd >= :weekStart ORDER BY teacher"
|
||||||
)
|
)
|
||||||
fun observeDistinctTeachers(season: String, weekStart: Long): Flow<List<String>>
|
fun observeDistinctTeachers(season: String, weekStart: Long): Flow<List<String>>
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"SELECT EXISTS(SELECT 1 FROM teacher_schedules WHERE season = :season AND weekStart = :weekStart)"
|
"SELECT EXISTS(SELECT 1 FROM teacher_schedules WHERE season = :season " +
|
||||||
|
"AND weekStart <= :weekStart AND weekEnd >= :weekStart)"
|
||||||
)
|
)
|
||||||
suspend fun hasSchedulesForPeriod(season: String, weekStart: Long): Boolean
|
suspend fun hasSchedulesForPeriod(season: String, weekStart: Long): Boolean
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,17 +26,24 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.Semaphore
|
import kotlinx.coroutines.sync.Semaphore
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.sync.withPermit
|
import kotlinx.coroutines.sync.withPermit
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.time.DayOfWeek
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import java.time.LocalTime
|
import java.time.LocalTime
|
||||||
|
import java.time.temporal.TemporalAdjusters
|
||||||
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
|
|
||||||
class TeacherScheduleRepositoryImpl(
|
class TeacherScheduleRepositoryImpl(
|
||||||
private val scheduleDao: TeacherScheduleDao,
|
private val scheduleDao: TeacherScheduleDao,
|
||||||
private val teacherDao: TeacherDao
|
private val teacherDao: TeacherDao
|
||||||
) : TeacherScheduleRepository {
|
) : TeacherScheduleRepository {
|
||||||
|
|
||||||
|
private val syncMutex = Mutex()
|
||||||
|
|
||||||
override fun observeSchedule(period: SchedulePeriod): Flow<TeacherSchedule?> =
|
override fun observeSchedule(period: SchedulePeriod): Flow<TeacherSchedule?> =
|
||||||
combine(
|
combine(
|
||||||
scheduleDao.observeDaysForPeriod(period.season.name, period.weekStart.toEpochDay()),
|
scheduleDao.observeDaysForPeriod(period.season.name, period.weekStart.toEpochDay()),
|
||||||
@@ -50,54 +57,60 @@ class TeacherScheduleRepositoryImpl(
|
|||||||
.map { names -> names.map { name -> Teacher(name = name) } }
|
.map { names -> names.map { name -> Teacher(name = name) } }
|
||||||
|
|
||||||
override suspend fun syncPeriod(date: LocalDate): Flow<BulkSyncProgress> = flow {
|
override suspend fun syncPeriod(date: LocalDate): Flow<BulkSyncProgress> = flow {
|
||||||
val targets = withContext(Dispatchers.IO) {
|
// Unique index (departmentId, season, weekStart) breaks on interleaved
|
||||||
teacherDao.getPeriodsForDate(date.toEpochDay())
|
// delete+insert from concurrent syncPeriod() callers.
|
||||||
}
|
syncMutex.withLock {
|
||||||
if (targets.isEmpty()) {
|
val targets = withContext(Dispatchers.IO) {
|
||||||
emit(BulkSyncProgress(0, 0, 0))
|
teacherDao.getPeriodsForDate(date.toEpochDay())
|
||||||
return@flow
|
}
|
||||||
}
|
if (targets.isEmpty()) {
|
||||||
|
emit(BulkSyncProgress(0, 0, 0))
|
||||||
targets.map { it.season to it.weekStart }
|
return@withLock
|
||||||
.distinct()
|
|
||||||
.forEach { (season, weekStart) ->
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
scheduleDao.deletePeriod(season.name, weekStart)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val total = targets.size
|
targets.map { it.season to it.weekStart }
|
||||||
val semaphore = Semaphore(PARALLELISM)
|
.distinct()
|
||||||
val jobs = coroutineScope {
|
.forEach { (season, weekStart) ->
|
||||||
targets.map { target ->
|
withContext(Dispatchers.IO) {
|
||||||
async {
|
scheduleDao.deletePeriod(season.name, weekStart)
|
||||||
semaphore.withPermit {
|
}
|
||||||
try {
|
}
|
||||||
withContext(Dispatchers.IO) { syncDepartment(target) }
|
|
||||||
true
|
val total = targets.size
|
||||||
} catch (e: Throwable) {
|
val semaphore = Semaphore(PARALLELISM)
|
||||||
Log.e(
|
val jobs = coroutineScope {
|
||||||
TAG,
|
targets.map { target ->
|
||||||
"sync: failed for department ${target.departmentId} " +
|
async {
|
||||||
"season ${target.season} week ${target.weekStart} " +
|
semaphore.withPermit {
|
||||||
"(${e::class.simpleName}: ${e.message})",
|
try {
|
||||||
e
|
withContext(Dispatchers.IO) { syncDepartment(target) }
|
||||||
)
|
true
|
||||||
false
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Log.e(
|
||||||
|
TAG,
|
||||||
|
"sync: failed for department ${target.departmentId} " +
|
||||||
|
"season ${target.season} week ${target.weekStart} " +
|
||||||
|
"(${e::class.simpleName}: ${e.message})",
|
||||||
|
e
|
||||||
|
)
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
var done = 0
|
var done = 0
|
||||||
var failed = 0
|
var failed = 0
|
||||||
for (job in jobs) {
|
for (job in jobs) {
|
||||||
if (job.await()) done++ else failed++
|
if (job.await()) done++ else failed++
|
||||||
emit(BulkSyncProgress(total, done + failed, failed))
|
emit(BulkSyncProgress(total, done + failed, failed))
|
||||||
}
|
}
|
||||||
|
|
||||||
withContext(Dispatchers.IO) { trimToRecent(keepDistinctPeriods = 2) }
|
withContext(Dispatchers.IO) { trimToRecent(keepDistinctPeriods = 2) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun hasData(period: SchedulePeriod): Boolean =
|
override suspend fun hasData(period: SchedulePeriod): Boolean =
|
||||||
@@ -105,6 +118,30 @@ class TeacherScheduleRepositoryImpl(
|
|||||||
scheduleDao.hasSchedulesForPeriod(period.season.name, period.weekStart.toEpochDay())
|
scheduleDao.hasSchedulesForPeriod(period.season.name, period.weekStart.toEpochDay())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun weeksFor(date: LocalDate): List<SchedulePeriod> =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
teacherDao.getPeriodsForDate(date.toEpochDay())
|
||||||
|
.distinctBy { it.season to (it.weekStart to it.weekEnd) }
|
||||||
|
.flatMap { window ->
|
||||||
|
val windowStart = LocalDate.ofEpochDay(window.weekStart)
|
||||||
|
val windowEnd = LocalDate.ofEpochDay(window.weekEnd)
|
||||||
|
generateSequence(
|
||||||
|
windowStart.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY))
|
||||||
|
) { it.plusWeeks(1) }
|
||||||
|
.takeWhile { it <= windowEnd }
|
||||||
|
.map { monday ->
|
||||||
|
val weekEnd = monday.plusDays(6)
|
||||||
|
SchedulePeriod(
|
||||||
|
weekStart = monday,
|
||||||
|
weekEnd = if (weekEnd.isAfter(windowEnd)) windowEnd else weekEnd,
|
||||||
|
season = window.season
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.distinctBy { it.season to it.weekStart }
|
||||||
|
.sortedBy { it.weekStart }
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun clearAll() = withContext(Dispatchers.IO) {
|
override suspend fun clearAll() = withContext(Dispatchers.IO) {
|
||||||
scheduleDao.deleteAll()
|
scheduleDao.deleteAll()
|
||||||
}
|
}
|
||||||
@@ -182,9 +219,13 @@ class TeacherScheduleRepositoryImpl(
|
|||||||
dayEntities: List<TeacherDayEntity>,
|
dayEntities: List<TeacherDayEntity>,
|
||||||
lessonEntities: List<TeacherLessonEntity>
|
lessonEntities: List<TeacherLessonEntity>
|
||||||
): TeacherSchedule? {
|
): TeacherSchedule? {
|
||||||
if (dayEntities.isEmpty()) return null
|
val weekDays = dayEntities
|
||||||
|
.filter {
|
||||||
|
it.date >= period.weekStart.toEpochDay() && it.date <= period.weekEnd.toEpochDay()
|
||||||
|
}
|
||||||
|
if (weekDays.isEmpty()) return null
|
||||||
val lessonsByDate = lessonEntities.groupBy { it.date }
|
val lessonsByDate = lessonEntities.groupBy { it.date }
|
||||||
val days = dayEntities
|
val days = weekDays
|
||||||
.sortedBy { it.date }
|
.sortedBy { it.date }
|
||||||
.distinctBy { it.date }
|
.distinctBy { it.date }
|
||||||
.map { day ->
|
.map { day ->
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ class AppContainer(context: Context) {
|
|||||||
AppDatabase::class.java,
|
AppDatabase::class.java,
|
||||||
"vyatsu_schedule.db"
|
"vyatsu_schedule.db"
|
||||||
)
|
)
|
||||||
|
.addMigrations(AppDatabase.MIGRATION_3_4)
|
||||||
.fallbackToDestructiveMigration(dropAllTables = true)
|
.fallbackToDestructiveMigration(dropAllTables = true)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
|||||||
@@ -12,5 +12,6 @@ interface TeacherScheduleRepository {
|
|||||||
fun observeSchedule(period: SchedulePeriod): Flow<TeacherSchedule?>
|
fun observeSchedule(period: SchedulePeriod): Flow<TeacherSchedule?>
|
||||||
suspend fun syncPeriod(date: LocalDate): Flow<BulkSyncProgress>
|
suspend fun syncPeriod(date: LocalDate): Flow<BulkSyncProgress>
|
||||||
suspend fun hasData(period: SchedulePeriod): Boolean
|
suspend fun hasData(period: SchedulePeriod): Boolean
|
||||||
|
suspend fun weeksFor(date: LocalDate): List<SchedulePeriod>
|
||||||
suspend fun clearAll()
|
suspend fun clearAll()
|
||||||
}
|
}
|
||||||
@@ -13,6 +13,7 @@ import androidx.lifecycle.ViewModel
|
|||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import java.time.DayOfWeek
|
import java.time.DayOfWeek
|
||||||
import java.time.temporal.TemporalAdjusters
|
import java.time.temporal.TemporalAdjusters
|
||||||
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -45,6 +46,7 @@ class PickerViewModel(
|
|||||||
val uiState: StateFlow<PickerUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<PickerUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
private var teachersJob: kotlinx.coroutines.Job? = null
|
private var teachersJob: kotlinx.coroutines.Job? = null
|
||||||
|
private var syncJob: kotlinx.coroutines.Job? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
groupsLoad()
|
groupsLoad()
|
||||||
@@ -139,11 +141,20 @@ class PickerViewModel(
|
|||||||
|
|
||||||
_uiState.value = _uiState.value.copy(syncProgress = null, teachersLoading = true)
|
_uiState.value = _uiState.value.copy(syncProgress = null, teachersLoading = true)
|
||||||
|
|
||||||
viewModelScope.launch {
|
syncJob?.cancel()
|
||||||
runCatching {
|
syncJob = viewModelScope.launch {
|
||||||
|
try {
|
||||||
teacherScheduleRepository.syncPeriod(today).collect { progress ->
|
teacherScheduleRepository.syncPeriod(today).collect { progress ->
|
||||||
_uiState.value = _uiState.value.copy(syncProgress = progress)
|
_uiState.value = _uiState.value.copy(syncProgress = progress)
|
||||||
}
|
}
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
_uiState.value = _uiState.value.copy(
|
||||||
|
error = e.message ?: "Не удалось загрузить расписание преподавателей"
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
_uiState.value = _uiState.value.copy(teachersLoading = false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,10 +162,11 @@ class PickerViewModel(
|
|||||||
teachersJob = viewModelScope.launch {
|
teachersJob = viewModelScope.launch {
|
||||||
teacherScheduleRepository.observeTeachers(period).collect { teachers ->
|
teacherScheduleRepository.observeTeachers(period).collect { teachers ->
|
||||||
val query = _uiState.value.teacherQuery
|
val query = _uiState.value.teacherQuery
|
||||||
|
val isLoading = _uiState.value.teachersLoading
|
||||||
_uiState.value = _uiState.value.copy(
|
_uiState.value = _uiState.value.copy(
|
||||||
teachersLoading = false,
|
|
||||||
teachers = teachers,
|
teachers = teachers,
|
||||||
filteredTeachers = filterTeachers(teachers, query),
|
filteredTeachers = filterTeachers(teachers, query),
|
||||||
|
teachersLoading = isLoading && teachers.isEmpty(),
|
||||||
error = null
|
error = null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -178,11 +190,4 @@ class PickerViewModel(
|
|||||||
fun retryTeachers() {
|
fun retryTeachers() {
|
||||||
syncAndObserveTeachers()
|
syncAndObserveTeachers()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun selectTeacher(teacher: Teacher) {
|
|
||||||
viewModelScope.launch {
|
|
||||||
preferencesRepository.clearSelectedGroup()
|
|
||||||
preferencesRepository.setSelectedTeacherId(teacher.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -132,13 +132,17 @@ fun TeacherScheduleScreen(viewModel: TeacherScheduleViewModel, onSwitchTeacher:
|
|||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
)
|
)
|
||||||
WeekSelector(
|
WeekSelector(
|
||||||
periods = listOf(schedule.period),
|
periods = state.weeks.ifEmpty { listOf(schedule.period) },
|
||||||
selectedWeekStart = state.weekStart,
|
selectedWeekStart = state.weekStart,
|
||||||
todayWeekStart = state.todayDate?.let { todayDate ->
|
todayWeekStart = state.todayDate
|
||||||
schedule.period.weekStart.takeIf {
|
?.let { todayDate ->
|
||||||
!todayDate.isBefore(it) && !todayDate.isAfter(it.plusDays(6))
|
state.weeks.firstOrNull { period ->
|
||||||
|
!todayDate.isBefore(period.weekStart) && !todayDate.isAfter(period.weekEnd)
|
||||||
|
}?.weekStart
|
||||||
}
|
}
|
||||||
},
|
?: schedule.period.weekStart.takeIf {
|
||||||
|
state.todayDate?.let { today -> !today.isBefore(it) && !today.isAfter(it.plusDays(6)) } == true
|
||||||
|
},
|
||||||
onSelect = viewModel::selectWeek
|
onSelect = viewModel::selectWeek
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import androidx.lifecycle.viewModelScope
|
|||||||
import java.time.DayOfWeek
|
import java.time.DayOfWeek
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import java.time.temporal.TemporalAdjusters
|
import java.time.temporal.TemporalAdjusters
|
||||||
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -27,6 +28,8 @@ class TeacherScheduleViewModel(
|
|||||||
private var currentTeacherName: String? = null
|
private var currentTeacherName: String? = null
|
||||||
private var currentPeriod: SchedulePeriod? = null
|
private var currentPeriod: SchedulePeriod? = null
|
||||||
private var scheduleJob: Job? = null
|
private var scheduleJob: Job? = null
|
||||||
|
private var weeksJob: Job? = null
|
||||||
|
private var loadRevision = 0
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
@@ -40,52 +43,76 @@ class TeacherScheduleViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun selectTeacher(name: String) {
|
fun selectTeacher(name: String) {
|
||||||
|
val revision = ++loadRevision
|
||||||
currentTeacherName = name
|
currentTeacherName = name
|
||||||
_uiState.value = _uiState.value.copy(noTeacher = false, teacherName = name, isLoading = true, loadError = false)
|
_uiState.value = _uiState.value.copy(noTeacher = false, teacherName = name, isLoading = true, loadError = false)
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
preferencesRepository.setSelectedTeacherId(name)
|
try {
|
||||||
|
preferencesRepository.setSelectedTeacherId(name)
|
||||||
|
|
||||||
val savedWeekStart = runCatching {
|
val savedWeekStart = runCatching {
|
||||||
preferencesRepository.selectedTeacherWeekStart()?.let(LocalDate::parse)
|
preferencesRepository.selectedTeacherWeekStart()?.let(LocalDate::parse)
|
||||||
}.getOrNull()
|
}.getOrNull()
|
||||||
val savedDayDate = runCatching {
|
val savedDayDate = runCatching {
|
||||||
preferencesRepository.selectedTeacherDayDate()?.let(LocalDate::parse)
|
preferencesRepository.selectedTeacherDayDate()?.let(LocalDate::parse)
|
||||||
}.getOrNull()
|
}.getOrNull()
|
||||||
|
|
||||||
val today = ScheduleDates.moscowToday()
|
val today = ScheduleDates.moscowToday()
|
||||||
val weekStart = savedWeekStart
|
val weekStart = savedWeekStart
|
||||||
?: today.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY))
|
?: today.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY))
|
||||||
_uiState.value = _uiState.value.copy(
|
if (revision != loadRevision) return@launch
|
||||||
noTeacher = false,
|
_uiState.value = _uiState.value.copy(
|
||||||
teacherName = name,
|
noTeacher = false,
|
||||||
weekStart = weekStart,
|
teacherName = name,
|
||||||
dayDate = savedDayDate
|
weekStart = weekStart,
|
||||||
)
|
dayDate = savedDayDate
|
||||||
|
)
|
||||||
|
|
||||||
val period = SchedulePeriod(
|
val period = SchedulePeriod(
|
||||||
weekStart = weekStart,
|
weekStart = weekStart,
|
||||||
weekEnd = weekStart.plusDays(6),
|
weekEnd = weekStart.plusDays(6),
|
||||||
season = ScheduleDates.currentSeason(today)
|
season = ScheduleDates.currentSeason(today)
|
||||||
)
|
)
|
||||||
loadSchedule(period)
|
loadSchedule(period, revision)
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun loadSchedule(period: SchedulePeriod) {
|
private suspend fun loadSchedule(period: SchedulePeriod, revision: Int) {
|
||||||
|
if (revision != loadRevision) return
|
||||||
scheduleJob?.cancel()
|
scheduleJob?.cancel()
|
||||||
scheduleJob = null
|
scheduleJob = null
|
||||||
currentPeriod = period
|
currentPeriod = period
|
||||||
|
|
||||||
if (!scheduleRepository.hasData(period)) {
|
if (!scheduleRepository.hasData(period)) {
|
||||||
_uiState.value = _uiState.value.copy(isLoading = true, loadError = false)
|
_uiState.value = _uiState.value.copy(isLoading = true, loadError = false)
|
||||||
runCatching { scheduleRepository.syncPeriod(period.weekStart).collect { } }
|
try {
|
||||||
|
scheduleRepository.syncPeriod(period.weekStart).collect { }
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
|
} catch (e: Exception) {
|
||||||
|
}
|
||||||
|
if (revision != loadRevision) return
|
||||||
if (!scheduleRepository.hasData(period)) {
|
if (!scheduleRepository.hasData(period)) {
|
||||||
_uiState.value = _uiState.value.copy(isLoading = false, loadError = true)
|
_uiState.value = _uiState.value.copy(isLoading = false, loadError = true)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
weeksJob?.cancel()
|
||||||
|
weeksJob = viewModelScope.launch {
|
||||||
|
val weeks = scheduleRepository.weeksFor(period.weekStart)
|
||||||
|
if (revision != loadRevision) return@launch
|
||||||
|
_uiState.value = _uiState.value.copy(weeks = weeks)
|
||||||
|
}
|
||||||
|
|
||||||
scheduleJob = viewModelScope.launch {
|
scheduleJob = viewModelScope.launch {
|
||||||
scheduleRepository.observeSchedule(period).collect { schedule ->
|
scheduleRepository.observeSchedule(period).collect { schedule ->
|
||||||
|
if (revision != loadRevision) {
|
||||||
|
throw CancellationException("stale schedule observation")
|
||||||
|
}
|
||||||
currentTeacherName?.let { applySchedule(schedule, it) }
|
currentTeacherName?.let { applySchedule(schedule, it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -122,21 +149,27 @@ class TeacherScheduleViewModel(
|
|||||||
val state = _uiState.value
|
val state = _uiState.value
|
||||||
val name = state.teacherName ?: return
|
val name = state.teacherName ?: return
|
||||||
if (state.weekStart == weekStart) return
|
if (state.weekStart == weekStart) return
|
||||||
|
val revision = ++loadRevision
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
preferencesRepository.setSelectedTeacherWeekStart(weekStart.toString())
|
try {
|
||||||
_uiState.value = _uiState.value.copy(
|
preferencesRepository.setSelectedTeacherWeekStart(weekStart.toString())
|
||||||
weekStart = weekStart,
|
if (revision != loadRevision) return@launch
|
||||||
dayDate = null,
|
_uiState.value = _uiState.value.copy(
|
||||||
schedule = null,
|
weekStart = weekStart,
|
||||||
isLoading = true,
|
dayDate = null,
|
||||||
loadError = false
|
schedule = null,
|
||||||
)
|
isLoading = true,
|
||||||
val period = SchedulePeriod(
|
loadError = false
|
||||||
weekStart = weekStart,
|
)
|
||||||
weekEnd = weekStart.plusDays(6),
|
val period = SchedulePeriod(
|
||||||
season = ScheduleDates.currentSeason(weekStart)
|
weekStart = weekStart,
|
||||||
)
|
weekEnd = weekStart.plusDays(6),
|
||||||
loadSchedule(period)
|
season = ScheduleDates.currentSeason(weekStart)
|
||||||
|
)
|
||||||
|
loadSchedule(period, revision)
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,14 +186,26 @@ class TeacherScheduleViewModel(
|
|||||||
|
|
||||||
fun refresh() {
|
fun refresh() {
|
||||||
val period = currentPeriod ?: return
|
val period = currentPeriod ?: return
|
||||||
|
val revision = ++loadRevision
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_uiState.value = _uiState.value.copy(isLoading = true, loadError = false)
|
try {
|
||||||
runCatching { scheduleRepository.syncPeriod(ScheduleDates.moscowToday()).collect { } }
|
_uiState.value = _uiState.value.copy(isLoading = true, loadError = false)
|
||||||
loadSchedule(period)
|
try {
|
||||||
|
scheduleRepository.syncPeriod(ScheduleDates.moscowToday()).collect { }
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
|
} catch (e: Exception) {
|
||||||
|
}
|
||||||
|
if (revision != loadRevision) return@launch
|
||||||
|
loadSchedule(period, revision)
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun switchTeacher() {
|
fun switchTeacher() {
|
||||||
|
loadRevision++
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
currentTeacherName = null
|
currentTeacherName = null
|
||||||
currentPeriod = null
|
currentPeriod = null
|
||||||
@@ -183,5 +228,6 @@ data class TeacherScheduleUiState(
|
|||||||
val teacherName: String? = null,
|
val teacherName: String? = null,
|
||||||
val weekStart: LocalDate? = null,
|
val weekStart: LocalDate? = null,
|
||||||
val dayDate: LocalDate? = null,
|
val dayDate: LocalDate? = null,
|
||||||
val todayDate: LocalDate? = null
|
val todayDate: LocalDate? = null,
|
||||||
|
val weeks: List<SchedulePeriod> = emptyList()
|
||||||
)
|
)
|
||||||
Reference in New Issue
Block a user