1
0

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
27 changed files with 2777 additions and 64 deletions

View File

@ -162,4 +162,8 @@ internal class RepositoryModule {
@Singleton
@Provides
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.SubjectDao
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.entities.Attendance
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.Teacher
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.Migration11
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.Migration29
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.Migration5
import io.github.wulkanowy.data.db.migrations.Migration6
@ -112,6 +115,7 @@ import javax.inject.Singleton
Teacher::class,
School::class,
Conference::class,
TimetableAdditional::class,
],
version = AppDatabase.VERSION_SCHEMA,
exportSchema = true
@ -120,7 +124,7 @@ import javax.inject.Singleton
abstract class AppDatabase : RoomDatabase() {
companion object {
const val VERSION_SCHEMA = 29
const val VERSION_SCHEMA = 30
fun getMigrations(sharedPrefProvider: SharedPrefProvider): Array<Migration> {
return arrayOf(
@ -151,7 +155,8 @@ abstract class AppDatabase : RoomDatabase() {
Migration26(),
Migration27(),
Migration28(),
Migration29()
Migration29(),
Migration30(),
)
}
@ -212,4 +217,6 @@ abstract class AppDatabase : RoomDatabase() {
abstract val schoolDao: SchoolDao
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(
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) },
saveFetchResult = { old, new ->
refreshGradeDetails(student, old.first, new.first, notify)
refreshGradeSummaries(old.second, new.second, notify)
saveFetchResult = { (oldDetails, oldSummary), (newDetails, newSummary) ->
refreshGradeDetails(student, oldDetails, newDetails, notify)
refreshGradeSummaries(oldSummary, newSummary, notify)
}
)

View File

@ -1,25 +1,42 @@
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.entities.Semester
import io.github.wulkanowy.data.db.entities.Timetable
import io.github.wulkanowy.data.db.entities.TimetableAdditional
import kotlinx.coroutines.flow.Flow
import java.time.LocalDate
import javax.inject.Inject
import javax.inject.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>) {
timetableDb.insertAll(timetables)
}
suspend fun saveTimetableAdditional(additional: List<TimetableAdditional>) {
timetableAdditionalDb.insertAll(additional)
}
suspend fun deleteTimetable(timetables: List<Timetable>) {
timetableDb.deleteAll(timetables)
}
suspend fun deleteTimetableAdditional(additional: List<TimetableAdditional>) {
timetableAdditionalDb.deleteAll(additional)
}
fun getTimetable(semester: Semester, startDate: LocalDate, endDate: LocalDate): Flow<List<Timetable>> {
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.Student
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.utils.init
import java.time.LocalDate
@ -12,29 +13,40 @@ import javax.inject.Singleton
@Singleton
class TimetableRemote @Inject constructor(private val sdk: Sdk) {
suspend fun getTimetable(student: Student, semester: Semester, startDate: LocalDate, endDate: LocalDate): List<Timetable> {
return sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear)
.getTimetable(startDate, endDate).first
.map {
Timetable(
studentId = semester.studentId,
diaryId = semester.diaryId,
number = it.number,
start = it.start,
end = it.end,
date = it.date,
subject = it.subject,
subjectOld = it.subjectOld,
group = it.group,
room = it.room,
roomOld = it.roomOld,
teacher = it.teacher,
teacherOld = it.teacherOld,
info = it.info,
isStudentPlan = it.studentPlan,
changes = it.changes,
canceled = it.canceled
)
}
suspend fun getTimetable(student: Student, semester: Semester, startDate: LocalDate, endDate: LocalDate): Pair<List<Timetable>, List<TimetableAdditional>> {
val (normal, additional) = sdk.init(student)
.switchDiary(semester.diaryId, semester.schoolYear)
.getTimetable(startDate, endDate)
return normal.map {
Timetable(
studentId = semester.studentId,
diaryId = semester.diaryId,
number = it.number,
start = it.start,
end = it.end,
date = it.date,
subject = it.subject,
subjectOld = it.subjectOld,
group = it.group,
room = it.room,
roomOld = it.roomOld,
teacher = it.teacher,
teacherOld = it.teacherOld,
info = it.info,
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.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.utils.monday
import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import java.time.LocalDate
import javax.inject.Inject
@ -19,23 +22,44 @@ class TimetableRepository @Inject constructor(
private val schedulerHelper: TimetableNotificationSchedulerHelper
) {
fun getTimetable(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean) = networkBoundResource(
shouldFetch = { it.isEmpty() || forceRefresh },
query = { local.getTimetable(semester, start.monday, end.sunday).map { schedulerHelper.scheduleNotifications(it, student); it } },
fetch = { remote.getTimetable(student, semester, start.monday, end.sunday) },
saveFetchResult = { old, new ->
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
)
}
fun getTimetable(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean, refreshAdditional: Boolean = false) = networkBoundResource(
shouldFetch = { (timetable, additional) -> timetable.isEmpty() || (additional.isEmpty() && refreshAdditional) || forceRefresh },
query = {
local.getTimetable(semester, start.monday, end.sunday)
.map { schedulerHelper.scheduleNotifications(it, student); it }
.combine(local.getTimetableAdditional(semester, start.monday, end.sunday)) { timetable, additional ->
timetable to additional
}
})
},
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.modules.main.MainActivity
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.widgets.DividerItemDecoration
import io.github.wulkanowy.utils.SchooldaysRangeLimiter
@ -84,8 +85,11 @@ class TimetableFragment : BaseFragment<FragmentTimetableBinding>(R.layout.fragme
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return if (item.itemId == R.id.timetableMenuCompletedLessons) presenter.onCompletedLessonsSwitchSelected()
else false
return when (item.itemId) {
R.id.timetableMenuAdditionalLessons -> presenter.onAdditionalLessonsSwitchSelected()
R.id.timetableMenuCompletedLessons -> presenter.onCompletedLessonsSwitchSelected()
else -> false
}
}
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() {
(activity as? MainActivity)?.pushView(CompletedLessonsFragment.newInstance())
}

View File

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

View File

@ -44,5 +44,7 @@ interface TimetableView : BaseView {
fun popView()
fun openAdditionalLessonsView()
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)
timetableRepository.getTimetable(student, semester, date, date, false)
.toFirstResult().data.orEmpty()
.toFirstResult().data?.first.orEmpty()
.sortedWith(compareBy({ it.number }, { !it.isStudentPlan }))
.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"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
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
android:id="@+id/timetableMenuCompletedLessons"
android:icon="@drawable/ic_menu_timetable_lessons_completed"

View File

@ -161,6 +161,12 @@
<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-->
<string name="attendance_summary_button">Attendance summary</string>
<string name="attendance_absence_school">Absent for school reasons</string>