Compare commits

...
3 Commits
Author SHA1 Message Date
Sweetbread 568ca32668 feat: ViewModel in Schedule.kt 2026-02-16 00:12:22 +03:00
Sweetbread 9b90b655ab wip: cache refactor 2026-02-15 23:28:40 +03:00
Sweetbread 12dc82e5f4 fixup! deps: bump 2026-02-15 15:41:49 +03:00
7 changed files with 207 additions and 272 deletions
@@ -1,4 +1,4 @@
// Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE). // Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE).
package ru.sweetbread.unn.api package ru.sweetbread.unn.api
@@ -66,11 +66,10 @@ private suspend fun getMyself(login: String) = coroutineScope {
} }
suspend fun getScheduleDay( suspend fun getScheduleDay(
date: LocalDate,
type: Type = ME.type, type: Type = ME.type,
id: Int = ME.unnId!!, id: Int = ME.unnId!!
date: LocalDate ): List<ScheduleUnit> {
): ArrayList<ScheduleUnit> {
if ((type == ME.type) and (id == ME.unnId!!)) { if ((type == ME.type) and (id == ME.unnId!!)) {
val schedule = withContext(Dispatchers.IO) { loadSchedule(date) } val schedule = withContext(Dispatchers.IO) { loadSchedule(date) }
Log.d("Schedule", schedule.joinToString()) Log.d("Schedule", schedule.joinToString())
@@ -1,4 +1,4 @@
// Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE). // Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE).
package ru.sweetbread.unn.api package ru.sweetbread.unn.api
@@ -116,6 +116,11 @@ suspend fun getUser(userId: Int? = null): User {
) )
} }
/**
* Fetch schedule from Uni API
*
* @return [ArrayList] of all [ScheduleUnit]s between [start] and [finish]
*/
suspend fun downloadSchedule( suspend fun downloadSchedule(
type: Type = ME.type, type: Type = ME.type,
id: Int = ME.unnId!!, id: Int = ME.unnId!!,
@@ -1,4 +1,4 @@
// Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE). // Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE).
package ru.sweetbread.unn.api package ru.sweetbread.unn.api
@@ -29,7 +29,7 @@ class ScheduleUnit(
val date: LocalDate, val date: LocalDate,
val discipline: Discipline, val discipline: Discipline,
val kindOfWork: KindOfWork, val kindOfWork: KindOfWork,
val lecturers: ArrayList<Lecturer>, val lecturers: List<Lecturer>,
val stream: String, val stream: String,
val begin: LocalTime, val begin: LocalTime,
val end: LocalTime val end: LocalTime
+2 -2
View File
@@ -1,4 +1,4 @@
// Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE). // Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE).
package ru.sweetbread.unn.db package ru.sweetbread.unn.db
@@ -17,7 +17,7 @@ val cacheDb = roomDb<AppDatabase>(name = "cache") {
DisciplineDB::class, DisciplineDB::class,
KindOfWorkDB::class, KindOfWorkDB::class,
LecturerDB::class, LecturerDB::class,
ScheduleUnitDB::class], version = 1) ScheduleUnitDB::class], version = 2)
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao abstract fun userDao(): UserDao
abstract fun scheduleDao(): ScheduleItemDao abstract fun scheduleDao(): ScheduleItemDao
@@ -1,8 +1,7 @@
// Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE). // Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE).
package ru.sweetbread.unn.db package ru.sweetbread.unn.db
import android.util.Log
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Delete import androidx.room.Delete
@@ -11,6 +10,7 @@ import androidx.room.Insert
import androidx.room.OnConflictStrategy import androidx.room.OnConflictStrategy
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import androidx.room.Query import androidx.room.Query
import kotlinx.coroutines.flow.Flow
import ru.sweetbread.unn.api.Auditorium import ru.sweetbread.unn.api.Auditorium
import ru.sweetbread.unn.api.Building import ru.sweetbread.unn.api.Building
import ru.sweetbread.unn.api.Discipline import ru.sweetbread.unn.api.Discipline
@@ -28,7 +28,7 @@ data class BuildingDB(
@PrimaryKey val oid: Int, @PrimaryKey val oid: Int,
@ColumnInfo val name: String, @ColumnInfo val name: String,
@ColumnInfo val gid: Int, @ColumnInfo val gid: Int,
@ColumnInfo val expiredAt: String @ColumnInfo val updatedAt: String
) )
@Dao @Dao
@@ -37,40 +37,15 @@ interface BuildingDao {
fun get(oid: Int): BuildingDB? fun get(oid: Int): BuildingDB?
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(building: BuildingDB) suspend fun insert(buildings: List<BuildingDB>)
@Delete @Delete
fun delete(building: BuildingDB) fun delete(building: BuildingDB)
} }
fun cacheBuilding(building: Building) { fun BuildingDB.toModel() = Building(name, gid, oid)
cacheDb.buildingDao().insert( fun Building.toEntity() = BuildingDB(oid, name, gid, LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME))
BuildingDB(
building.oid,
building.name,
building.gid,
LocalDateTime.now().plusMonths(1).format(DateTimeFormatter.ISO_DATE_TIME)
)
)
}
fun loadBuilding(oid: Int): Building? {
return cacheDb.buildingDao().get(oid)?.let {
if (LocalDateTime.parse(
it.expiredAt,
DateTimeFormatter.ISO_DATE_TIME
) > LocalDateTime.now()
) {
cacheDb.buildingDao().delete(it)
return null
}
Building(
it.name,
it.gid,
it.oid
)
}
}
@Entity @Entity
data class AuditoriumDB( data class AuditoriumDB(
@@ -78,7 +53,7 @@ data class AuditoriumDB(
@ColumnInfo val name: String, @ColumnInfo val name: String,
@ColumnInfo val floor: Int?, @ColumnInfo val floor: Int?,
@ColumnInfo val buildingOid: Int, @ColumnInfo val buildingOid: Int,
@ColumnInfo val expiredAt: String @ColumnInfo val updatedAt: String
) )
@Dao @Dao
@@ -87,43 +62,36 @@ interface AuditoriumDao {
fun get(oid: Int): AuditoriumDB? fun get(oid: Int): AuditoriumDB?
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(auditorium: AuditoriumDB) suspend fun insert(auditoriums: List<AuditoriumDB>)
@Delete @Delete
fun delete(auditorium: AuditoriumDB) fun delete(auditorium: AuditoriumDB)
} }
fun cacheAuditorium(auditorium: Auditorium) { fun AuditoriumDB.toModel(): Auditorium? {
cacheBuilding(auditorium.building) return Auditorium(
cacheDb.auditoriumDao().insert( name,
AuditoriumDB( oid,
auditorium.oid, floor ?: 0,
auditorium.name, cacheDb.buildingDao().get(buildingOid)?.toModel() ?: return null
auditorium.floor,
auditorium.building.oid,
LocalDateTime.now().plusMonths(1).format(DateTimeFormatter.ISO_DATE_TIME)
)
) )
} }
fun Auditorium.toEntity() = AuditoriumDB(
oid,
name,
floor,
building.oid,
LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME)
)
fun loadAuditorium(oid: Int): Auditorium? { suspend fun cacheAuditorium(auditoriums: List<Auditorium>) {
return cacheDb.auditoriumDao().get(oid)?.let { cacheDb.buildingDao().insert(
if (LocalDateTime.parse( auditoriums.map { it.building }.map { it.toEntity() }
it.expiredAt, )
DateTimeFormatter.ISO_DATE_TIME
) > LocalDateTime.now() cacheDb.auditoriumDao().insert(
) { auditoriums.map { it.toEntity() }
cacheDb.auditoriumDao().delete(it) )
return null
}
val building = loadBuilding(it.buildingOid) ?: return null
return Auditorium(
it.name,
it.oid,
it.floor ?: 0,
building
)
}
} }
@Entity @Entity
@@ -131,7 +99,7 @@ data class DisciplineDB(
@PrimaryKey val oid: Int, @PrimaryKey val oid: Int,
@ColumnInfo val name: String, @ColumnInfo val name: String,
@ColumnInfo val type: Int, @ColumnInfo val type: Int,
@ColumnInfo val expiredAt: String @ColumnInfo val updatedAt: String
) )
@Dao @Dao
@@ -140,41 +108,15 @@ interface DisciplineDao {
fun get(oid: Int): DisciplineDB? fun get(oid: Int): DisciplineDB?
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(discipline: DisciplineDB) suspend fun insert(disciplines: List<DisciplineDB>)
@Delete @Delete
fun delete(discipline: DisciplineDB) fun delete(discipline: DisciplineDB)
} }
fun cacheDiscipline(discipline: Discipline) { fun DisciplineDB.toModel() = Discipline(name, oid, type)
cacheDb.disciplineDao().insert( fun Discipline.toEntity() = DisciplineDB(oid, name, type, LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME))
DisciplineDB(
discipline.oid,
discipline.name,
discipline.type,
LocalDateTime.now().plusMonths(1).format(DateTimeFormatter.ISO_DATE_TIME)
)
)
}
fun loadDiscipline(oid: Int): Discipline? {
return cacheDb.disciplineDao().get(oid)?.let {
if (LocalDateTime.parse(
it.expiredAt,
DateTimeFormatter.ISO_DATE_TIME
) > LocalDateTime.now()
) {
cacheDb.disciplineDao().delete(it)
return null
}
return Discipline(
it.name,
it.oid,
it.type
)
}
}
@Entity @Entity
data class KindOfWorkDB( data class KindOfWorkDB(
@@ -182,7 +124,7 @@ data class KindOfWorkDB(
@ColumnInfo val name: String, @ColumnInfo val name: String,
@ColumnInfo val uid: String, @ColumnInfo val uid: String,
@ColumnInfo val complexity: Int, @ColumnInfo val complexity: Int,
@ColumnInfo val expiredAt: String @ColumnInfo val updatedAt: String
) )
@Dao @Dao
@@ -191,43 +133,21 @@ interface KindOfWorkDao {
fun get(oid: Int): KindOfWorkDB? fun get(oid: Int): KindOfWorkDB?
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(kindOfWork: KindOfWorkDB) suspend fun insert(kindOfWork: List<KindOfWorkDB>)
@Delete @Delete
fun delete(kindOfWork: KindOfWorkDB) fun delete(kindOfWork: KindOfWorkDB)
} }
fun cacheKindOfWork(kindOfWork: KindOfWork) { fun KindOfWorkDB.toModel() = KindOfWork(name, oid, uid, complexity)
cacheDb.kindOfWorkDao().insert( fun KindOfWork.toEntity() = KindOfWorkDB(
KindOfWorkDB( oid,
kindOfWork.oid, name,
kindOfWork.name, uid,
kindOfWork.uid, complexity,
kindOfWork.complexity, LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME)
LocalDateTime.now().plusMonths(1).format(DateTimeFormatter.ISO_DATE_TIME) )
)
)
}
fun loadKindOfWork(oid: Int): KindOfWork? {
return cacheDb.kindOfWorkDao().get(oid)?.let {
if (LocalDateTime.parse(
it.expiredAt,
DateTimeFormatter.ISO_DATE_TIME
) > LocalDateTime.now()
) {
cacheDb.kindOfWorkDao().delete(it)
return null
}
return KindOfWork(
it.name,
it.oid,
it.uid,
it.complexity
)
}
}
@Entity @Entity
data class LecturerDB( data class LecturerDB(
@@ -236,7 +156,7 @@ data class LecturerDB(
@ColumnInfo val rank: LecturerRank, @ColumnInfo val rank: LecturerRank,
@ColumnInfo val email: String, @ColumnInfo val email: String,
@ColumnInfo val uid: String, @ColumnInfo val uid: String,
@ColumnInfo val expiredAt: String @ColumnInfo val updatedAt: String
) )
@Dao @Dao
@@ -245,45 +165,22 @@ interface LecturerDao {
fun get(unnId: Int): LecturerDB? fun get(unnId: Int): LecturerDB?
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(lecturer: LecturerDB) suspend fun insert(lecturers: List<LecturerDB>)
@Delete @Delete
fun delete(lecturer: LecturerDB) fun delete(lecturer: LecturerDB)
} }
fun cacheLecturer(lecturer: Lecturer) { fun LecturerDB.toModel() = Lecturer(name, rank, email, unnId, uid)
cacheDb.lecturerDao().insert( fun Lecturer.toEntity() = LecturerDB(
LecturerDB( unnId,
lecturer.unnId, name,
lecturer.name, rank,
lecturer.rank, email,
lecturer.email, uid,
lecturer.uid, LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME)
LocalDateTime.now().plusMonths(1).format(DateTimeFormatter.ISO_DATE_TIME) )
)
)
}
fun loadLecturer(unnId: Int): Lecturer? {
return cacheDb.lecturerDao().get(unnId)?.let {
if (LocalDateTime.parse(
it.expiredAt,
DateTimeFormatter.ISO_DATE_TIME
) > LocalDateTime.now()
) {
cacheDb.lecturerDao().delete(it)
return null
}
return Lecturer(
it.name,
it.rank,
it.email,
it.unnId,
it.uid
)
}
}
@Entity @Entity
data class ScheduleUnitDB( data class ScheduleUnitDB(
@@ -296,9 +193,9 @@ data class ScheduleUnitDB(
@ColumnInfo val auditoriumOid: Int, @ColumnInfo val auditoriumOid: Int,
@ColumnInfo val disciplineOid: Int, @ColumnInfo val disciplineOid: Int,
@ColumnInfo val kindOfWorkOid: Int, @ColumnInfo val kindOfWorkOid: Int,
@ColumnInfo val lecturerId: Int, // TODO: many-to-many @ColumnInfo val lecturersId: String,
@ColumnInfo val expiredAt: String @ColumnInfo val updatedAt: String
) )
@Dao @Dao
interface ScheduleItemDao { interface ScheduleItemDao {
@@ -308,78 +205,52 @@ interface ScheduleItemDao {
@Query("SELECT * FROM scheduleUnitDB WHERE date = :date ORDER BY `begin`") @Query("SELECT * FROM scheduleUnitDB WHERE date = :date ORDER BY `begin`")
fun getSchedule(date: String): List<ScheduleUnitDB> fun getSchedule(date: String): List<ScheduleUnitDB>
@Query("SELECT * FROM scheduleUnitDB WHERE date = :date ORDER BY `begin`")
fun getScheduleFlow(date: String): Flow<List<ScheduleUnitDB>>
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(user: ScheduleUnitDB) suspend fun insert(units: List<ScheduleUnitDB>)
@Delete @Delete
fun delete(user: ScheduleUnitDB) fun delete(unit: ScheduleUnitDB)
@Query("DELETE FROM scheduleUnitDB WHERE date = :date")
fun deleteByDate(date: String)
} }
fun cacheSchedule(item: ScheduleUnit) { fun ScheduleUnitDB.toModel(): ScheduleUnit? {
cacheAuditorium(item.auditorium) return ScheduleUnit(
cacheDiscipline(item.discipline) oid,
cacheKindOfWork(item.kindOfWork) cacheDb.auditoriumDao().get(auditoriumOid)?.toModel() ?: return null,
cacheLecturer(item.lecturers[0]) LocalDate.parse(date, DateTimeFormatter.ISO_DATE),
cacheDb.disciplineDao().get(disciplineOid)?.toModel() ?: return null,
cacheDb.scheduleDao().insert( cacheDb.kindOfWorkDao().get(kindOfWorkOid)?.toModel() ?: return null,
ScheduleUnitDB( lecturersId.split(",").map { cacheDb.lecturerDao().get(it.toInt())?.toModel() ?: return null },
item.oid, stream,
item.date.format(DateTimeFormatter.ISO_DATE), LocalTime.parse(begin, DateTimeFormatter.ISO_TIME),
item.stream, LocalTime.parse(end, DateTimeFormatter.ISO_TIME)
item.begin.format(DateTimeFormatter.ISO_TIME),
item.end.format(DateTimeFormatter.ISO_TIME),
item.auditorium.oid,
item.discipline.oid,
item.kindOfWork.oid,
item.lecturers[0].unnId,
when {
(LocalDate.now() > item.date) -> LocalDateTime.now().plusWeeks(2)
(LocalDate.now().plusWeeks(1) > item.date) -> LocalDateTime.now().plusDays(1)
(LocalDate.now().plusWeeks(2) > item.date) -> LocalDateTime.now().plusWeeks(1)
else -> LocalDateTime.now().plusWeeks(2)
}.format(DateTimeFormatter.ISO_DATE_TIME)
)
) )
} }
fun ScheduleUnit.toEntity() = ScheduleUnitDB(
oid,
date.format(DateTimeFormatter.ISO_DATE),
stream,
begin.format(DateTimeFormatter.ISO_TIME),
end.format(DateTimeFormatter.ISO_TIME),
auditorium.oid,
discipline.oid,
kindOfWork.oid,
lecturers.map { it.unnId }.joinToString(","),
LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME)
)
fun cacheSchedule(items: ArrayList<ScheduleUnit>) { suspend fun cacheSchedule(units: List<ScheduleUnit>) {
for (item in items) cacheAuditorium(units.map { it.auditorium })
cacheSchedule(item) cacheDb.disciplineDao().insert(units.map { it.discipline.toEntity() })
cacheDb.kindOfWorkDao().insert(units.map { it.kindOfWork.toEntity() })
cacheDb.lecturerDao().insert(units.flatMap { it.lecturers }.map { it.toEntity() })
cacheDb.scheduleDao().insert(units.map { it.toEntity() })
} }
fun loadSchedule(date: LocalDate): List<ScheduleUnit> =
fun loadSchedule(oid: Int): ScheduleUnit? {
cacheDb.scheduleDao().getSchedule(oid)?.let {
Log.d("load", it.oid.toString())
if (LocalDateTime.parse(
it.expiredAt,
DateTimeFormatter.ISO_DATE_TIME
) < LocalDateTime.now()
) {
Log.d("delete", it.oid.toString())
cacheDb.scheduleDao().delete(it)
return null
}
return ScheduleUnit(
it.oid,
loadAuditorium(it.auditoriumOid) ?: return null,
LocalDate.parse(it.date, DateTimeFormatter.ISO_DATE),
loadDiscipline(it.disciplineOid) ?: return null,
loadKindOfWork(it.kindOfWorkOid) ?: return null,
arrayListOf(loadLecturer(it.lecturerId) ?: return null),
it.stream,
LocalTime.parse(it.begin, DateTimeFormatter.ISO_TIME),
LocalTime.parse(it.end, DateTimeFormatter.ISO_TIME)
)
}
return null
}
fun loadSchedule(date: LocalDate): ArrayList<ScheduleUnit> {
cacheDb.scheduleDao().getSchedule(date.format(DateTimeFormatter.ISO_DATE)) cacheDb.scheduleDao().getSchedule(date.format(DateTimeFormatter.ISO_DATE))
.map { Log.d("meow", "${it.oid}: ${loadSchedule(it.oid)}") } .mapNotNull { it.toModel() }
return ArrayList(
cacheDb.scheduleDao().getSchedule(date.format(DateTimeFormatter.ISO_DATE))
.mapNotNull { loadSchedule(it.oid) }
)
}
@@ -2,7 +2,6 @@
package ru.sweetbread.unn.ui.composes package ru.sweetbread.unn.ui.composes
import android.util.Log
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColorAsState import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.Canvas import androidx.compose.foundation.Canvas
@@ -24,12 +23,11 @@ import androidx.compose.material3.Surface
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.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex 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.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay 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.R
import ru.sweetbread.unn.api.Auditorium import ru.sweetbread.unn.api.Auditorium
import ru.sweetbread.unn.api.Building 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.LecturerRank
import ru.sweetbread.unn.api.ScheduleUnit import ru.sweetbread.unn.api.ScheduleUnit
import ru.sweetbread.unn.api.getScheduleDay 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 ru.sweetbread.unn.ui.theme.UNNTheme
import splitties.resources.appStr import splitties.resources.appStr
import java.time.LocalDate import java.time.LocalDate
@@ -64,58 +77,105 @@ import java.time.ZoneId
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import java.util.Calendar import java.util.Calendar
class ScheduleRepository {
fun getFromDb(date: LocalDate): List<ScheduleUnit> =
cacheDb.scheduleDao()
.getSchedule(date.format(DateTimeFormatter.ISO_DATE))
.mapNotNull { it.toModel() }
suspend fun getFromApi(date: LocalDate): List<ScheduleUnit> = getScheduleDay(date)
}
sealed class ScheduleState {
object Loading : ScheduleState()
data class Success(val data: List<ScheduleUnit>) : ScheduleState()
data class Error(val message: String) : ScheduleState()
}
class ScheduleViewModel : ViewModel() {
private val repository = ScheduleRepository()
private val _date = MutableStateFlow(LocalDate.now())
val date: StateFlow<LocalDate> = _date.asStateFlow()
@OptIn(ExperimentalCoroutinesApi::class)
val scheduleState: StateFlow<ScheduleState> = _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 @Composable
fun Schedule() { fun Schedule() {
var selectedDate by remember { mutableStateOf(LocalDate.now()) } val viewModel: ScheduleViewModel = viewModel()
Column { Column {
BoundedWeekPicker( BoundedWeekPicker(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(vertical = 8.dp), .padding(vertical = 8.dp),
selectedDate = selectedDate, selectedDate = viewModel.date.collectAsState().value,
onDateSelected = { selectedDate = it } onDateSelected = { viewModel.updateDate(it) }
) )
ScheduleDay(date = selectedDate) ScheduleDay(viewModel = viewModel)
} }
} }
@Composable @Composable
fun ScheduleDay(modifier: Modifier = Modifier, date: LocalDate) { fun ScheduleDay(modifier: Modifier = Modifier, viewModel: ScheduleViewModel) {
val scope = rememberCoroutineScope()
var loadedDate by remember { mutableStateOf(LocalDate.MIN) }
val lessons = remember { mutableListOf<ScheduleUnit>() }
var expanded by remember { mutableIntStateOf(0) } var expanded by remember { mutableIntStateOf(0) }
val state by viewModel.scheduleState.collectAsState()
if (loadedDate == date) { when (state) {
Log.d("Loaded", "${date.format(DateTimeFormatter.ISO_DATE)} ${lessons.size}") is ScheduleState.Success -> {
LazyColumn (modifier) { val lessons = (state as ScheduleState.Success).data
items(lessons) { LazyColumn(modifier) {
ScheduleItem(unit = it, modifier = Modifier.clickable { items(lessons) {
expanded = if (it.oid == expanded) 0 ScheduleItem(unit = it, modifier = Modifier.clickable {
else it.oid expanded = if (it.oid == expanded) 0
}, expanded = expanded == it.oid) else it.oid
} }, expanded = expanded == it.oid)
}
if (lessons.isEmpty()) if (lessons.isEmpty())
item { Text(appStr(R.string.noData)) } 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
} }
} }
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)
} }
} }
+1 -1
View File
@@ -3,6 +3,6 @@
#Sat Mar 16 18:30:45 MSK 2024 #Sat Mar 16 18:30:45 MSK 2024
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists