Compare commits
3
Commits
a775c2799c
...
568ca32668
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
568ca32668
|
||
|
|
9b90b655ab
|
||
|
|
12dc82e5f4
|
@@ -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
|
||||
|
||||
@@ -66,11 +66,10 @@ private suspend fun getMyself(login: String) = coroutineScope {
|
||||
}
|
||||
|
||||
suspend fun getScheduleDay(
|
||||
date: LocalDate,
|
||||
type: Type = ME.type,
|
||||
id: Int = ME.unnId!!,
|
||||
date: LocalDate
|
||||
): ArrayList<ScheduleUnit> {
|
||||
|
||||
id: Int = ME.unnId!!
|
||||
): List<ScheduleUnit> {
|
||||
if ((type == ME.type) and (id == ME.unnId!!)) {
|
||||
val schedule = withContext(Dispatchers.IO) { loadSchedule(date) }
|
||||
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
|
||||
|
||||
@@ -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(
|
||||
type: Type = ME.type,
|
||||
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
|
||||
|
||||
@@ -29,7 +29,7 @@ class ScheduleUnit(
|
||||
val date: LocalDate,
|
||||
val discipline: Discipline,
|
||||
val kindOfWork: KindOfWork,
|
||||
val lecturers: ArrayList<Lecturer>,
|
||||
val lecturers: List<Lecturer>,
|
||||
val stream: String,
|
||||
val begin: LocalTime,
|
||||
val end: LocalTime
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -17,7 +17,7 @@ val cacheDb = roomDb<AppDatabase>(name = "cache") {
|
||||
DisciplineDB::class,
|
||||
KindOfWorkDB::class,
|
||||
LecturerDB::class,
|
||||
ScheduleUnitDB::class], version = 1)
|
||||
ScheduleUnitDB::class], version = 2)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun userDao(): UserDao
|
||||
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
|
||||
|
||||
import android.util.Log
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
@@ -11,6 +10,7 @@ import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.PrimaryKey
|
||||
import androidx.room.Query
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import ru.sweetbread.unn.api.Auditorium
|
||||
import ru.sweetbread.unn.api.Building
|
||||
import ru.sweetbread.unn.api.Discipline
|
||||
@@ -28,7 +28,7 @@ data class BuildingDB(
|
||||
@PrimaryKey val oid: Int,
|
||||
@ColumnInfo val name: String,
|
||||
@ColumnInfo val gid: Int,
|
||||
@ColumnInfo val expiredAt: String
|
||||
@ColumnInfo val updatedAt: String
|
||||
)
|
||||
|
||||
@Dao
|
||||
@@ -37,40 +37,15 @@ interface BuildingDao {
|
||||
fun get(oid: Int): BuildingDB?
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insert(building: BuildingDB)
|
||||
suspend fun insert(buildings: List<BuildingDB>)
|
||||
|
||||
@Delete
|
||||
fun delete(building: BuildingDB)
|
||||
}
|
||||
|
||||
fun cacheBuilding(building: Building) {
|
||||
cacheDb.buildingDao().insert(
|
||||
BuildingDB(
|
||||
building.oid,
|
||||
building.name,
|
||||
building.gid,
|
||||
LocalDateTime.now().plusMonths(1).format(DateTimeFormatter.ISO_DATE_TIME)
|
||||
)
|
||||
)
|
||||
}
|
||||
fun BuildingDB.toModel() = Building(name, gid, oid)
|
||||
fun Building.toEntity() = BuildingDB(oid, name, gid, LocalDateTime.now().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
|
||||
data class AuditoriumDB(
|
||||
@@ -78,7 +53,7 @@ data class AuditoriumDB(
|
||||
@ColumnInfo val name: String,
|
||||
@ColumnInfo val floor: Int?,
|
||||
@ColumnInfo val buildingOid: Int,
|
||||
@ColumnInfo val expiredAt: String
|
||||
@ColumnInfo val updatedAt: String
|
||||
)
|
||||
|
||||
@Dao
|
||||
@@ -87,43 +62,36 @@ interface AuditoriumDao {
|
||||
fun get(oid: Int): AuditoriumDB?
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insert(auditorium: AuditoriumDB)
|
||||
suspend fun insert(auditoriums: List<AuditoriumDB>)
|
||||
|
||||
@Delete
|
||||
fun delete(auditorium: AuditoriumDB)
|
||||
}
|
||||
|
||||
fun cacheAuditorium(auditorium: Auditorium) {
|
||||
cacheBuilding(auditorium.building)
|
||||
cacheDb.auditoriumDao().insert(
|
||||
AuditoriumDB(
|
||||
auditorium.oid,
|
||||
auditorium.name,
|
||||
auditorium.floor,
|
||||
auditorium.building.oid,
|
||||
LocalDateTime.now().plusMonths(1).format(DateTimeFormatter.ISO_DATE_TIME)
|
||||
)
|
||||
fun AuditoriumDB.toModel(): Auditorium? {
|
||||
return Auditorium(
|
||||
name,
|
||||
oid,
|
||||
floor ?: 0,
|
||||
cacheDb.buildingDao().get(buildingOid)?.toModel() ?: return null
|
||||
)
|
||||
}
|
||||
fun Auditorium.toEntity() = AuditoriumDB(
|
||||
oid,
|
||||
name,
|
||||
floor,
|
||||
building.oid,
|
||||
LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME)
|
||||
)
|
||||
|
||||
fun loadAuditorium(oid: Int): Auditorium? {
|
||||
return cacheDb.auditoriumDao().get(oid)?.let {
|
||||
if (LocalDateTime.parse(
|
||||
it.expiredAt,
|
||||
DateTimeFormatter.ISO_DATE_TIME
|
||||
) > LocalDateTime.now()
|
||||
) {
|
||||
cacheDb.auditoriumDao().delete(it)
|
||||
return null
|
||||
}
|
||||
val building = loadBuilding(it.buildingOid) ?: return null
|
||||
return Auditorium(
|
||||
it.name,
|
||||
it.oid,
|
||||
it.floor ?: 0,
|
||||
building
|
||||
suspend fun cacheAuditorium(auditoriums: List<Auditorium>) {
|
||||
cacheDb.buildingDao().insert(
|
||||
auditoriums.map { it.building }.map { it.toEntity() }
|
||||
)
|
||||
|
||||
cacheDb.auditoriumDao().insert(
|
||||
auditoriums.map { it.toEntity() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Entity
|
||||
@@ -131,7 +99,7 @@ data class DisciplineDB(
|
||||
@PrimaryKey val oid: Int,
|
||||
@ColumnInfo val name: String,
|
||||
@ColumnInfo val type: Int,
|
||||
@ColumnInfo val expiredAt: String
|
||||
@ColumnInfo val updatedAt: String
|
||||
)
|
||||
|
||||
@Dao
|
||||
@@ -140,41 +108,15 @@ interface DisciplineDao {
|
||||
fun get(oid: Int): DisciplineDB?
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insert(discipline: DisciplineDB)
|
||||
suspend fun insert(disciplines: List<DisciplineDB>)
|
||||
|
||||
@Delete
|
||||
fun delete(discipline: DisciplineDB)
|
||||
}
|
||||
|
||||
fun cacheDiscipline(discipline: Discipline) {
|
||||
cacheDb.disciplineDao().insert(
|
||||
DisciplineDB(
|
||||
discipline.oid,
|
||||
discipline.name,
|
||||
discipline.type,
|
||||
LocalDateTime.now().plusMonths(1).format(DateTimeFormatter.ISO_DATE_TIME)
|
||||
)
|
||||
)
|
||||
}
|
||||
fun DisciplineDB.toModel() = Discipline(name, oid, type)
|
||||
fun Discipline.toEntity() = DisciplineDB(oid, name, type, LocalDateTime.now().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
|
||||
data class KindOfWorkDB(
|
||||
@@ -182,7 +124,7 @@ data class KindOfWorkDB(
|
||||
@ColumnInfo val name: String,
|
||||
@ColumnInfo val uid: String,
|
||||
@ColumnInfo val complexity: Int,
|
||||
@ColumnInfo val expiredAt: String
|
||||
@ColumnInfo val updatedAt: String
|
||||
)
|
||||
|
||||
@Dao
|
||||
@@ -191,43 +133,21 @@ interface KindOfWorkDao {
|
||||
fun get(oid: Int): KindOfWorkDB?
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insert(kindOfWork: KindOfWorkDB)
|
||||
suspend fun insert(kindOfWork: List<KindOfWorkDB>)
|
||||
|
||||
@Delete
|
||||
fun delete(kindOfWork: KindOfWorkDB)
|
||||
}
|
||||
|
||||
fun cacheKindOfWork(kindOfWork: KindOfWork) {
|
||||
cacheDb.kindOfWorkDao().insert(
|
||||
KindOfWorkDB(
|
||||
kindOfWork.oid,
|
||||
kindOfWork.name,
|
||||
kindOfWork.uid,
|
||||
kindOfWork.complexity,
|
||||
LocalDateTime.now().plusMonths(1).format(DateTimeFormatter.ISO_DATE_TIME)
|
||||
)
|
||||
)
|
||||
}
|
||||
fun KindOfWorkDB.toModel() = KindOfWork(name, oid, uid, complexity)
|
||||
fun KindOfWork.toEntity() = KindOfWorkDB(
|
||||
oid,
|
||||
name,
|
||||
uid,
|
||||
complexity,
|
||||
LocalDateTime.now().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
|
||||
data class LecturerDB(
|
||||
@@ -236,7 +156,7 @@ data class LecturerDB(
|
||||
@ColumnInfo val rank: LecturerRank,
|
||||
@ColumnInfo val email: String,
|
||||
@ColumnInfo val uid: String,
|
||||
@ColumnInfo val expiredAt: String
|
||||
@ColumnInfo val updatedAt: String
|
||||
)
|
||||
|
||||
@Dao
|
||||
@@ -245,45 +165,22 @@ interface LecturerDao {
|
||||
fun get(unnId: Int): LecturerDB?
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insert(lecturer: LecturerDB)
|
||||
suspend fun insert(lecturers: List<LecturerDB>)
|
||||
|
||||
@Delete
|
||||
fun delete(lecturer: LecturerDB)
|
||||
}
|
||||
|
||||
fun cacheLecturer(lecturer: Lecturer) {
|
||||
cacheDb.lecturerDao().insert(
|
||||
LecturerDB(
|
||||
lecturer.unnId,
|
||||
lecturer.name,
|
||||
lecturer.rank,
|
||||
lecturer.email,
|
||||
lecturer.uid,
|
||||
LocalDateTime.now().plusMonths(1).format(DateTimeFormatter.ISO_DATE_TIME)
|
||||
)
|
||||
)
|
||||
}
|
||||
fun LecturerDB.toModel() = Lecturer(name, rank, email, unnId, uid)
|
||||
fun Lecturer.toEntity() = LecturerDB(
|
||||
unnId,
|
||||
name,
|
||||
rank,
|
||||
email,
|
||||
uid,
|
||||
LocalDateTime.now().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
|
||||
data class ScheduleUnitDB(
|
||||
@@ -296,9 +193,9 @@ data class ScheduleUnitDB(
|
||||
@ColumnInfo val auditoriumOid: Int,
|
||||
@ColumnInfo val disciplineOid: 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
|
||||
interface ScheduleItemDao {
|
||||
@@ -308,78 +205,52 @@ interface ScheduleItemDao {
|
||||
@Query("SELECT * FROM scheduleUnitDB WHERE date = :date ORDER BY `begin`")
|
||||
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)
|
||||
fun insert(user: ScheduleUnitDB)
|
||||
suspend fun insert(units: List<ScheduleUnitDB>)
|
||||
|
||||
@Delete
|
||||
fun delete(user: ScheduleUnitDB)
|
||||
fun delete(unit: ScheduleUnitDB)
|
||||
|
||||
@Query("DELETE FROM scheduleUnitDB WHERE date = :date")
|
||||
fun deleteByDate(date: String)
|
||||
}
|
||||
|
||||
fun cacheSchedule(item: ScheduleUnit) {
|
||||
cacheAuditorium(item.auditorium)
|
||||
cacheDiscipline(item.discipline)
|
||||
cacheKindOfWork(item.kindOfWork)
|
||||
cacheLecturer(item.lecturers[0])
|
||||
|
||||
cacheDb.scheduleDao().insert(
|
||||
ScheduleUnitDB(
|
||||
item.oid,
|
||||
item.date.format(DateTimeFormatter.ISO_DATE),
|
||||
item.stream,
|
||||
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 cacheSchedule(items: ArrayList<ScheduleUnit>) {
|
||||
for (item in items)
|
||||
cacheSchedule(item)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
fun ScheduleUnitDB.toModel(): ScheduleUnit? {
|
||||
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)
|
||||
oid,
|
||||
cacheDb.auditoriumDao().get(auditoriumOid)?.toModel() ?: return null,
|
||||
LocalDate.parse(date, DateTimeFormatter.ISO_DATE),
|
||||
cacheDb.disciplineDao().get(disciplineOid)?.toModel() ?: return null,
|
||||
cacheDb.kindOfWorkDao().get(kindOfWorkOid)?.toModel() ?: return null,
|
||||
lecturersId.split(",").map { cacheDb.lecturerDao().get(it.toInt())?.toModel() ?: return null },
|
||||
stream,
|
||||
LocalTime.parse(begin, DateTimeFormatter.ISO_TIME),
|
||||
LocalTime.parse(end, DateTimeFormatter.ISO_TIME)
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
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 loadSchedule(date: LocalDate): ArrayList<ScheduleUnit> {
|
||||
cacheDb.scheduleDao().getSchedule(date.format(DateTimeFormatter.ISO_DATE))
|
||||
.map { Log.d("meow", "${it.oid}: ${loadSchedule(it.oid)}") }
|
||||
return ArrayList(
|
||||
cacheDb.scheduleDao().getSchedule(date.format(DateTimeFormatter.ISO_DATE))
|
||||
.mapNotNull { loadSchedule(it.oid) }
|
||||
)
|
||||
suspend fun cacheSchedule(units: List<ScheduleUnit>) {
|
||||
cacheAuditorium(units.map { it.auditorium })
|
||||
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> =
|
||||
cacheDb.scheduleDao().getSchedule(date.format(DateTimeFormatter.ISO_DATE))
|
||||
.mapNotNull { it.toModel() }
|
||||
|
||||
@@ -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,33 +77,84 @@ import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
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
|
||||
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<ScheduleUnit>() }
|
||||
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) {
|
||||
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
|
||||
@@ -101,7 +165,8 @@ fun ScheduleDay(modifier: Modifier = Modifier, date: LocalDate) {
|
||||
if (lessons.isEmpty())
|
||||
item { Text(appStr(R.string.noData)) }
|
||||
}
|
||||
} else {
|
||||
}
|
||||
is ScheduleState.Loading ->
|
||||
LinearProgressIndicator(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -109,13 +174,8 @@ fun ScheduleDay(modifier: Modifier = Modifier, date: LocalDate) {
|
||||
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.Error ->
|
||||
Text((state as ScheduleState.Error).message, color = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -3,6 +3,6 @@
|
||||
#Sat Mar 16 18:30:45 MSK 2024
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
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
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
Reference in New Issue
Block a user