diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..35fbd4661 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +[*] +charset=utf-8 +end_of_line=lf +insert_final_newline=true +indent_style=space +indent_size=4 + +[*.json] +indent_size=2 + +[*.{kt,kts}] +disabled_rules=import-ordering,no-wildcard-imports diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableAdapter.kt index eacd12c69..d6917672a 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableAdapter.kt @@ -1,116 +1,69 @@ package io.github.wulkanowy.ui.modules.timetable -import android.graphics.Paint -import android.os.Handler -import android.os.Looper import android.view.LayoutInflater import android.view.View.GONE import android.view.View.VISIBLE import android.view.ViewGroup import android.widget.TextView +import androidx.core.view.isVisible import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Timetable -import io.github.wulkanowy.data.enums.TimetableMode import io.github.wulkanowy.databinding.ItemTimetableBinding import io.github.wulkanowy.databinding.ItemTimetableSmallBinding -import io.github.wulkanowy.utils.* -import timber.log.Timber -import java.time.Instant -import java.util.* +import io.github.wulkanowy.utils.getThemeAttrColor +import io.github.wulkanowy.utils.toFormattedString import javax.inject.Inject -import kotlin.concurrent.timer -class TimetableAdapter @Inject constructor() : RecyclerView.Adapter() { +class TimetableAdapter @Inject constructor() : + ListAdapter(differ) { - private enum class ViewType { - ITEM_NORMAL, - ITEM_SMALL - } - - var onClickListener: (Timetable) -> Unit = {} - - private var showWholeClassPlan = TimetableMode.ONLY_CURRENT_GROUP - - private var showGroupsInPlan: Boolean = false - - private var showTimers: Boolean = false - - private val timers = mutableMapOf() - - private val items = mutableListOf() - - fun submitList( - newTimetable: List, - showWholeClassPlan: TimetableMode = this.showWholeClassPlan, - showGroupsInPlan: Boolean = this.showGroupsInPlan, - showTimers: Boolean = this.showTimers - ) { - val isFlagsDifferent = this.showWholeClassPlan != showWholeClassPlan - || this.showGroupsInPlan != showGroupsInPlan - || this.showTimers != showTimers - - val diffResult = DiffUtil.calculateDiff( - TimetableAdapterDiffCallback( - oldList = items.toMutableList(), - newList = newTimetable, - isFlagsDifferent = isFlagsDifferent - ) - ) - - this.showGroupsInPlan = showGroupsInPlan - this.showTimers = showTimers - this.showWholeClassPlan = showWholeClassPlan - - items.clear() - items.addAll(newTimetable) - - diffResult.dispatchUpdatesTo(this) - } - - fun clearTimers() { - Timber.d("Timetable timers (${timers.size}) cleared") - with(timers) { - forEach { (_, timer) -> - timer?.cancel() - timer?.purge() - } - clear() - } - } - - override fun getItemCount() = items.size - - override fun getItemViewType(position: Int) = when { - !items[position].isStudentPlan && showWholeClassPlan == TimetableMode.SMALL_OTHER_GROUP -> ViewType.ITEM_SMALL.ordinal - else -> ViewType.ITEM_NORMAL.ordinal - } + override fun getItemViewType(position: Int): Int = getItem(position).type.ordinal override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { val inflater = LayoutInflater.from(parent.context) - return when (viewType) { - ViewType.ITEM_NORMAL.ordinal -> ItemViewHolder( - ItemTimetableBinding.inflate(inflater, parent, false) - ) - ViewType.ITEM_SMALL.ordinal -> SmallItemViewHolder( + return when (TimetableItemType.values()[viewType]) { + TimetableItemType.SMALL -> SmallViewHolder( ItemTimetableSmallBinding.inflate(inflater, parent, false) ) - else -> throw IllegalStateException() + TimetableItemType.NORMAL -> NormalViewHolder( + ItemTimetableBinding.inflate(inflater, parent, false) + ) } } + override fun onBindViewHolder( + holder: RecyclerView.ViewHolder, + position: Int, + payloads: MutableList + ) { + if (payloads.isEmpty()) return super.onBindViewHolder(holder, position, payloads) + + if (holder is NormalViewHolder) updateTimeLeft( + binding = holder.binding, + timeLeft = (getItem(position) as TimetableItem.Normal).timeLeft, + ) + } + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - val lesson = items[position] - when (holder) { - is ItemViewHolder -> bindNormalView(holder.binding, lesson, position) - is SmallItemViewHolder -> bindSmallView(holder.binding, lesson) + is SmallViewHolder -> bindSmallView( + binding = holder.binding, + item = getItem(position) as TimetableItem.Small, + ) + is NormalViewHolder -> bindNormalView( + binding = holder.binding, + item = getItem(position) as TimetableItem.Normal, + ) } } - private fun bindSmallView(binding: ItemTimetableSmallBinding, lesson: Timetable) { + private fun bindSmallView(binding: ItemTimetableSmallBinding, item: TimetableItem.Small) { + val lesson = item.lesson + with(binding) { timetableSmallItemNumber.text = lesson.number.toString() timetableSmallItemSubject.text = lesson.subject @@ -122,11 +75,13 @@ class TimetableAdapter @Inject constructor() : RecyclerView.Adapter i < position && !item.isStudentPlan }.size) - ?.let { - if (!it.canceled && it.isStudentPlan) it.end - else null - } - } - - private fun updateTimeLeft(binding: ItemTimetableBinding, lesson: Timetable, position: Int) { - val isShowTimeUntil = lesson.isShowTimeUntil(getPreviousLesson(position)) - val until = lesson.until.plusMinutes(1) - val left = lesson.left?.plusMinutes(1) - val isJustFinished = lesson.isJustFinished - + private fun updateTimeLeft(binding: ItemTimetableBinding, timeLeft: TimeLeft?) { with(binding) { when { // before lesson - isShowTimeUntil -> { - Timber.d("Show time until lesson: $position") + timeLeft?.until != null -> { timetableItemTimeLeft.visibility = GONE with(timetableItemTimeUntil) { visibility = VISIBLE @@ -189,14 +112,13 @@ class TimetableAdapter @Inject constructor() : RecyclerView.Adapter { - Timber.d("Show time left lesson: $position") + timeLeft?.left != null -> { timetableItemTimeUntil.visibility = GONE with(timetableItemTimeLeft) { visibility = VISIBLE @@ -204,14 +126,13 @@ class TimetableAdapter @Inject constructor() : RecyclerView.Adapter { - Timber.d("Show just finished lesson: $position") + timeLeft?.isJustFinished == true -> { timetableItemTimeUntil.visibility = GONE timetableItemTimeLeft.visibility = VISIBLE timetableItemTimeLeft.text = root.context.getString(R.string.timetable_finished) @@ -225,9 +146,7 @@ class TimetableAdapter @Inject constructor() : RecyclerView.Adapter, - private val newList: List, - private val isFlagsDifferent: Boolean - ) : DiffUtil.Callback() { + companion object { + private val differ = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: TimetableItem, newItem: TimetableItem): Boolean = + when { + oldItem is TimetableItem.Small && newItem is TimetableItem.Small -> { + oldItem.lesson.start == newItem.lesson.start + } + oldItem is TimetableItem.Normal && newItem is TimetableItem.Normal -> { + oldItem.lesson.start == newItem.lesson.start + } + else -> oldItem == newItem + } - override fun getOldListSize() = oldList.size + override fun areContentsTheSame(oldItem: TimetableItem, newItem: TimetableItem) = + oldItem == newItem - override fun getNewListSize() = newList.size - - override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) = - oldList[oldItemPosition].id == newList[newItemPosition].id - - override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) = - oldList[oldItemPosition] == newList[newItemPosition] && !isFlagsDifferent + override fun getChangePayload(oldItem: TimetableItem, newItem: TimetableItem): Any? { + return if (oldItem is TimetableItem.Normal && newItem is TimetableItem.Normal) { + if (oldItem.lesson == newItem.lesson && oldItem.timeLeft != newItem.timeLeft) { + "time_left" + } else super.getChangePayload(oldItem, newItem) + } else super.getChangePayload(oldItem, newItem) + } + } } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt index f59c6432c..fdd4aface 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt @@ -12,7 +12,6 @@ import androidx.recyclerview.widget.LinearLayoutManager import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Timetable -import io.github.wulkanowy.data.enums.TimetableMode import io.github.wulkanowy.databinding.FragmentTimetableBinding import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.modules.main.MainActivity @@ -20,11 +19,7 @@ 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.dpToPx -import io.github.wulkanowy.utils.firstSchoolDayInSchoolYear -import io.github.wulkanowy.utils.getThemeAttrColor -import io.github.wulkanowy.utils.lastSchoolDayInSchoolYear -import io.github.wulkanowy.utils.openMaterialDatePicker +import io.github.wulkanowy.utils.* import java.time.LocalDate import javax.inject.Inject @@ -73,8 +68,6 @@ class TimetableFragment : BaseFragment(R.layout.fragme } override fun initView() { - timetableAdapter.onClickListener = presenter::onTimetableItemSelected - with(binding.timetableRecycler) { layoutManager = LinearLayoutManager(context) adapter = timetableAdapter @@ -110,18 +103,8 @@ class TimetableFragment : BaseFragment(R.layout.fragme } } - override fun updateData( - data: List, - showWholeClassPlanType: TimetableMode, - showGroupsInPlanType: Boolean, - showTimetableTimers: Boolean - ) { - timetableAdapter.submitList( - newTimetable = data.toMutableList(), - showGroupsInPlan = showGroupsInPlanType, - showTimers = showTimetableTimers, - showWholeClassPlan = showWholeClassPlanType - ) + override fun updateData(data: List) { + timetableAdapter.submitList(data) } override fun clearData() { @@ -214,7 +197,6 @@ class TimetableFragment : BaseFragment(R.layout.fragme } override fun onDestroyView() { - timetableAdapter.clearTimers() presenter.onDetachView() super.onDestroyView() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableItem.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableItem.kt new file mode 100644 index 000000000..92716ace8 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableItem.kt @@ -0,0 +1,30 @@ +package io.github.wulkanowy.ui.modules.timetable + +import io.github.wulkanowy.data.db.entities.Timetable +import java.time.Duration + +sealed class TimetableItem(val type: TimetableItemType) { + + data class Small( + val lesson: Timetable, + val onClick: (Timetable) -> Unit, + ) : TimetableItem(TimetableItemType.SMALL) + + data class Normal( + val lesson: Timetable, + val showGroupsInPlan: Boolean, + val timeLeft: TimeLeft?, + val onClick: (Timetable) -> Unit, + ) : TimetableItem(TimetableItemType.NORMAL) +} + +data class TimeLeft( + val until: Duration?, + val left: Duration?, + val isJustFinished: Boolean, +) + +enum class TimetableItemType { + SMALL, + NORMAL, +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetablePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetablePresenter.kt index 87f5cac3d..ec3ef7b02 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetablePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetablePresenter.kt @@ -1,6 +1,5 @@ package io.github.wulkanowy.ui.modules.timetable -import android.annotation.SuppressLint import io.github.wulkanowy.data.Status import io.github.wulkanowy.data.db.entities.Timetable import io.github.wulkanowy.data.enums.TimetableMode @@ -10,25 +9,17 @@ import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.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.capitalise -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 io.github.wulkanowy.utils.* import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.onEach import timber.log.Timber +import java.time.Instant import java.time.LocalDate -import java.time.LocalDate.now -import java.time.LocalDate.of -import java.time.LocalDate.ofEpochDay +import java.time.LocalDate.* +import java.util.* import javax.inject.Inject +import kotlin.concurrent.timer class TimetablePresenter @Inject constructor( errorHandler: ErrorHandler, @@ -46,6 +37,8 @@ class TimetablePresenter @Inject constructor( private lateinit var lastError: Throwable + private var tickTimer: Timer? = null + fun onAttachView(view: TimetableView, date: Long?) { super.onAttachView(view) view.initView() @@ -106,11 +99,6 @@ class TimetablePresenter @Inject constructor( } } - fun onTimetableItemSelected(lesson: Timetable) { - Timber.i("Select timetable item ${lesson.id}") - view?.showTimetableDialog(lesson) - } - fun onAdditionalLessonsSwitchSelected(): Boolean { view?.openAdditionalLessonsView() return true @@ -148,12 +136,12 @@ class TimetablePresenter @Inject constructor( Status.LOADING -> { if (!it.data?.lessons.isNullOrEmpty()) { view?.run { + updateData(it.data!!.lessons) enableSwipe(true) showRefresh(true) showErrorView(false) showProgress(false) showContent(true) - updateData(it.data!!.lessons) } } } @@ -189,17 +177,62 @@ class TimetablePresenter @Inject constructor( } private fun updateData(lessons: List) { - view?.updateData( - showWholeClassPlanType = prefRepository.showWholeClassPlan, - showGroupsInPlanType = prefRepository.showGroupsInPlan, - showTimetableTimers = prefRepository.showTimetableTimers, - data = createItems(lessons) + tickTimer?.cancel() + + if (!prefRepository.showTimetableTimers) { + view?.updateData(createItems(lessons)) + } else { + tickTimer = timer(period = 2_000) { + view?.updateData(createItems(lessons)) + } + } + } + + private fun createItems(items: List): List { + val filteredItems = items + .filter { + if (prefRepository.showWholeClassPlan == TimetableMode.ONLY_CURRENT_GROUP) { + it.isStudentPlan + } else true + }.sortedWith( + compareBy({ item -> item.number }, { item -> !item.isStudentPlan }) + ) + + return filteredItems.mapIndexed { i, it -> + if (it.isStudentPlan) TimetableItem.Normal( + lesson = it, + showGroupsInPlan = prefRepository.showGroupsInPlan, + timeLeft = filteredItems.getTimeLeftForLesson(it, i), + onClick = ::onTimetableItemSelected + ) else TimetableItem.Small( + lesson = it, + onClick = ::onTimetableItemSelected + ) + } + } + + private fun List.getTimeLeftForLesson(lesson: Timetable, index: Int): TimeLeft { + val isShowTimeUntil = lesson.isShowTimeUntil(getPreviousLesson(index)) + return TimeLeft( + until = lesson.until.plusMinutes(1).takeIf { isShowTimeUntil }, + left = lesson.left?.plusMinutes(1), + isJustFinished = lesson.isJustFinished, ) } - private fun createItems(items: List) = items.filter { item -> - if (prefRepository.showWholeClassPlan == TimetableMode.ONLY_CURRENT_GROUP) item.isStudentPlan else true - }.sortedWith(compareBy({ item -> item.number }, { item -> !item.isStudentPlan })) + private fun List.getPreviousLesson(position: Int): Instant? { + return filter { it.isStudentPlan } + .getOrNull(position - 1 - filterIndexed { i, item -> i < position && !item.isStudentPlan }.size) + ?.let { + if (!it.canceled && it.isStudentPlan) it.end + else null + } + } + + private fun onTimetableItemSelected(lesson: Timetable) { + Timber.i("Select timetable item ${lesson.id}") + view?.showTimetableDialog(lesson) + } private fun showErrorViewOnError(message: String, error: Throwable) { view?.run { @@ -227,7 +260,6 @@ class TimetablePresenter @Inject constructor( } } - @SuppressLint("DefaultLocale") private fun reloadNavigation() { view?.apply { showPreButton(!currentDate.minusDays(1).isHolidays) @@ -235,4 +267,10 @@ class TimetablePresenter @Inject constructor( updateNavigationDay(currentDate.toFormattedString("EEEE, dd.MM").capitalise()) } } + + override fun onDetachView() { + tickTimer?.cancel() + tickTimer = null + super.onDetachView() + } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableView.kt index 4f6af4b9e..8cfb26204 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableView.kt @@ -1,7 +1,6 @@ package io.github.wulkanowy.ui.modules.timetable import io.github.wulkanowy.data.db.entities.Timetable -import io.github.wulkanowy.data.enums.TimetableMode import io.github.wulkanowy.ui.base.BaseView import java.time.LocalDate @@ -13,12 +12,7 @@ interface TimetableView : BaseView { fun initView() - fun updateData( - data: List, - showWholeClassPlanType: TimetableMode, - showGroupsInPlanType: Boolean, - showTimetableTimers: Boolean - ) + fun updateData(data: List) fun updateNavigationDay(date: String)