refactor: database multi-period schedule storage
This commit is contained in:
@@ -65,6 +65,10 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ksp {
|
||||||
|
arg("room.schemaLocation", "$projectDir/schemas")
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(libs.androidx.core.ktx)
|
implementation(libs.androidx.core.ktx)
|
||||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||||
|
|||||||
@@ -0,0 +1,381 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 2,
|
||||||
|
"identityHash": "48411ce0f1f22ef1dccd5852a9e97c5d",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "groups",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `faculty` TEXT NOT NULL, `autumnFirst` INTEGER, `autumnSecond` INTEGER, `springFirst` INTEGER, `springSecond` INTEGER, `lastSyncedAt` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "faculty",
|
||||||
|
"columnName": "faculty",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "autumnFirst",
|
||||||
|
"columnName": "autumnFirst",
|
||||||
|
"affinity": "INTEGER"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "autumnSecond",
|
||||||
|
"columnName": "autumnSecond",
|
||||||
|
"affinity": "INTEGER"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "springFirst",
|
||||||
|
"columnName": "springFirst",
|
||||||
|
"affinity": "INTEGER"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "springSecond",
|
||||||
|
"columnName": "springSecond",
|
||||||
|
"affinity": "INTEGER"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastSyncedAt",
|
||||||
|
"columnName": "lastSyncedAt",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "schedules",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupId` TEXT NOT NULL, `season` TEXT NOT NULL, `weekStart` INTEGER NOT NULL, `weekEnd` INTEGER NOT NULL, `lastSyncedAt` INTEGER NOT NULL, FOREIGN KEY(`groupId`) REFERENCES `groups`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "groupId",
|
||||||
|
"columnName": "groupId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "season",
|
||||||
|
"columnName": "season",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "weekStart",
|
||||||
|
"columnName": "weekStart",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "weekEnd",
|
||||||
|
"columnName": "weekEnd",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastSyncedAt",
|
||||||
|
"columnName": "lastSyncedAt",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_schedules_groupId",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"groupId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_schedules_groupId` ON `${TABLE_NAME}` (`groupId`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_schedules_groupId_season_weekStart",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"groupId",
|
||||||
|
"season",
|
||||||
|
"weekStart"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_schedules_groupId_season_weekStart` ON `${TABLE_NAME}` (`groupId`, `season`, `weekStart`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "groups",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"groupId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "days",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `scheduleId` INTEGER NOT NULL, `date` INTEGER NOT NULL, FOREIGN KEY(`scheduleId`) REFERENCES `schedules`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "scheduleId",
|
||||||
|
"columnName": "scheduleId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "date",
|
||||||
|
"columnName": "date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_days_scheduleId",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"scheduleId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_days_scheduleId` ON `${TABLE_NAME}` (`scheduleId`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_days_date",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"date"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_days_date` ON `${TABLE_NAME}` (`date`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "schedules",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"scheduleId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "lessons",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `dayId` INTEGER NOT NULL, `date` INTEGER NOT NULL, `slotIndex` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `lessonIndex` INTEGER NOT NULL, `raw` TEXT NOT NULL, `room` TEXT, `groupName` TEXT, `subgroup` TEXT, `subject` TEXT, `type` TEXT NOT NULL, `teacher` TEXT, FOREIGN KEY(`dayId`) REFERENCES `days`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "dayId",
|
||||||
|
"columnName": "dayId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "date",
|
||||||
|
"columnName": "date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "slotIndex",
|
||||||
|
"columnName": "slotIndex",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "start",
|
||||||
|
"columnName": "start",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "end",
|
||||||
|
"columnName": "end",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lessonIndex",
|
||||||
|
"columnName": "lessonIndex",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "raw",
|
||||||
|
"columnName": "raw",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "room",
|
||||||
|
"columnName": "room",
|
||||||
|
"affinity": "TEXT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "groupName",
|
||||||
|
"columnName": "groupName",
|
||||||
|
"affinity": "TEXT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subgroup",
|
||||||
|
"columnName": "subgroup",
|
||||||
|
"affinity": "TEXT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subject",
|
||||||
|
"columnName": "subject",
|
||||||
|
"affinity": "TEXT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "type",
|
||||||
|
"columnName": "type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "teacher",
|
||||||
|
"columnName": "teacher",
|
||||||
|
"affinity": "TEXT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_lessons_dayId",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"dayId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_lessons_dayId` ON `${TABLE_NAME}` (`dayId`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_lessons_date",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"date"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_lessons_date` ON `${TABLE_NAME}` (`date`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_lessons_subject",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"subject"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_lessons_subject` ON `${TABLE_NAME}` (`subject`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_lessons_room",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"room"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_lessons_room` ON `${TABLE_NAME}` (`room`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "days",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"dayId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "preferences",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`key`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "key",
|
||||||
|
"columnName": "key",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "value",
|
||||||
|
"columnName": "value",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"key"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '48411ce0f1f22ef1dccd5852a9e97c5d')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,16 +2,20 @@ package agzam.vyatsu.data.local
|
|||||||
|
|
||||||
import androidx.room.Database
|
import androidx.room.Database
|
||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
|
import androidx.room.TypeConverters
|
||||||
|
|
||||||
@Database(
|
@Database(
|
||||||
entities = [
|
entities = [
|
||||||
GroupEntity::class,
|
GroupEntity::class,
|
||||||
ScheduleEntity::class,
|
ScheduleEntity::class,
|
||||||
|
DayEntity::class,
|
||||||
|
LessonEntity::class,
|
||||||
PreferenceEntity::class
|
PreferenceEntity::class
|
||||||
],
|
],
|
||||||
version = 1,
|
version = 2,
|
||||||
exportSchema = false
|
exportSchema = true
|
||||||
)
|
)
|
||||||
|
@TypeConverters(Converters::class)
|
||||||
abstract class AppDatabase : RoomDatabase() {
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
abstract fun groupDao(): GroupDao
|
abstract fun groupDao(): GroupDao
|
||||||
abstract fun scheduleDao(): ScheduleDao
|
abstract fun scheduleDao(): ScheduleDao
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package agzam.vyatsu.data.local
|
||||||
|
|
||||||
|
import agzam.vyatsu.domain.model.LessonType
|
||||||
|
import androidx.room.TypeConverter
|
||||||
|
import java.time.LocalDate
|
||||||
|
import java.time.LocalTime
|
||||||
|
|
||||||
|
class Converters {
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun localDateToEpochDay(date: LocalDate?): Long? = date?.toEpochDay()
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun epochDayToLocalDate(epochDay: Long?): LocalDate? = epochDay?.let(LocalDate::ofEpochDay)
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun localTimeToMinutes(time: LocalTime?): Int? = time?.let { it.hour * 60 + it.minute }
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun minutesToLocalTime(minutes: Int?): LocalTime? =
|
||||||
|
minutes?.let { LocalTime.of(it / 60, it % 60) }
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun lessonTypeToString(type: LessonType): String = type.name
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun stringToLessonType(value: String): LessonType =
|
||||||
|
runCatching { LessonType.valueOf(value) }.getOrDefault(LessonType.OTHER)
|
||||||
|
}
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
package agzam.vyatsu.data.local
|
|
||||||
|
|
||||||
import agzam.vyatsu.domain.model.Day
|
|
||||||
import com.squareup.moshi.JsonClass
|
|
||||||
import java.time.LocalDate
|
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
|
||||||
data class DayDto(
|
|
||||||
val date: String,
|
|
||||||
val slots: List<SlotDto> = emptyList()
|
|
||||||
) {
|
|
||||||
fun toDomain(): Day = Day(
|
|
||||||
date = LocalDate.parse(date),
|
|
||||||
intervals = slots.map { it.toDomain() }
|
|
||||||
)
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun fromDomain(d: Day): DayDto = DayDto(
|
|
||||||
date = d.date.toString(),
|
|
||||||
slots = d.intervals.map { SlotDto.fromDomain(it) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package agzam.vyatsu.data.local
|
||||||
|
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.ForeignKey
|
||||||
|
import androidx.room.Index
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = "days",
|
||||||
|
foreignKeys = [
|
||||||
|
ForeignKey(
|
||||||
|
entity = ScheduleEntity::class,
|
||||||
|
parentColumns = ["id"],
|
||||||
|
childColumns = ["scheduleId"],
|
||||||
|
onDelete = ForeignKey.CASCADE
|
||||||
|
)
|
||||||
|
],
|
||||||
|
indices = [
|
||||||
|
Index("scheduleId"),
|
||||||
|
Index("date")
|
||||||
|
]
|
||||||
|
)
|
||||||
|
data class DayEntity(
|
||||||
|
@PrimaryKey(autoGenerate = true) val id: Long = 0,
|
||||||
|
val scheduleId: Long,
|
||||||
|
val date: Long
|
||||||
|
)
|
||||||
@@ -8,9 +8,9 @@ data class GroupEntity(
|
|||||||
@PrimaryKey val id: String,
|
@PrimaryKey val id: String,
|
||||||
val name: String,
|
val name: String,
|
||||||
val faculty: String,
|
val faculty: String,
|
||||||
val autumnFirst: String,
|
val autumnFirst: Long?,
|
||||||
val autumnSecond: String,
|
val autumnSecond: Long?,
|
||||||
val springFirst: String,
|
val springFirst: Long?,
|
||||||
val springSecond: String,
|
val springSecond: Long?,
|
||||||
val lastSyncedAt: Long
|
val lastSyncedAt: Long
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
package agzam.vyatsu.data.local
|
|
||||||
|
|
||||||
import agzam.vyatsu.domain.model.Lesson
|
|
||||||
import agzam.vyatsu.domain.model.LessonType
|
|
||||||
import com.squareup.moshi.JsonClass
|
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
|
||||||
data class LessonDto(
|
|
||||||
val raw: String = "",
|
|
||||||
val room: String? = null,
|
|
||||||
val group: String? = null,
|
|
||||||
val subgroup: String? = null,
|
|
||||||
val subject: String? = null,
|
|
||||||
val type: String = "",
|
|
||||||
val teacher: String? = null
|
|
||||||
) {
|
|
||||||
fun toDomain(): Lesson = Lesson(
|
|
||||||
raw = raw,
|
|
||||||
room = room?.takeIf { it.isNotBlank() },
|
|
||||||
group = group?.takeIf { it.isNotBlank() },
|
|
||||||
subgroup = subgroup?.takeIf { it.isNotBlank() },
|
|
||||||
subject = subject?.takeIf { it.isNotBlank() },
|
|
||||||
type = runCatching { LessonType.valueOf(type) }.getOrDefault(LessonType.OTHER),
|
|
||||||
teacher = teacher?.takeIf { it.isNotBlank() }
|
|
||||||
)
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun fromDomain(l: Lesson): LessonDto = LessonDto(
|
|
||||||
raw = l.raw,
|
|
||||||
room = l.room,
|
|
||||||
group = l.group,
|
|
||||||
subgroup = l.subgroup,
|
|
||||||
subject = l.subject,
|
|
||||||
type = l.type.name,
|
|
||||||
teacher = l.teacher
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package agzam.vyatsu.data.local
|
||||||
|
|
||||||
|
import agzam.vyatsu.domain.model.LessonType
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.ForeignKey
|
||||||
|
import androidx.room.Index
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = "lessons",
|
||||||
|
foreignKeys = [
|
||||||
|
ForeignKey(
|
||||||
|
entity = DayEntity::class,
|
||||||
|
parentColumns = ["id"],
|
||||||
|
childColumns = ["dayId"],
|
||||||
|
onDelete = ForeignKey.CASCADE
|
||||||
|
)
|
||||||
|
],
|
||||||
|
indices = [
|
||||||
|
Index("dayId"),
|
||||||
|
Index("date"),
|
||||||
|
Index("subject"),
|
||||||
|
Index("room")
|
||||||
|
]
|
||||||
|
)
|
||||||
|
data class LessonEntity(
|
||||||
|
@PrimaryKey(autoGenerate = true) val id: Long = 0,
|
||||||
|
val dayId: Long,
|
||||||
|
val date: Long,
|
||||||
|
val slotIndex: Int,
|
||||||
|
val start: Int,
|
||||||
|
val end: Int,
|
||||||
|
val lessonIndex: Int,
|
||||||
|
val raw: String,
|
||||||
|
val room: String?,
|
||||||
|
val groupName: String?,
|
||||||
|
val subgroup: String?,
|
||||||
|
val subject: String?,
|
||||||
|
val type: LessonType,
|
||||||
|
val teacher: String?
|
||||||
|
)
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
package agzam.vyatsu.data.local
|
|
||||||
|
|
||||||
import agzam.vyatsu.domain.model.Day
|
|
||||||
import com.squareup.moshi.Moshi
|
|
||||||
import com.squareup.moshi.Types
|
|
||||||
|
|
||||||
class ScheduleCodec(moshi: Moshi) {
|
|
||||||
private val type: java.lang.reflect.Type =
|
|
||||||
Types.newParameterizedType(List::class.java, DayDto::class.java)
|
|
||||||
private val adapter = moshi.adapter<List<DayDto>>(type)
|
|
||||||
|
|
||||||
fun toJson(days: List<Day>): String {
|
|
||||||
val dto = days.map { DayDto.fromDomain(it) }
|
|
||||||
return adapter.toJson(dto)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun fromJson(json: String): List<Day> {
|
|
||||||
return try {
|
|
||||||
val dto = adapter.fromJson(json) ?: return emptyList()
|
|
||||||
dto.map { it.toDomain() }
|
|
||||||
} catch (t: Throwable) {
|
|
||||||
emptyList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,15 +8,68 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface ScheduleDao {
|
interface ScheduleDao {
|
||||||
@Query("SELECT * FROM schedules WHERE groupId = :groupId")
|
|
||||||
fun observe(groupId: String): Flow<ScheduleEntity?>
|
|
||||||
|
|
||||||
@Query("SELECT * FROM schedules WHERE groupId = :groupId")
|
@Query("SELECT * FROM schedules WHERE groupId = :groupId ORDER BY weekStart DESC")
|
||||||
suspend fun get(groupId: String): ScheduleEntity?
|
fun observeSchedules(groupId: String): Flow<List<ScheduleEntity>>
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Query("SELECT * FROM schedules WHERE groupId = :groupId ORDER BY weekStart DESC")
|
||||||
suspend fun upsert(entity: ScheduleEntity)
|
suspend fun getSchedules(groupId: String): List<ScheduleEntity>
|
||||||
|
|
||||||
|
@Query("DELETE FROM days WHERE scheduleId = :scheduleId")
|
||||||
|
suspend fun deleteDaysForSchedule(scheduleId: Long)
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.ABORT)
|
||||||
|
suspend fun insertSchedule(schedule: ScheduleEntity): Long
|
||||||
|
|
||||||
|
@Query("DELETE FROM schedules WHERE groupId = :groupId AND season = :season AND weekStart = :weekStart")
|
||||||
|
suspend fun deleteSchedule(groupId: String, season: String, weekStart: Long)
|
||||||
|
|
||||||
@Query("DELETE FROM schedules WHERE groupId = :groupId")
|
@Query("DELETE FROM schedules WHERE groupId = :groupId")
|
||||||
suspend fun delete(groupId: String)
|
suspend fun deleteAll(groupId: String)
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
DELETE FROM schedules
|
||||||
|
WHERE groupId = :groupId
|
||||||
|
AND id NOT IN (
|
||||||
|
SELECT id FROM schedules
|
||||||
|
WHERE groupId = :groupId
|
||||||
|
ORDER BY weekStart DESC
|
||||||
|
LIMIT :keep
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
suspend fun trim(groupId: String, keep: Int)
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
suspend fun insertDays(days: List<DayEntity>)
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
suspend fun insertLessons(lessons: List<LessonEntity>)
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"SELECT days.* FROM days INNER JOIN schedules ON schedules.id = days.scheduleId " +
|
||||||
|
"WHERE schedules.groupId = :groupId ORDER BY days.date"
|
||||||
|
)
|
||||||
|
suspend fun getDaysForGroup(groupId: String): List<DayEntity>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"SELECT lessons.* FROM lessons INNER JOIN days ON days.id = lessons.dayId " +
|
||||||
|
"INNER JOIN schedules ON schedules.id = days.scheduleId " +
|
||||||
|
"WHERE schedules.groupId = :groupId ORDER BY lessons.date, lessons.slotIndex, lessons.lessonIndex"
|
||||||
|
)
|
||||||
|
suspend fun getLessonsForGroup(groupId: String): List<LessonEntity>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"SELECT days.* FROM days INNER JOIN schedules ON schedules.id = days.scheduleId " +
|
||||||
|
"WHERE schedules.groupId = :groupId ORDER BY days.date"
|
||||||
|
)
|
||||||
|
fun observeDaysForGroup(groupId: String): Flow<List<DayEntity>>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"SELECT lessons.* FROM lessons INNER JOIN days ON days.id = lessons.dayId " +
|
||||||
|
"INNER JOIN schedules ON schedules.id = days.scheduleId " +
|
||||||
|
"WHERE schedules.groupId = :groupId ORDER BY lessons.date, lessons.slotIndex, lessons.lessonIndex"
|
||||||
|
)
|
||||||
|
fun observeLessonsForGroup(groupId: String): Flow<List<LessonEntity>>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,31 @@
|
|||||||
package agzam.vyatsu.data.local
|
package agzam.vyatsu.data.local
|
||||||
|
|
||||||
|
import agzam.vyatsu.domain.model.Season
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
|
import androidx.room.ForeignKey
|
||||||
|
import androidx.room.Index
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
|
|
||||||
@Entity(tableName = "schedules")
|
@Entity(
|
||||||
|
tableName = "schedules",
|
||||||
|
foreignKeys = [
|
||||||
|
ForeignKey(
|
||||||
|
entity = GroupEntity::class,
|
||||||
|
parentColumns = ["id"],
|
||||||
|
childColumns = ["groupId"],
|
||||||
|
onDelete = ForeignKey.CASCADE
|
||||||
|
)
|
||||||
|
],
|
||||||
|
indices = [
|
||||||
|
Index("groupId"),
|
||||||
|
Index(value = ["groupId", "season", "weekStart"], unique = true)
|
||||||
|
]
|
||||||
|
)
|
||||||
data class ScheduleEntity(
|
data class ScheduleEntity(
|
||||||
@PrimaryKey val groupId: String,
|
@PrimaryKey(autoGenerate = true) val id: Long = 0,
|
||||||
val season: String,
|
val groupId: String,
|
||||||
val dateRangeFirst: String,
|
val season: Season,
|
||||||
val dateRangeSecond: String,
|
val weekStart: Long,
|
||||||
val weeksJson: String,
|
val weekEnd: Long,
|
||||||
val lastSyncedAt: Long
|
val lastSyncedAt: Long
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
package agzam.vyatsu.data.local
|
|
||||||
|
|
||||||
import agzam.vyatsu.domain.model.TimeSlot
|
|
||||||
import com.squareup.moshi.JsonClass
|
|
||||||
import java.time.LocalTime
|
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
|
||||||
data class SlotDto(
|
|
||||||
val start: String,
|
|
||||||
val end: String,
|
|
||||||
val lessons: List<LessonDto> = emptyList()
|
|
||||||
) {
|
|
||||||
fun toDomain(): TimeSlot = TimeSlot(
|
|
||||||
start = LocalTime.parse(start),
|
|
||||||
end = LocalTime.parse(end),
|
|
||||||
lessons = lessons.map { it.toDomain() }
|
|
||||||
)
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun fromDomain(s: TimeSlot): SlotDto = SlotDto(
|
|
||||||
start = s.start.toString(),
|
|
||||||
end = s.end.toString(),
|
|
||||||
lessons = s.lessons.map { LessonDto.fromDomain(it) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,6 +5,7 @@ import agzam.vyatsu.data.local.GroupEntity
|
|||||||
import agzam.vyatsu.data.remote.GroupsParser
|
import agzam.vyatsu.data.remote.GroupsParser
|
||||||
import agzam.vyatsu.data.remote.VyatsuHttpClient
|
import agzam.vyatsu.data.remote.VyatsuHttpClient
|
||||||
import agzam.vyatsu.data.remote.VyatsuUrls
|
import agzam.vyatsu.data.remote.VyatsuUrls
|
||||||
|
import agzam.vyatsu.domain.ScheduleDates
|
||||||
import agzam.vyatsu.domain.model.Group
|
import agzam.vyatsu.domain.model.Group
|
||||||
import agzam.vyatsu.domain.model.Season
|
import agzam.vyatsu.domain.model.Season
|
||||||
import agzam.vyatsu.domain.repository.GroupRepository
|
import agzam.vyatsu.domain.repository.GroupRepository
|
||||||
@@ -50,16 +51,18 @@ class GroupRepositoryImpl(
|
|||||||
id = group.id,
|
id = group.id,
|
||||||
name = group.name,
|
name = group.name,
|
||||||
faculty = group.faculty,
|
faculty = group.faculty,
|
||||||
autumnFirst = autumn?.first ?: "",
|
autumnFirst = autumn?.first?.let(::epochDay),
|
||||||
autumnSecond = autumn?.second ?: "",
|
autumnSecond = autumn?.second?.let(::epochDay),
|
||||||
springFirst = spring?.first ?: "",
|
springFirst = spring?.first?.let(::epochDay),
|
||||||
springSecond = spring?.second ?: "",
|
springSecond = spring?.second?.let(::epochDay),
|
||||||
lastSyncedAt = now()
|
lastSyncedAt = now()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
groupDao.insertAll(entities)
|
groupDao.insertAll(entities)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun epochDay(dateStr: String): Long = ScheduleDates.asDate(dateStr).toEpochDay()
|
||||||
|
|
||||||
private fun now(): Long = System.currentTimeMillis()
|
private fun now(): Long = System.currentTimeMillis()
|
||||||
|
|
||||||
private fun GroupEntity.toDomain(): Group = Group(
|
private fun GroupEntity.toDomain(): Group = Group(
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package agzam.vyatsu.data.repository
|
package agzam.vyatsu.data.repository
|
||||||
|
|
||||||
import agzam.vyatsu.data.local.ScheduleCodec
|
import agzam.vyatsu.data.local.DayEntity
|
||||||
|
import agzam.vyatsu.data.local.LessonEntity
|
||||||
import agzam.vyatsu.data.local.ScheduleDao
|
import agzam.vyatsu.data.local.ScheduleDao
|
||||||
import agzam.vyatsu.data.local.ScheduleEntity
|
import agzam.vyatsu.data.local.ScheduleEntity
|
||||||
import agzam.vyatsu.data.local.GroupDao
|
import agzam.vyatsu.data.local.GroupDao
|
||||||
@@ -9,39 +10,50 @@ import agzam.vyatsu.data.remote.pdf.PdfScheduleParser
|
|||||||
import agzam.vyatsu.data.remote.VyatsuHttpClient
|
import agzam.vyatsu.data.remote.VyatsuHttpClient
|
||||||
import agzam.vyatsu.data.remote.VyatsuUrls
|
import agzam.vyatsu.data.remote.VyatsuUrls
|
||||||
import agzam.vyatsu.domain.ScheduleDates
|
import agzam.vyatsu.domain.ScheduleDates
|
||||||
|
import agzam.vyatsu.domain.model.Day
|
||||||
import agzam.vyatsu.domain.model.DateRange
|
import agzam.vyatsu.domain.model.DateRange
|
||||||
|
import agzam.vyatsu.domain.model.Lesson
|
||||||
import agzam.vyatsu.domain.model.Schedule
|
import agzam.vyatsu.domain.model.Schedule
|
||||||
|
import agzam.vyatsu.domain.model.SchedulePeriod
|
||||||
import agzam.vyatsu.domain.model.Season
|
import agzam.vyatsu.domain.model.Season
|
||||||
|
import agzam.vyatsu.domain.model.TimeSlot
|
||||||
import agzam.vyatsu.domain.repository.ScheduleRepository
|
import agzam.vyatsu.domain.repository.ScheduleRepository
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import java.time.LocalDate
|
||||||
|
import java.time.LocalTime
|
||||||
|
|
||||||
class ScheduleRepositoryImpl(
|
class ScheduleRepositoryImpl(
|
||||||
private val scheduleDao: ScheduleDao,
|
private val scheduleDao: ScheduleDao,
|
||||||
private val groupDao: GroupDao,
|
private val groupDao: GroupDao
|
||||||
private val codec: ScheduleCodec
|
|
||||||
) : ScheduleRepository {
|
) : ScheduleRepository {
|
||||||
|
|
||||||
override fun observeSchedule(groupId: String): Flow<Schedule?> =
|
override fun observeSchedule(groupId: String): Flow<Schedule?> =
|
||||||
scheduleDao.observe(groupId).map { it?.toDomain() }
|
combine(
|
||||||
|
scheduleDao.observeSchedules(groupId),
|
||||||
|
scheduleDao.observeDaysForGroup(groupId),
|
||||||
|
scheduleDao.observeLessonsForGroup(groupId)
|
||||||
|
) { schedules, days, lessons ->
|
||||||
|
buildSchedule(schedules, days, lessons)
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun getSchedule(groupId: String): Schedule? =
|
override suspend fun getSchedule(groupId: String): Schedule? =
|
||||||
scheduleDao.get(groupId)?.toDomain()
|
buildSchedule(
|
||||||
|
scheduleDao.getSchedules(groupId),
|
||||||
|
scheduleDao.getDaysForGroup(groupId),
|
||||||
|
scheduleDao.getLessonsForGroup(groupId)
|
||||||
|
)
|
||||||
|
|
||||||
override suspend fun sync(groupId: String): Schedule? {
|
override suspend fun sync(groupId: String): Schedule? {
|
||||||
Log.d(TAG, "sync(groupId=$groupId) start")
|
Log.d(TAG, "sync(groupId=$groupId) start")
|
||||||
val group = groupDao.getById(groupId)
|
val group = groupDao.getById(groupId) ?: run {
|
||||||
if (group == null) {
|
|
||||||
Log.w(TAG, "sync: group $groupId not found in DB")
|
Log.w(TAG, "sync: group $groupId not found in DB")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
val season = ScheduleDates.currentSeason()
|
val season = ScheduleDates.currentSeason()
|
||||||
val range = dateRangeFor(group, season)
|
val range = dateRangeFor(group, season) ?: run {
|
||||||
if (range == null) {
|
Log.w(TAG, "sync: no date range for season $season (group ${group.id})")
|
||||||
Log.w(TAG, "sync: no date range for season $season (group ${group.id}, " +
|
|
||||||
"autumn=[${group.autumnFirst}, ${group.autumnSecond}], " +
|
|
||||||
"spring=[${group.springFirst}, ${group.springSecond}])")
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
Log.d(TAG, "sync: season=$season range=[${range.first}, ${range.second}]")
|
Log.d(TAG, "sync: season=$season range=[${range.first}, ${range.second}]")
|
||||||
@@ -55,17 +67,26 @@ class ScheduleRepositoryImpl(
|
|||||||
val days = PdfScheduleParser.parse(bytes)
|
val days = PdfScheduleParser.parse(bytes)
|
||||||
Log.d(TAG, "sync: parsed ${days.size} days")
|
Log.d(TAG, "sync: parsed ${days.size} days")
|
||||||
|
|
||||||
val entity = ScheduleEntity(
|
val weekStart = ScheduleDates.asDate(range.first).toEpochDay()
|
||||||
|
val weekEnd = ScheduleDates.asDate(range.second).toEpochDay()
|
||||||
|
|
||||||
|
val existing = scheduleDao.getSchedules(groupId)
|
||||||
|
.firstOrNull { it.season == season && it.weekStart == weekStart }
|
||||||
|
val scheduleId = if (existing != null) existing.id
|
||||||
|
else scheduleDao.insertSchedule(
|
||||||
|
ScheduleEntity(
|
||||||
groupId = groupId,
|
groupId = groupId,
|
||||||
season = season.name,
|
season = season,
|
||||||
dateRangeFirst = range.first,
|
weekStart = weekStart,
|
||||||
dateRangeSecond = range.second,
|
weekEnd = weekEnd,
|
||||||
weeksJson = codec.toJson(days),
|
|
||||||
lastSyncedAt = now()
|
lastSyncedAt = now()
|
||||||
)
|
)
|
||||||
scheduleDao.upsert(entity)
|
)
|
||||||
Log.d(TAG, "sync: upserted schedule for $groupId")
|
replaceDays(scheduleId, days)
|
||||||
entity.toDomain()
|
|
||||||
|
scheduleDao.trim(groupId, MAX_PERIODS)
|
||||||
|
|
||||||
|
getSchedule(groupId)
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Log.e(TAG, "sync: failed for $groupId (${e::class.simpleName}: ${e.message})", e)
|
Log.e(TAG, "sync: failed for $groupId (${e::class.simpleName}: ${e.message})", e)
|
||||||
null
|
null
|
||||||
@@ -73,33 +94,126 @@ class ScheduleRepositoryImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun isStale(groupId: String): Boolean {
|
override suspend fun isStale(groupId: String): Boolean {
|
||||||
val entity = scheduleDao.get(groupId) ?: return true
|
val schedules = scheduleDao.getSchedules(groupId)
|
||||||
return now() - entity.lastSyncedAt > STALE_WINDOW_MS
|
val latest = schedules.maxByOrNull { it.weekStart } ?: return true
|
||||||
|
return now() - latest.lastSyncedAt > STALE_WINDOW_MS
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun clear(groupId: String) = scheduleDao.delete(groupId)
|
override suspend fun clearPeriod(groupId: String, period: SchedulePeriod) {
|
||||||
|
scheduleDao.deleteSchedule(
|
||||||
|
groupId = groupId,
|
||||||
|
season = period.season.name,
|
||||||
|
weekStart = period.weekStart.toEpochDay()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun clearAll(groupId: String) = scheduleDao.deleteAll(groupId)
|
||||||
|
|
||||||
|
private suspend fun replaceDays(scheduleId: Long, days: List<Day>) {
|
||||||
|
scheduleDao.deleteDaysForSchedule(scheduleId)
|
||||||
|
val dayEntities = days.map { DayEntity(scheduleId = scheduleId, date = it.date.toEpochDay()) }
|
||||||
|
val dayIdByDate = mutableMapOf<LocalDate, Long>()
|
||||||
|
scheduleDao.insertDays(dayEntities)
|
||||||
|
for (day in dayEntities) {
|
||||||
|
dayIdByDate[LocalDate.ofEpochDay(day.date)] = day.id
|
||||||
|
}
|
||||||
|
val lessons = days.flatMap { day ->
|
||||||
|
day.intervals.flatMapIndexed { slotIndex, slot ->
|
||||||
|
slot.lessons.mapIndexed { lessonIndex, lesson ->
|
||||||
|
LessonEntity(
|
||||||
|
dayId = dayIdByDate.getValue(day.date),
|
||||||
|
date = day.date.toEpochDay(),
|
||||||
|
slotIndex = slotIndex,
|
||||||
|
start = slot.start.toMinutesOfDay(),
|
||||||
|
end = slot.end.toMinutesOfDay(),
|
||||||
|
lessonIndex = lessonIndex,
|
||||||
|
raw = lesson.raw,
|
||||||
|
room = lesson.room,
|
||||||
|
groupName = lesson.group,
|
||||||
|
subgroup = lesson.subgroup,
|
||||||
|
subject = lesson.subject,
|
||||||
|
type = lesson.type,
|
||||||
|
teacher = lesson.teacher
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scheduleDao.insertLessons(lessons)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildSchedule(
|
||||||
|
schedules: List<ScheduleEntity>,
|
||||||
|
dayEntities: List<DayEntity>,
|
||||||
|
lessonEntities: List<LessonEntity>
|
||||||
|
): Schedule? {
|
||||||
|
if (schedules.isEmpty()) return null
|
||||||
|
val groupId = schedules.first().groupId
|
||||||
|
|
||||||
|
val periods = schedules.sortedBy { it.weekStart }.map {
|
||||||
|
SchedulePeriod(
|
||||||
|
weekStart = LocalDate.ofEpochDay(it.weekStart),
|
||||||
|
weekEnd = LocalDate.ofEpochDay(it.weekEnd),
|
||||||
|
season = it.season
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val lessonsByDay = lessonEntities.groupBy { it.dayId }
|
||||||
|
val days = dayEntities
|
||||||
|
.sortedBy { it.date }
|
||||||
|
.map { day ->
|
||||||
|
val lessons = lessonsByDay[day.id].orEmpty()
|
||||||
|
.groupBy { it.slotIndex }
|
||||||
|
.toSortedMap()
|
||||||
|
.map { (_, slotLessons) ->
|
||||||
|
TimeSlot(
|
||||||
|
start = LocalTime.of(slotLessons.first().start / 60, slotLessons.first().start % 60),
|
||||||
|
end = LocalTime.of(slotLessons.first().end / 60, slotLessons.first().end % 60),
|
||||||
|
lessons = slotLessons
|
||||||
|
.sortedBy { it.lessonIndex }
|
||||||
|
.map { it.toDomain() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Day(date = LocalDate.ofEpochDay(day.date), intervals = lessons)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Schedule(groupId = groupId, periods = periods, days = days)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun LessonEntity.toDomain(): Lesson = Lesson(
|
||||||
|
raw = raw,
|
||||||
|
room = room,
|
||||||
|
group = groupName,
|
||||||
|
subgroup = subgroup,
|
||||||
|
subject = subject,
|
||||||
|
type = type,
|
||||||
|
teacher = teacher
|
||||||
|
)
|
||||||
|
|
||||||
private fun dateRangeFor(group: GroupEntity, season: Season): DateRange? {
|
private fun dateRangeFor(group: GroupEntity, season: Season): DateRange? {
|
||||||
return when (season) {
|
return when (season) {
|
||||||
Season.AUTUMN -> if (group.autumnFirst.isNotEmpty() && group.autumnSecond.isNotEmpty()) {
|
Season.AUTUMN -> if (group.autumnFirst != null && group.autumnSecond != null) {
|
||||||
DateRange(group.autumnFirst, group.autumnSecond)
|
DateRange(
|
||||||
|
LocalDate.ofEpochDay(group.autumnFirst).format(RANGE_FMT),
|
||||||
|
LocalDate.ofEpochDay(group.autumnSecond).format(RANGE_FMT)
|
||||||
|
)
|
||||||
} else null
|
} else null
|
||||||
Season.SPRING -> if (group.springFirst.isNotEmpty() && group.springSecond.isNotEmpty()) {
|
Season.SPRING -> if (group.springFirst != null && group.springSecond != null) {
|
||||||
DateRange(group.springFirst, group.springSecond)
|
DateRange(
|
||||||
|
LocalDate.ofEpochDay(group.springFirst).format(RANGE_FMT),
|
||||||
|
LocalDate.ofEpochDay(group.springSecond).format(RANGE_FMT)
|
||||||
|
)
|
||||||
} else null
|
} else null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun LocalTime.toMinutesOfDay(): Int = hour * 60 + minute
|
||||||
|
|
||||||
private fun now(): Long = System.currentTimeMillis()
|
private fun now(): Long = System.currentTimeMillis()
|
||||||
|
|
||||||
private fun ScheduleEntity.toDomain(): Schedule = Schedule(
|
|
||||||
groupId = groupId,
|
|
||||||
season = Season.valueOf(season),
|
|
||||||
days = codec.fromJson(weeksJson)
|
|
||||||
)
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val STALE_WINDOW_MS = 12 * 60 * 60 * 1000L
|
private const val STALE_WINDOW_MS = 12 * 60 * 60 * 1000L
|
||||||
|
private const val MAX_PERIODS = 3
|
||||||
private const val TAG = "ScheduleRepo"
|
private const val TAG = "ScheduleRepo"
|
||||||
|
private val RANGE_FMT = java.time.format.DateTimeFormatter.ofPattern("ddMMyyyy")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package agzam.vyatsu.di
|
package agzam.vyatsu.di
|
||||||
|
|
||||||
import agzam.vyatsu.data.local.AppDatabase
|
import agzam.vyatsu.data.local.AppDatabase
|
||||||
import agzam.vyatsu.data.local.ScheduleCodec
|
|
||||||
import agzam.vyatsu.data.remote.GroupsParser
|
import agzam.vyatsu.data.remote.GroupsParser
|
||||||
import agzam.vyatsu.data.remote.VyatsuHttpClient
|
import agzam.vyatsu.data.remote.VyatsuHttpClient
|
||||||
import agzam.vyatsu.data.repository.GroupRepositoryImpl
|
import agzam.vyatsu.data.repository.GroupRepositoryImpl
|
||||||
@@ -12,18 +11,15 @@ import agzam.vyatsu.domain.repository.PreferencesRepository
|
|||||||
import agzam.vyatsu.domain.repository.ScheduleRepository
|
import agzam.vyatsu.domain.repository.ScheduleRepository
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.room.Room
|
import androidx.room.Room
|
||||||
import com.squareup.moshi.Moshi
|
|
||||||
|
|
||||||
class AppContainer(context: Context) {
|
class AppContainer(context: Context) {
|
||||||
private val moshi: Moshi = Moshi.Builder().build()
|
|
||||||
|
|
||||||
private val database: AppDatabase = Room.databaseBuilder(
|
private val database: AppDatabase = Room.databaseBuilder(
|
||||||
context.applicationContext,
|
context.applicationContext,
|
||||||
AppDatabase::class.java,
|
AppDatabase::class.java,
|
||||||
"vyatsu_schedule.db"
|
"vyatsu_schedule.db"
|
||||||
).build()
|
)
|
||||||
|
.fallbackToDestructiveMigration(dropAllTables = true)
|
||||||
private val codec: ScheduleCodec = ScheduleCodec(moshi)
|
.build()
|
||||||
|
|
||||||
val groupRepository: GroupRepository by lazy {
|
val groupRepository: GroupRepository by lazy {
|
||||||
GroupRepositoryImpl(
|
GroupRepositoryImpl(
|
||||||
@@ -35,7 +31,6 @@ class AppContainer(context: Context) {
|
|||||||
ScheduleRepositoryImpl(
|
ScheduleRepositoryImpl(
|
||||||
scheduleDao = database.scheduleDao(),
|
scheduleDao = database.scheduleDao(),
|
||||||
groupDao = database.groupDao(),
|
groupDao = database.groupDao(),
|
||||||
codec = codec
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
package agzam.vyatsu.domain.model
|
package agzam.vyatsu.domain.model
|
||||||
|
|
||||||
/**
|
import java.time.LocalDate
|
||||||
* Расписание группы, как правило содержит 2 недели
|
|
||||||
* @author Agzam
|
data class SchedulePeriod(
|
||||||
*/
|
val weekStart: LocalDate,
|
||||||
|
val weekEnd: LocalDate,
|
||||||
|
val season: Season
|
||||||
|
)
|
||||||
|
|
||||||
data class Schedule(
|
data class Schedule(
|
||||||
|
|
||||||
val groupId: String,
|
val groupId: String,
|
||||||
val season: Season,
|
val periods: List<SchedulePeriod>,
|
||||||
|
|
||||||
/** Может содержать более 7 дней */
|
|
||||||
val days: List<Day>
|
val days: List<Day>
|
||||||
|
|
||||||
) {
|
) {
|
||||||
|
fun periodOf(date: LocalDate): SchedulePeriod? =
|
||||||
|
periods.firstOrNull { !date.isBefore(it.weekStart) && !date.isAfter(it.weekEnd) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package agzam.vyatsu.domain.repository
|
package agzam.vyatsu.domain.repository
|
||||||
|
|
||||||
import agzam.vyatsu.domain.model.Schedule
|
import agzam.vyatsu.domain.model.Schedule
|
||||||
|
import agzam.vyatsu.domain.model.SchedulePeriod
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
interface ScheduleRepository {
|
interface ScheduleRepository {
|
||||||
@@ -8,5 +9,6 @@ interface ScheduleRepository {
|
|||||||
suspend fun getSchedule(groupId: String): Schedule?
|
suspend fun getSchedule(groupId: String): Schedule?
|
||||||
suspend fun sync(groupId: String): Schedule?
|
suspend fun sync(groupId: String): Schedule?
|
||||||
suspend fun isStale(groupId: String): Boolean
|
suspend fun isStale(groupId: String): Boolean
|
||||||
suspend fun clear(groupId: String)
|
suspend fun clearPeriod(groupId: String, period: SchedulePeriod)
|
||||||
|
suspend fun clearAll(groupId: String)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,6 +94,17 @@ fun ScheduleScreen(viewModel: ScheduleViewModel, onSwitchGroup: () -> Unit) {
|
|||||||
"$dayLabel ${dayName(day.date.dayOfWeek)} · ${day.date.format(FULL_DATE)}",
|
"$dayLabel ${dayName(day.date.dayOfWeek)} · ${day.date.format(FULL_DATE)}",
|
||||||
style = MaterialTheme.typography.titleMedium
|
style = MaterialTheme.typography.titleMedium
|
||||||
)
|
)
|
||||||
|
Row(
|
||||||
|
Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
TextButton(onClick = { viewModel.clearSelectedPeriod() }) {
|
||||||
|
Text("Очистить неделю")
|
||||||
|
}
|
||||||
|
TextButton(onClick = { viewModel.clearAll() }) {
|
||||||
|
Text("Очистить всё")
|
||||||
|
}
|
||||||
|
}
|
||||||
HorizontalDivider(Modifier.padding(vertical = 8.dp))
|
HorizontalDivider(Modifier.padding(vertical = 8.dp))
|
||||||
|
|
||||||
DayChips(
|
DayChips(
|
||||||
|
|||||||
@@ -93,6 +93,23 @@ class ScheduleViewModel(
|
|||||||
_uiState.value = _uiState.value.copy(selectedIndex = index)
|
_uiState.value = _uiState.value.copy(selectedIndex = index)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun clearAll() {
|
||||||
|
val groupId = currentGroupId ?: return
|
||||||
|
viewModelScope.launch {
|
||||||
|
scheduleRepository.clearAll(groupId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearSelectedPeriod() {
|
||||||
|
val state = _uiState.value
|
||||||
|
val schedule = state.schedule ?: return
|
||||||
|
val day = schedule.days.getOrNull(state.selectedIndex) ?: return
|
||||||
|
val period = schedule.periodOf(day.date) ?: return
|
||||||
|
viewModelScope.launch {
|
||||||
|
scheduleRepository.clearPeriod(schedule.groupId, period)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun todayIndex(days: List<Day>): Int {
|
private fun todayIndex(days: List<Day>): Int {
|
||||||
if (days.isEmpty()) return 0
|
if (days.isEmpty()) return 0
|
||||||
val today = ScheduleDates.moscowToday()
|
val today = ScheduleDates.moscowToday()
|
||||||
|
|||||||
Reference in New Issue
Block a user