Add additional lessons to timetable (#1019)

This commit is contained in:
Mikołaj Pich 2020-12-27 14:06:07 +01:00 committed by GitHub
parent 295fd0fd90
commit 9763208688
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 2777 additions and 64 deletions

View File

@ -142,7 +142,7 @@ configurations.all {
} }
dependencies { dependencies {
implementation "io.github.wulkanowy:sdk:0.23.1" implementation "io.github.wulkanowy:sdk:6edc8531"
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1'

File diff suppressed because it is too large Load Diff

View File

@ -26,7 +26,7 @@ class TimetableLocalTest {
fun createDb() { fun createDb() {
testDb = Room.inMemoryDatabaseBuilder(ApplicationProvider.getApplicationContext(), AppDatabase::class.java) testDb = Room.inMemoryDatabaseBuilder(ApplicationProvider.getApplicationContext(), AppDatabase::class.java)
.build() .build()
timetableDb = TimetableLocal(testDb.timetableDao) timetableDb = TimetableLocal(testDb.timetableDao, testDb.timetableAdditionalDao)
} }
@After @After

View File

@ -46,7 +46,7 @@ class TimetableRepositoryTest {
fun initApi() { fun initApi() {
MockKAnnotations.init(this) MockKAnnotations.init(this)
testDb = Room.inMemoryDatabaseBuilder(getApplicationContext(), AppDatabase::class.java).build() testDb = Room.inMemoryDatabaseBuilder(getApplicationContext(), AppDatabase::class.java).build()
timetableLocal = TimetableLocal(testDb.timetableDao) timetableLocal = TimetableLocal(testDb.timetableDao, testDb.timetableAdditionalDao)
} }
@After @After
@ -65,12 +65,12 @@ class TimetableRepositoryTest {
)) ))
} }
coEvery { timetableRemote.getTimetable(student, semester, any(), any()) } returns listOf( coEvery { timetableRemote.getTimetable(student, semester, any(), any()) } returns (listOf(
createTimetableLocal(of(2019, 3, 5, 8, 0), 1, "", "Przyroda"), createTimetableLocal(of(2019, 3, 5, 8, 0), 1, "", "Przyroda"),
createTimetableLocal(of(2019, 3, 5, 8, 50), 2, "", "Religia"), createTimetableLocal(of(2019, 3, 5, 8, 50), 2, "", "Religia"),
createTimetableLocal(of(2019, 3, 5, 9, 40), 3, "", "W-F"), createTimetableLocal(of(2019, 3, 5, 9, 40), 3, "", "W-F"),
createTimetableLocal(of(2019, 3, 5, 10, 30), 4, "", "W-F") createTimetableLocal(of(2019, 3, 5, 10, 30), 4, "", "W-F")
) ) to emptyList())
val lessons = runBlocking { val lessons = runBlocking {
TimetableRepository(timetableLocal, timetableRemote, timetableNotificationSchedulerHelper).getTimetable( TimetableRepository(timetableLocal, timetableRemote, timetableNotificationSchedulerHelper).getTimetable(
@ -79,7 +79,7 @@ class TimetableRepositoryTest {
start = LocalDate.of(2019, 3, 5), start = LocalDate.of(2019, 3, 5),
end = LocalDate.of(2019, 3, 5), end = LocalDate.of(2019, 3, 5),
forceRefresh = true forceRefresh = true
).filter { it.status == Status.SUCCESS }.first().data.orEmpty() ).filter { it.status == Status.SUCCESS }.first().data?.first.orEmpty()
} }
assertEquals(4, lessons.size) assertEquals(4, lessons.size)
@ -108,7 +108,7 @@ class TimetableRepositoryTest {
) )
runBlocking { timetableLocal.saveTimetable(list) } runBlocking { timetableLocal.saveTimetable(list) }
coEvery { timetableRemote.getTimetable(student, semester, any(), any()) } returns listOf( coEvery { timetableRemote.getTimetable(student, semester, any(), any()) } returns (listOf(
createTimetableLocal(of(2019, 12, 23, 8, 0), 1, "123", "Matematyka", "Paweł Poniedziałkowski", false), createTimetableLocal(of(2019, 12, 23, 8, 0), 1, "123", "Matematyka", "Paweł Poniedziałkowski", false),
createTimetableLocal(of(2019, 12, 23, 8, 50), 2, "124", "Matematyka", "Jakub Wtorkowski", true), createTimetableLocal(of(2019, 12, 23, 8, 50), 2, "124", "Matematyka", "Jakub Wtorkowski", true),
createTimetableLocal(of(2019, 12, 23, 9, 40), 3, "125", "Język polski", "Joanna Poniedziałkowska", false), createTimetableLocal(of(2019, 12, 23, 9, 40), 3, "125", "Język polski", "Joanna Poniedziałkowska", false),
@ -123,7 +123,7 @@ class TimetableRepositoryTest {
createTimetableLocal(of(2019, 12, 25, 8, 50), 2, "124", "Matematyka", "Paweł Czwartkowski", true), createTimetableLocal(of(2019, 12, 25, 8, 50), 2, "124", "Matematyka", "Paweł Czwartkowski", true),
createTimetableLocal(of(2019, 12, 25, 9, 40), 3, "125", "Matematyka", "Paweł Środowski", false), createTimetableLocal(of(2019, 12, 25, 9, 40), 3, "125", "Matematyka", "Paweł Środowski", false),
createTimetableLocal(of(2019, 12, 25, 10, 40), 4, "126", "Matematyka", "Paweł Czwartkowski", true) createTimetableLocal(of(2019, 12, 25, 10, 40), 4, "126", "Matematyka", "Paweł Czwartkowski", true)
) ) to emptyList())
val lessons = runBlocking { val lessons = runBlocking {
TimetableRepository(timetableLocal, timetableRemote, timetableNotificationSchedulerHelper).getTimetable( TimetableRepository(timetableLocal, timetableRemote, timetableNotificationSchedulerHelper).getTimetable(
@ -132,7 +132,7 @@ class TimetableRepositoryTest {
start = LocalDate.of(2019, 12, 23), start = LocalDate.of(2019, 12, 23),
end = LocalDate.of(2019, 12, 25), end = LocalDate.of(2019, 12, 25),
forceRefresh = true forceRefresh = true
).filter { it.status == Status.SUCCESS }.first().data.orEmpty() ).filter { it.status == Status.SUCCESS }.first().data?.first.orEmpty()
} }
assertEquals(12, lessons.size) assertEquals(12, lessons.size)

View File

@ -162,4 +162,8 @@ internal class RepositoryModule {
@Singleton @Singleton
@Provides @Provides
fun provideConferenceDao(database: AppDatabase) = database.conferenceDao fun provideConferenceDao(database: AppDatabase) = database.conferenceDao
@Singleton
@Provides
fun provideTimetableAdditionalDao(database: AppDatabase) = database.timetableAdditionalDao
} }

View File

@ -30,6 +30,7 @@ import io.github.wulkanowy.data.db.dao.SemesterDao
import io.github.wulkanowy.data.db.dao.StudentDao import io.github.wulkanowy.data.db.dao.StudentDao
import io.github.wulkanowy.data.db.dao.SubjectDao import io.github.wulkanowy.data.db.dao.SubjectDao
import io.github.wulkanowy.data.db.dao.TeacherDao import io.github.wulkanowy.data.db.dao.TeacherDao
import io.github.wulkanowy.data.db.dao.TimetableAdditionalDao
import io.github.wulkanowy.data.db.dao.TimetableDao import io.github.wulkanowy.data.db.dao.TimetableDao
import io.github.wulkanowy.data.db.entities.Attendance import io.github.wulkanowy.data.db.entities.Attendance
import io.github.wulkanowy.data.db.entities.AttendanceSummary import io.github.wulkanowy.data.db.entities.AttendanceSummary
@ -55,6 +56,7 @@ import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.Subject import io.github.wulkanowy.data.db.entities.Subject
import io.github.wulkanowy.data.db.entities.Teacher import io.github.wulkanowy.data.db.entities.Teacher
import io.github.wulkanowy.data.db.entities.Timetable import io.github.wulkanowy.data.db.entities.Timetable
import io.github.wulkanowy.data.db.entities.TimetableAdditional
import io.github.wulkanowy.data.db.migrations.Migration10 import io.github.wulkanowy.data.db.migrations.Migration10
import io.github.wulkanowy.data.db.migrations.Migration11 import io.github.wulkanowy.data.db.migrations.Migration11
import io.github.wulkanowy.data.db.migrations.Migration12 import io.github.wulkanowy.data.db.migrations.Migration12
@ -77,6 +79,7 @@ import io.github.wulkanowy.data.db.migrations.Migration27
import io.github.wulkanowy.data.db.migrations.Migration28 import io.github.wulkanowy.data.db.migrations.Migration28
import io.github.wulkanowy.data.db.migrations.Migration29 import io.github.wulkanowy.data.db.migrations.Migration29
import io.github.wulkanowy.data.db.migrations.Migration3 import io.github.wulkanowy.data.db.migrations.Migration3
import io.github.wulkanowy.data.db.migrations.Migration30
import io.github.wulkanowy.data.db.migrations.Migration4 import io.github.wulkanowy.data.db.migrations.Migration4
import io.github.wulkanowy.data.db.migrations.Migration5 import io.github.wulkanowy.data.db.migrations.Migration5
import io.github.wulkanowy.data.db.migrations.Migration6 import io.github.wulkanowy.data.db.migrations.Migration6
@ -112,6 +115,7 @@ import javax.inject.Singleton
Teacher::class, Teacher::class,
School::class, School::class,
Conference::class, Conference::class,
TimetableAdditional::class,
], ],
version = AppDatabase.VERSION_SCHEMA, version = AppDatabase.VERSION_SCHEMA,
exportSchema = true exportSchema = true
@ -120,7 +124,7 @@ import javax.inject.Singleton
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
companion object { companion object {
const val VERSION_SCHEMA = 29 const val VERSION_SCHEMA = 30
fun getMigrations(sharedPrefProvider: SharedPrefProvider): Array<Migration> { fun getMigrations(sharedPrefProvider: SharedPrefProvider): Array<Migration> {
return arrayOf( return arrayOf(
@ -151,7 +155,8 @@ abstract class AppDatabase : RoomDatabase() {
Migration26(), Migration26(),
Migration27(), Migration27(),
Migration28(), Migration28(),
Migration29() Migration29(),
Migration30(),
) )
} }
@ -212,4 +217,6 @@ abstract class AppDatabase : RoomDatabase() {
abstract val schoolDao: SchoolDao abstract val schoolDao: SchoolDao
abstract val conferenceDao: ConferenceDao abstract val conferenceDao: ConferenceDao
abstract val timetableAdditionalDao: TimetableAdditionalDao
} }

View File

@ -0,0 +1,16 @@
package io.github.wulkanowy.data.db.dao
import androidx.room.Dao
import androidx.room.Query
import io.github.wulkanowy.data.db.entities.TimetableAdditional
import kotlinx.coroutines.flow.Flow
import java.time.LocalDate
import javax.inject.Singleton
@Dao
@Singleton
interface TimetableAdditionalDao : BaseDao<TimetableAdditional> {
@Query("SELECT * FROM TimetableAdditional WHERE diary_id = :diaryId AND student_id = :studentId AND date >= :from AND date <= :end")
fun loadAll(diaryId: Int, studentId: Int, from: LocalDate, end: LocalDate): Flow<List<TimetableAdditional>>
}

View File

@ -0,0 +1,30 @@
package io.github.wulkanowy.data.db.entities
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import java.io.Serializable
import java.time.LocalDate
import java.time.LocalDateTime
@Entity(tableName = "TimetableAdditional")
data class TimetableAdditional(
@ColumnInfo(name = "student_id")
val studentId: Int,
@ColumnInfo(name = "diary_id")
val diaryId: Int,
val start: LocalDateTime,
val end: LocalDateTime,
val date: LocalDate,
val subject: String,
) : Serializable {
@PrimaryKey(autoGenerate = true)
var id: Long = 0
}

View File

@ -0,0 +1,21 @@
package io.github.wulkanowy.data.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration30 : Migration(29, 30) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("""
CREATE TABLE TimetableAdditional (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
student_id INTEGER NOT NULL,
diary_id INTEGER NOT NULL,
start INTEGER NOT NULL,
`end` INTEGER NOT NULL,
date INTEGER NOT NULL,
subject TEXT NOT NULL
)
""")
}
}

View File

@ -21,11 +21,15 @@ class GradeRepository @Inject constructor(
fun getGrades(student: Student, semester: Semester, forceRefresh: Boolean, notify: Boolean = false) = networkBoundResource( fun getGrades(student: Student, semester: Semester, forceRefresh: Boolean, notify: Boolean = false) = networkBoundResource(
shouldFetch = { (details, summaries) -> details.isEmpty() || summaries.isEmpty() || forceRefresh }, shouldFetch = { (details, summaries) -> details.isEmpty() || summaries.isEmpty() || forceRefresh },
query = { local.getGradesDetails(semester).combine(local.getGradesSummary(semester)) { details, summaries -> details to summaries } }, query = {
local.getGradesDetails(semester).combine(local.getGradesSummary(semester)) { details, summaries ->
details to summaries
}
},
fetch = { remote.getGrades(student, semester) }, fetch = { remote.getGrades(student, semester) },
saveFetchResult = { old, new -> saveFetchResult = { (oldDetails, oldSummary), (newDetails, newSummary) ->
refreshGradeDetails(student, old.first, new.first, notify) refreshGradeDetails(student, oldDetails, newDetails, notify)
refreshGradeSummaries(old.second, new.second, notify) refreshGradeSummaries(oldSummary, newSummary, notify)
} }
) )

View File

@ -1,25 +1,42 @@
package io.github.wulkanowy.data.repositories.timetable package io.github.wulkanowy.data.repositories.timetable
import io.github.wulkanowy.data.db.dao.TimetableAdditionalDao
import io.github.wulkanowy.data.db.dao.TimetableDao import io.github.wulkanowy.data.db.dao.TimetableDao
import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Timetable import io.github.wulkanowy.data.db.entities.Timetable
import io.github.wulkanowy.data.db.entities.TimetableAdditional
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import java.time.LocalDate import java.time.LocalDate
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
class TimetableLocal @Inject constructor(private val timetableDb: TimetableDao) { class TimetableLocal @Inject constructor(
private val timetableDb: TimetableDao,
private val timetableAdditionalDb: TimetableAdditionalDao
) {
suspend fun saveTimetable(timetables: List<Timetable>) { suspend fun saveTimetable(timetables: List<Timetable>) {
timetableDb.insertAll(timetables) timetableDb.insertAll(timetables)
} }
suspend fun saveTimetableAdditional(additional: List<TimetableAdditional>) {
timetableAdditionalDb.insertAll(additional)
}
suspend fun deleteTimetable(timetables: List<Timetable>) { suspend fun deleteTimetable(timetables: List<Timetable>) {
timetableDb.deleteAll(timetables) timetableDb.deleteAll(timetables)
} }
suspend fun deleteTimetableAdditional(additional: List<TimetableAdditional>) {
timetableAdditionalDb.deleteAll(additional)
}
fun getTimetable(semester: Semester, startDate: LocalDate, endDate: LocalDate): Flow<List<Timetable>> { fun getTimetable(semester: Semester, startDate: LocalDate, endDate: LocalDate): Flow<List<Timetable>> {
return timetableDb.loadAll(semester.diaryId, semester.studentId, startDate, endDate) return timetableDb.loadAll(semester.diaryId, semester.studentId, startDate, endDate)
} }
fun getTimetableAdditional(semester: Semester, startDate: LocalDate, endDate: LocalDate): Flow<List<TimetableAdditional>> {
return timetableAdditionalDb.loadAll(semester.diaryId, semester.studentId, startDate, endDate)
}
} }

View File

@ -3,6 +3,7 @@ package io.github.wulkanowy.data.repositories.timetable
import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.Timetable import io.github.wulkanowy.data.db.entities.Timetable
import io.github.wulkanowy.data.db.entities.TimetableAdditional
import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.init import io.github.wulkanowy.utils.init
import java.time.LocalDate import java.time.LocalDate
@ -12,29 +13,40 @@ import javax.inject.Singleton
@Singleton @Singleton
class TimetableRemote @Inject constructor(private val sdk: Sdk) { class TimetableRemote @Inject constructor(private val sdk: Sdk) {
suspend fun getTimetable(student: Student, semester: Semester, startDate: LocalDate, endDate: LocalDate): List<Timetable> { suspend fun getTimetable(student: Student, semester: Semester, startDate: LocalDate, endDate: LocalDate): Pair<List<Timetable>, List<TimetableAdditional>> {
return sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) val (normal, additional) = sdk.init(student)
.getTimetable(startDate, endDate).first .switchDiary(semester.diaryId, semester.schoolYear)
.map { .getTimetable(startDate, endDate)
Timetable(
studentId = semester.studentId, return normal.map {
diaryId = semester.diaryId, Timetable(
number = it.number, studentId = semester.studentId,
start = it.start, diaryId = semester.diaryId,
end = it.end, number = it.number,
date = it.date, start = it.start,
subject = it.subject, end = it.end,
subjectOld = it.subjectOld, date = it.date,
group = it.group, subject = it.subject,
room = it.room, subjectOld = it.subjectOld,
roomOld = it.roomOld, group = it.group,
teacher = it.teacher, room = it.room,
teacherOld = it.teacherOld, roomOld = it.roomOld,
info = it.info, teacher = it.teacher,
isStudentPlan = it.studentPlan, teacherOld = it.teacherOld,
changes = it.changes, info = it.info,
canceled = it.canceled isStudentPlan = it.studentPlan,
) changes = it.changes,
} canceled = it.canceled
)
} to additional.map {
TimetableAdditional(
studentId = semester.studentId,
diaryId = semester.diaryId,
subject = it.subject,
date = it.date,
start = it.start,
end = it.end
)
}
} }
} }

View File

@ -2,11 +2,14 @@ package io.github.wulkanowy.data.repositories.timetable
import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.Timetable
import io.github.wulkanowy.data.db.entities.TimetableAdditional
import io.github.wulkanowy.services.alarm.TimetableNotificationSchedulerHelper import io.github.wulkanowy.services.alarm.TimetableNotificationSchedulerHelper
import io.github.wulkanowy.utils.monday import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.sunday import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.uniqueSubtract import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import java.time.LocalDate import java.time.LocalDate
import javax.inject.Inject import javax.inject.Inject
@ -19,23 +22,44 @@ class TimetableRepository @Inject constructor(
private val schedulerHelper: TimetableNotificationSchedulerHelper private val schedulerHelper: TimetableNotificationSchedulerHelper
) { ) {
fun getTimetable(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean) = networkBoundResource( fun getTimetable(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean, refreshAdditional: Boolean = false) = networkBoundResource(
shouldFetch = { it.isEmpty() || forceRefresh }, shouldFetch = { (timetable, additional) -> timetable.isEmpty() || (additional.isEmpty() && refreshAdditional) || forceRefresh },
query = { local.getTimetable(semester, start.monday, end.sunday).map { schedulerHelper.scheduleNotifications(it, student); it } }, query = {
fetch = { remote.getTimetable(student, semester, start.monday, end.sunday) }, local.getTimetable(semester, start.monday, end.sunday)
saveFetchResult = { old, new -> .map { schedulerHelper.scheduleNotifications(it, student); it }
local.deleteTimetable(old.uniqueSubtract(new).also { schedulerHelper.cancelScheduled(it) }) .combine(local.getTimetableAdditional(semester, start.monday, end.sunday)) { timetable, additional ->
local.saveTimetable(new.uniqueSubtract(old).also { schedulerHelper.scheduleNotifications(it, student) }.map { item -> timetable to additional
item.also { new ->
old.singleOrNull { new.start == it.start }?.let { old ->
return@map new.copy(
room = if (new.room.isEmpty()) old.room else new.room,
teacher = if (new.teacher.isEmpty() && !new.changes && !old.changes) old.teacher else new.teacher
)
}
} }
})
}, },
filterResult = { it.filter { item -> item.date in start..end } } fetch = { remote.getTimetable(student, semester, start.monday, end.sunday) },
saveFetchResult = { (oldTimetable, oldAdditional), (newTimetable, newAdditional) ->
refreshTimetable(student, oldTimetable, newTimetable)
refreshAdditional(oldAdditional, newAdditional)
},
filterResult = { (timetable, additional) ->
timetable.filter { item ->
item.date in start..end
} to additional.filter { item -> item.date in start..end }
}
) )
private suspend fun refreshTimetable(student: Student, old: List<Timetable>, new: List<Timetable>) {
local.deleteTimetable(old.uniqueSubtract(new).also { schedulerHelper.cancelScheduled(it) })
local.saveTimetable(new.uniqueSubtract(old).also { schedulerHelper.scheduleNotifications(it, student) }.map { item ->
item.also { new ->
old.singleOrNull { new.start == it.start }?.let { old ->
return@map new.copy(
room = if (new.room.isEmpty()) old.room else new.room,
teacher = if (new.teacher.isEmpty() && !new.changes && !old.changes) old.teacher else new.teacher
)
}
}
})
}
private suspend fun refreshAdditional(old: List<TimetableAdditional>, new: List<TimetableAdditional>) {
local.deleteTimetableAdditional(old.uniqueSubtract(new))
local.saveTimetableAdditional(new.uniqueSubtract(old))
}
} }

View File

@ -16,6 +16,7 @@ import io.github.wulkanowy.databinding.FragmentTimetableBinding
import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.main.MainActivity import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.modules.timetable.additional.AdditionalLessonsFragment
import io.github.wulkanowy.ui.modules.timetable.completed.CompletedLessonsFragment import io.github.wulkanowy.ui.modules.timetable.completed.CompletedLessonsFragment
import io.github.wulkanowy.ui.widgets.DividerItemDecoration import io.github.wulkanowy.ui.widgets.DividerItemDecoration
import io.github.wulkanowy.utils.SchooldaysRangeLimiter import io.github.wulkanowy.utils.SchooldaysRangeLimiter
@ -84,8 +85,11 @@ class TimetableFragment : BaseFragment<FragmentTimetableBinding>(R.layout.fragme
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
return if (item.itemId == R.id.timetableMenuCompletedLessons) presenter.onCompletedLessonsSwitchSelected() return when (item.itemId) {
else false R.id.timetableMenuAdditionalLessons -> presenter.onAdditionalLessonsSwitchSelected()
R.id.timetableMenuCompletedLessons -> presenter.onCompletedLessonsSwitchSelected()
else -> false
}
} }
override fun updateData(data: List<Timetable>, showWholeClassPlanType: String, showGroupsInPlanType: Boolean, showTimetableTimers: Boolean) { override fun updateData(data: List<Timetable>, showWholeClassPlanType: String, showGroupsInPlanType: Boolean, showTimetableTimers: Boolean) {
@ -176,6 +180,10 @@ class TimetableFragment : BaseFragment<FragmentTimetableBinding>(R.layout.fragme
} }
} }
override fun openAdditionalLessonsView() {
(activity as? MainActivity)?.pushView(AdditionalLessonsFragment.newInstance())
}
override fun openCompletedLessonsView() { override fun openCompletedLessonsView() {
(activity as? MainActivity)?.pushView(CompletedLessonsFragment.newInstance()) (activity as? MainActivity)?.pushView(CompletedLessonsFragment.newInstance())
} }

View File

@ -109,6 +109,11 @@ class TimetablePresenter @Inject constructor(
view?.showTimetableDialog(lesson) view?.showTimetableDialog(lesson)
} }
fun onAdditionalLessonsSwitchSelected(): Boolean {
view?.openAdditionalLessonsView()
return true
}
fun onCompletedLessonsSwitchSelected(): Boolean { fun onCompletedLessonsSwitchSelected(): Boolean {
view?.openCompletedLessonsView() view?.openCompletedLessonsView()
return true return true
@ -144,18 +149,18 @@ class TimetablePresenter @Inject constructor(
showWholeClassPlanType = prefRepository.showWholeClassPlan, showWholeClassPlanType = prefRepository.showWholeClassPlan,
showGroupsInPlanType = prefRepository.showGroupsInPlan, showGroupsInPlanType = prefRepository.showGroupsInPlan,
showTimetableTimers = prefRepository.showTimetableTimers, showTimetableTimers = prefRepository.showTimetableTimers,
data = it.data!! data = it.data!!.first
.filter { item -> if (prefRepository.showWholeClassPlan == "no") item.isStudentPlan else true } .filter { item -> if (prefRepository.showWholeClassPlan == "no") item.isStudentPlan else true }
.sortedWith(compareBy({ item -> item.number }, { item -> !item.isStudentPlan })) .sortedWith(compareBy({ item -> item.number }, { item -> !item.isStudentPlan }))
) )
showEmpty(it.data.isEmpty()) showEmpty(it.data.first.isEmpty())
showErrorView(false) showErrorView(false)
showContent(it.data.isNotEmpty()) showContent(it.data.first.isNotEmpty())
} }
analytics.logEvent( analytics.logEvent(
"load_data", "load_data",
"type" to "timetable", "type" to "timetable",
"items" to it.data!!.size "items" to it.data!!.first.size
) )
} }
Status.ERROR -> { Status.ERROR -> {

View File

@ -44,5 +44,7 @@ interface TimetableView : BaseView {
fun popView() fun popView()
fun openAdditionalLessonsView()
fun openCompletedLessonsView() fun openCompletedLessonsView()
} }

View File

@ -0,0 +1,35 @@
package io.github.wulkanowy.ui.modules.timetable.additional
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.data.db.entities.TimetableAdditional
import io.github.wulkanowy.databinding.ItemTimetableAdditionalBinding
import io.github.wulkanowy.utils.toFormattedString
import javax.inject.Inject
class AdditionalLessonsAdapter @Inject constructor() :
RecyclerView.Adapter<AdditionalLessonsAdapter.ItemViewHolder>() {
var items = emptyList<TimetableAdditional>()
override fun getItemCount() = items.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ItemViewHolder(
ItemTimetableAdditionalBinding.inflate(LayoutInflater.from(parent.context), parent, false)
)
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
val item = items[position]
with(holder.binding) {
additionalLessonItemTime.text = "${item.start.toFormattedString("HH:mm")} - ${item.end.toFormattedString("HH:mm")}"
additionalLessonItemSubject.text = item.subject
}
}
class ItemViewHolder(val binding: ItemTimetableAdditionalBinding) :
RecyclerView.ViewHolder(binding.root)
}

View File

@ -0,0 +1,144 @@
package io.github.wulkanowy.ui.modules.timetable.additional
import android.os.Bundle
import android.view.View
import androidx.recyclerview.widget.LinearLayoutManager
import com.wdullaer.materialdatetimepicker.date.DatePickerDialog
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.TimetableAdditional
import io.github.wulkanowy.databinding.FragmentTimetableAdditionalBinding
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.widgets.DividerItemDecoration
import io.github.wulkanowy.utils.SchooldaysRangeLimiter
import io.github.wulkanowy.utils.dpToPx
import java.time.LocalDate
import javax.inject.Inject
@AndroidEntryPoint
class AdditionalLessonsFragment :
BaseFragment<FragmentTimetableAdditionalBinding>(R.layout.fragment_timetable_additional),
AdditionalLessonsView, MainView.TitledView {
@Inject
lateinit var presenter: AdditionalLessonsPresenter
@Inject
lateinit var additionalLessonsAdapter: AdditionalLessonsAdapter
companion object {
private const val SAVED_DATE_KEY = "CURRENT_DATE"
fun newInstance() = AdditionalLessonsFragment()
}
override val titleStringId get() = R.string.additional_lessons_title
override val isViewEmpty get() = additionalLessonsAdapter.items.isEmpty()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentTimetableAdditionalBinding.bind(view)
messageContainer = binding.additionalLessonsRecycler
presenter.onAttachView(this, savedInstanceState?.getLong(SAVED_DATE_KEY))
}
override fun initView() {
with(binding.additionalLessonsRecycler) {
layoutManager = LinearLayoutManager(context)
adapter = additionalLessonsAdapter
addItemDecoration(DividerItemDecoration(context))
}
with(binding) {
additionalLessonsSwipe.setOnRefreshListener(presenter::onSwipeRefresh)
additionalLessonsErrorRetry.setOnClickListener { presenter.onRetry() }
additionalLessonsPreviousButton.setOnClickListener { presenter.onPreviousDay() }
additionalLessonsNavDate.setOnClickListener { presenter.onPickDate() }
additionalLessonsNextButton.setOnClickListener { presenter.onNextDay() }
additionalLessonsNavContainer.setElevationCompat(requireContext().dpToPx(8f))
}
}
override fun updateData(data: List<TimetableAdditional>) {
with(additionalLessonsAdapter) {
items = data
notifyDataSetChanged()
}
}
override fun clearData() {
with(additionalLessonsAdapter) {
items = emptyList()
notifyDataSetChanged()
}
}
override fun updateNavigationDay(date: String) {
binding.additionalLessonsNavDate.text = date
}
override fun hideRefresh() {
binding.additionalLessonsSwipe.isRefreshing = false
}
override fun showEmpty(show: Boolean) {
binding.additionalLessonsEmpty.visibility = if (show) View.VISIBLE else View.GONE
}
override fun showErrorView(show: Boolean) {
binding.additionalLessonsError.visibility = if (show) View.VISIBLE else View.GONE
}
override fun setErrorDetails(message: String) {
binding.additionalLessonsErrorMessage.text = message
}
override fun showProgress(show: Boolean) {
binding.additionalLessonsProgress.visibility = if (show) View.VISIBLE else View.GONE
}
override fun enableSwipe(enable: Boolean) {
binding.additionalLessonsSwipe.isEnabled = enable
}
override fun showContent(show: Boolean) {
binding.additionalLessonsRecycler.visibility = if (show) View.VISIBLE else View.GONE
}
override fun showPreButton(show: Boolean) {
binding.additionalLessonsPreviousButton.visibility = if (show) View.VISIBLE else View.INVISIBLE
}
override fun showNextButton(show: Boolean) {
binding.additionalLessonsNextButton.visibility = if (show) View.VISIBLE else View.INVISIBLE
}
override fun showDatePickerDialog(currentDate: LocalDate) {
val dateSetListener = DatePickerDialog.OnDateSetListener { _, year, month, dayOfMonth ->
presenter.onDateSet(year, month + 1, dayOfMonth)
}
val datePickerDialog = DatePickerDialog.newInstance(dateSetListener,
currentDate.year, currentDate.monthValue - 1, currentDate.dayOfMonth)
with(datePickerDialog) {
setDateRangeLimiter(SchooldaysRangeLimiter())
version = DatePickerDialog.Version.VERSION_2
scrollOrientation = DatePickerDialog.ScrollOrientation.VERTICAL
show(this@AdditionalLessonsFragment.parentFragmentManager, null)
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putLong(SAVED_DATE_KEY, presenter.currentDate.toEpochDay())
}
override fun onDestroyView() {
presenter.onDetachView()
super.onDestroyView()
}
}

View File

@ -0,0 +1,166 @@
package io.github.wulkanowy.ui.modules.timetable.additional
import android.annotation.SuppressLint
import io.github.wulkanowy.data.Status
import io.github.wulkanowy.data.repositories.semester.SemesterRepository
import io.github.wulkanowy.data.repositories.student.StudentRepository
import io.github.wulkanowy.data.repositories.timetable.TimetableRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.utils.AnalyticsHelper
import io.github.wulkanowy.utils.afterLoading
import io.github.wulkanowy.utils.flowWithResourceIn
import io.github.wulkanowy.utils.getLastSchoolDayIfHoliday
import io.github.wulkanowy.utils.isHolidays
import io.github.wulkanowy.utils.nextOrSameSchoolDay
import io.github.wulkanowy.utils.nextSchoolDay
import io.github.wulkanowy.utils.previousSchoolDay
import io.github.wulkanowy.utils.toFormattedString
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.onEach
import timber.log.Timber
import java.time.LocalDate
import javax.inject.Inject
class AdditionalLessonsPresenter @Inject constructor(
studentRepository: StudentRepository,
errorHandler: ErrorHandler,
private val semesterRepository: SemesterRepository,
private val timetableRepository: TimetableRepository,
private val analytics: AnalyticsHelper
) : BasePresenter<AdditionalLessonsView>(errorHandler, studentRepository) {
private var baseDate: LocalDate = LocalDate.now().nextOrSameSchoolDay
lateinit var currentDate: LocalDate
private set
private lateinit var lastError: Throwable
fun onAttachView(view: AdditionalLessonsView, date: Long?) {
super.onAttachView(view)
view.initView()
Timber.i("Additional lessons was initialized")
errorHandler.showErrorMessage = ::showErrorViewOnError
loadData(LocalDate.ofEpochDay(date ?: baseDate.toEpochDay()))
if (currentDate.isHolidays) setBaseDateOnHolidays()
reloadView()
}
fun onPreviousDay() {
loadData(currentDate.previousSchoolDay)
reloadView()
}
fun onNextDay() {
loadData(currentDate.nextSchoolDay)
reloadView()
}
fun onPickDate() {
view?.showDatePickerDialog(currentDate)
}
fun onDateSet(year: Int, month: Int, day: Int) {
loadData(LocalDate.of(year, month, day))
reloadView()
}
fun onSwipeRefresh() {
Timber.i("Force refreshing the additional lessons")
loadData(currentDate, true)
}
fun onRetry() {
view?.run {
showErrorView(false)
showProgress(true)
}
loadData(currentDate, true)
}
private fun setBaseDateOnHolidays() {
flow {
val student = studentRepository.getCurrentStudent()
emit(semesterRepository.getCurrentSemester(student))
}.catch {
Timber.i("Loading semester result: An exception occurred")
}.onEach {
baseDate = baseDate.getLastSchoolDayIfHoliday(it.schoolYear)
currentDate = baseDate
reloadNavigation()
}.launch("holidays")
}
private fun loadData(date: LocalDate, forceRefresh: Boolean = false) {
currentDate = date
flowWithResourceIn {
val student = studentRepository.getCurrentStudent()
val semester = semesterRepository.getCurrentSemester(student)
timetableRepository.getTimetable(student, semester, date, date, forceRefresh, true)
}.onEach {
when (it.status) {
Status.LOADING -> Timber.i("Loading additional lessons data started")
Status.SUCCESS -> {
Timber.i("Loading additional lessons lessons result: Success")
view?.apply {
updateData(it.data!!.second.sortedBy { item -> item.date })
showEmpty(it.data.second.isEmpty())
showErrorView(false)
showContent(it.data.second.isNotEmpty())
}
analytics.logEvent(
"load_data",
"type" to "additional_lessons",
"items" to it.data!!.second.size
)
}
Status.ERROR -> {
Timber.i("Loading additional lessons result: An exception occurred")
errorHandler.dispatch(it.error!!)
}
}
}.afterLoading {
view?.run {
hideRefresh()
showProgress(false)
enableSwipe(true)
}
}.launch()
}
private fun showErrorViewOnError(message: String, error: Throwable) {
view?.run {
if (isViewEmpty) {
lastError = error
setErrorDetails(message)
showErrorView(true)
showEmpty(false)
} else showError(message, error)
}
}
private fun reloadView() {
Timber.i("Reload additional lessons view with the date ${currentDate.toFormattedString()}")
view?.apply {
showProgress(true)
enableSwipe(false)
showContent(false)
showEmpty(false)
showErrorView(false)
clearData()
reloadNavigation()
}
}
@SuppressLint("DefaultLocale")
private fun reloadNavigation() {
view?.apply {
showPreButton(!currentDate.minusDays(1).isHolidays)
showNextButton(!currentDate.plusDays(1).isHolidays)
updateNavigationDay(currentDate.toFormattedString("EEEE, dd.MM").capitalize())
}
}
}

View File

@ -0,0 +1,38 @@
package io.github.wulkanowy.ui.modules.timetable.additional
import io.github.wulkanowy.data.db.entities.TimetableAdditional
import io.github.wulkanowy.ui.base.BaseView
import java.time.LocalDate
interface AdditionalLessonsView : BaseView {
val isViewEmpty: Boolean
fun initView()
fun updateData(data: List<TimetableAdditional>)
fun clearData()
fun updateNavigationDay(date: String)
fun hideRefresh()
fun showEmpty(show: Boolean)
fun showErrorView(show: Boolean)
fun setErrorDetails(message: String)
fun showProgress(show: Boolean)
fun enableSwipe(enable: Boolean)
fun showContent(show: Boolean)
fun showPreButton(show: Boolean)
fun showNextButton(show: Boolean)
fun showDatePickerDialog(currentDate: LocalDate)
}

View File

@ -107,7 +107,7 @@ class TimetableWidgetFactory(
val semester = semesterRepository.getCurrentSemester(student) val semester = semesterRepository.getCurrentSemester(student)
timetableRepository.getTimetable(student, semester, date, date, false) timetableRepository.getTimetable(student, semester, date, date, false)
.toFirstResult().data.orEmpty() .toFirstResult().data?.first.orEmpty()
.sortedWith(compareBy({ it.number }, { !it.isStudentPlan })) .sortedWith(compareBy({ it.number }, { !it.isStudentPlan }))
.filter { if (prefRepository.showWholeClassPlan == "no") it.isStudentPlan else true } .filter { if (prefRepository.showWholeClassPlan == "no") it.isStudentPlan else true }
} }

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFF"
android:pathData="M19,19V8H5V19H19M16,1H18V3H19C20.11,3 21,3.9 21,5V19C21,20.11 20.11,21 19,21H5C3.89,21 3,20.1 3,19V5C3,3.89 3.89,3 5,3H6V1H8V3H16V1M11,9.5H13V12.5H16V14.5H13V17.5H11V14.5H8V12.5H11V9.5Z" />
</vector>

View File

@ -0,0 +1,163 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.modules.timetable.additional.AdditionalLessonsFragment">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="50dp">
<me.zhanghai.android.materialprogressbar.MaterialProgressBar
android:id="@+id/additionalLessonsProgress"
style="@style/Widget.MaterialProgressBar.ProgressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true" />
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/additionalLessonsSwipe"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/additionalLessonsRecycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:listitem="@layout/item_timetable_additional" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<LinearLayout
android:id="@+id/additionalLessonsEmpty"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:padding="10dp"
android:visibility="gone">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/additionalLessonsInfoImage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="100dp"
android:minHeight="100dp"
app:srcCompat="@drawable/ic_menu_timetable_lessons_additional"
app:tint="?android:attr/textColorPrimary"
tools:ignore="contentDescription" />
<TextView
android:id="@+id/additionalLessonsInfo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:gravity="center"
android:text="@string/additional_lessons_no_items"
android:textSize="20sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/additionalLessonsError"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:visibility="invisible"
tools:ignore="UseCompoundDrawables"
tools:visibility="invisible">
<ImageView
android:layout_width="100dp"
android:layout_height="100dp"
app:srcCompat="@drawable/ic_error"
app:tint="?colorOnBackground"
tools:ignore="contentDescription" />
<TextView
android:id="@+id/additionalLessonsErrorMessage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:gravity="center"
android:padding="8dp"
android:text="@string/error_unknown"
android:textSize="20sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:gravity="center"
android:orientation="horizontal">
<com.google.android.material.button.MaterialButton
android:id="@+id/additionalLessonsErrorDetails"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:text="@string/all_details" />
<com.google.android.material.button.MaterialButton
android:id="@+id/additionalLessonsErrorRetry"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/all_retry" />
</LinearLayout>
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<io.github.wulkanowy.ui.widgets.MaterialLinearLayout
android:id="@+id/additionalLessonsNavContainer"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_gravity="bottom"
android:gravity="center"
android:orientation="horizontal"
tools:ignore="UnusedAttribute">
<ImageButton
android:id="@+id/additionalLessonsPreviousButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/all_prev"
android:paddingLeft="12dp"
android:paddingTop="8dp"
android:paddingRight="12dp"
android:paddingBottom="8dp"
android:scaleType="fitStart"
android:tint="?colorPrimary"
app:srcCompat="@drawable/ic_chevron_left" />
<TextView
android:id="@+id/additionalLessonsNavDate"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="?selectableItemBackgroundBorderless"
android:fontFamily="sans-serif"
android:gravity="center"
android:textSize="16sp"
tools:text="@tools:sample/date/ddmmyy" />
<ImageButton
android:id="@+id/additionalLessonsNextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/all_next"
android:paddingLeft="12dp"
android:paddingTop="8dp"
android:paddingRight="12dp"
android:paddingBottom="8dp"
android:scaleType="fitEnd"
android:tint="?colorPrimary"
app:srcCompat="@drawable/ic_chevron_right" />
</io.github.wulkanowy.ui.widgets.MaterialLinearLayout>
</FrameLayout>

View File

@ -0,0 +1,41 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:paddingStart="8dp"
android:paddingTop="6dp"
android:paddingEnd="12dp"
android:paddingBottom="6dp"
tools:context=".ui.modules.timetable.additional.AdditionalLessonsAdapter">
<TextView
android:id="@+id/additionalLessonItemSubject"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="16dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?android:textColorPrimary"
android:textSize="15sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="@tools:sample/lorem" />
<TextView
android:id="@+id/additionalLessonItemTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="4dp"
android:maxLines="1"
android:textColor="?android:textColorSecondary"
android:textSize="13sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/additionalLessonItemSubject"
tools:text="11:11 - 12:12" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,6 +1,13 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" <menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/timetableMenuAdditionalLessons"
android:icon="@drawable/ic_menu_timetable_lessons_additional"
android:orderInCategory="1"
android:title="@string/additional_lessons_button"
app:iconTint="@color/material_on_surface_emphasis_medium"
app:showAsAction="ifRoom" />
<item <item
android:id="@+id/timetableMenuCompletedLessons" android:id="@+id/timetableMenuCompletedLessons"
android:icon="@drawable/ic_menu_timetable_lessons_completed" android:icon="@drawable/ic_menu_timetable_lessons_completed"

View File

@ -161,6 +161,12 @@
<string name="completed_lessons_resources">Resources</string> <string name="completed_lessons_resources">Resources</string>
<!--Additional lessons-->
<string name="additional_lessons_title">Additional lessons</string>
<string name="additional_lessons_button">Show additional lessons</string>
<string name="additional_lessons_no_items">No info about additional lessons</string>
<!--Attendance--> <!--Attendance-->
<string name="attendance_summary_button">Attendance summary</string> <string name="attendance_summary_button">Attendance summary</string>
<string name="attendance_absence_school">Absent for school reasons</string> <string name="attendance_absence_school">Absent for school reasons</string>

View File

@ -56,7 +56,7 @@ class TimetableRemoteTest {
of(2018, 9, 15) of(2018, 9, 15)
) )
} }
assertEquals(2, timetable.size) assertEquals(2, timetable.first.size)
} }
private fun getTimetable(date: LocalDate): Timetable { private fun getTimetable(date: LocalDate): Timetable {