forked from github/szkolny
[UI] Implement home timetable card.
This commit is contained in:
@ -36,7 +36,6 @@ import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile
|
||||
import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher
|
||||
import pl.szczodrzynski.edziennik.data.db.modules.teams.Team
|
||||
import pl.szczodrzynski.edziennik.utils.models.Time
|
||||
import pl.szczodrzynski.navlib.R
|
||||
import pl.szczodrzynski.navlib.getColorFromRes
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
@ -379,13 +378,13 @@ fun CharSequence?.asItalicSpannable(): Spannable {
|
||||
*/
|
||||
fun <T : CharSequence> listOfNotEmpty(vararg elements: T): List<T> = elements.filterNot { it.isEmpty() }
|
||||
|
||||
fun List<CharSequence>.concat(delimiter: String? = null): CharSequence {
|
||||
fun List<CharSequence?>.concat(delimiter: String? = null): CharSequence {
|
||||
if (this.isEmpty()) {
|
||||
return ""
|
||||
}
|
||||
|
||||
if (this.size == 1) {
|
||||
return this[0]
|
||||
return this[0] ?: ""
|
||||
}
|
||||
|
||||
var spanned = false
|
||||
@ -400,6 +399,8 @@ fun List<CharSequence>.concat(delimiter: String? = null): CharSequence {
|
||||
if (spanned) {
|
||||
val ssb = SpannableStringBuilder()
|
||||
for (piece in this) {
|
||||
if (piece == null)
|
||||
continue
|
||||
if (!first && delimiter != null)
|
||||
ssb.append(delimiter)
|
||||
first = false
|
||||
@ -409,6 +410,8 @@ fun List<CharSequence>.concat(delimiter: String? = null): CharSequence {
|
||||
} else {
|
||||
val sb = StringBuilder()
|
||||
for (piece in this) {
|
||||
if (piece == null)
|
||||
continue
|
||||
if (!first && delimiter != null)
|
||||
sb.append(delimiter)
|
||||
first = false
|
||||
@ -533,3 +536,54 @@ operator fun Time?.compareTo(other: Time?): Int {
|
||||
operator fun StringBuilder.plusAssign(str: String?) {
|
||||
this.append(str)
|
||||
}
|
||||
|
||||
fun Context.timeTill(time: Int, delimiter: String = " "): String {
|
||||
val parts = mutableListOf<Pair<Int, Int>>()
|
||||
|
||||
val hours = time / 3600
|
||||
val minutes = (time - hours*3600) / 60
|
||||
val seconds = time - minutes*60 - hours*3600
|
||||
|
||||
var prefixAdded = false
|
||||
if (hours > 0) {
|
||||
if (!prefixAdded) parts += R.plurals.time_till_text to hours; prefixAdded = true
|
||||
parts += R.plurals.time_till_hours to hours
|
||||
}
|
||||
if (minutes > 0) {
|
||||
if (!prefixAdded) parts += R.plurals.time_till_text to minutes; prefixAdded = true
|
||||
parts += R.plurals.time_till_minutes to minutes
|
||||
}
|
||||
if (hours == 0 && minutes < 10) {
|
||||
if (!prefixAdded) parts += R.plurals.time_till_text to seconds; prefixAdded = true
|
||||
parts += R.plurals.time_till_seconds to seconds
|
||||
}
|
||||
|
||||
return parts.joinToString(delimiter) { resources.getQuantityString(it.first, it.second, it.second) }
|
||||
}
|
||||
|
||||
fun Context.timeLeft(time: Int, delimiter: String = " "): String {
|
||||
val parts = mutableListOf<Pair<Int, Int>>()
|
||||
|
||||
val hours = time / 3600
|
||||
val minutes = (time - hours*3600) / 60
|
||||
val seconds = time - minutes*60 - hours*3600
|
||||
|
||||
var prefixAdded = false
|
||||
if (hours > 0) {
|
||||
if (!prefixAdded) parts += R.plurals.time_left_text to hours
|
||||
prefixAdded = true
|
||||
parts += R.plurals.time_left_hours to hours
|
||||
}
|
||||
if (minutes > 0) {
|
||||
if (!prefixAdded) parts += R.plurals.time_left_text to minutes
|
||||
prefixAdded = true
|
||||
parts += R.plurals.time_left_minutes to minutes
|
||||
}
|
||||
if (hours == 0 && minutes < 10) {
|
||||
if (!prefixAdded) parts += R.plurals.time_left_text to seconds
|
||||
prefixAdded = true
|
||||
parts += R.plurals.time_left_seconds to seconds
|
||||
}
|
||||
|
||||
return parts.joinToString(delimiter) { resources.getQuantityString(it.first, it.second, it.second) }
|
||||
}
|
||||
|
@ -59,6 +59,11 @@ open class Lesson(val profileId: Int, @PrimaryKey var id: Long) {
|
||||
return startTime ?: oldStartTime
|
||||
}
|
||||
|
||||
val isCancelled
|
||||
get() = type == TYPE_CANCELLED || type == TYPE_SHIFTED_SOURCE
|
||||
val isChange
|
||||
get() = type == TYPE_CHANGE || type == TYPE_SHIFTED_TARGET
|
||||
|
||||
fun buildId(): Long = (displayDate?.combineWith(displayStartTime) ?: 0L) / 6L * 10L + (hashCode() and 0xFFFF)
|
||||
|
||||
override fun toString(): String {
|
||||
@ -110,7 +115,7 @@ open class Lesson(val profileId: Int, @PrimaryKey var id: Long) {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
override fun hashCode(): Int { // intentionally ignoring ID and display* here
|
||||
var result = profileId
|
||||
result = 31 * result + type
|
||||
result = 31 * result + (date?.hashCode() ?: 0)
|
||||
@ -131,32 +136,4 @@ open class Lesson(val profileId: Int, @PrimaryKey var id: Long) {
|
||||
result = 31 * result + (oldClassroom?.hashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
}
|
||||
/*
|
||||
DROP TABLE lessons;
|
||||
DROP TABLE lessonChanges;
|
||||
CREATE TABLE lessons (
|
||||
profileId INTEGER NOT NULL,
|
||||
type INTEGER NOT NULL,
|
||||
|
||||
date TEXT DEFAULT NULL,
|
||||
lessonNumber INTEGER DEFAULT NULL,
|
||||
startTime TEXT DEFAULT NULL,
|
||||
endTime TEXT DEFAULT NULL,
|
||||
teacherId INTEGER DEFAULT NULL,
|
||||
subjectId INTEGER DEFAULT NULL,
|
||||
teamId INTEGER DEFAULT NULL,
|
||||
classroom TEXT DEFAULT NULL,
|
||||
|
||||
oldDate TEXT DEFAULT NULL,
|
||||
oldLessonNumber INTEGER DEFAULT NULL,
|
||||
oldStartTime TEXT DEFAULT NULL,
|
||||
oldEndTime TEXT DEFAULT NULL,
|
||||
oldTeacherId INTEGER DEFAULT NULL,
|
||||
oldSubjectId INTEGER DEFAULT NULL,
|
||||
oldTeamId INTEGER DEFAULT NULL,
|
||||
oldClassroom TEXT DEFAULT NULL,
|
||||
|
||||
PRIMARY KEY(profileId)
|
||||
);
|
||||
*/
|
||||
}
|
@ -5,11 +5,15 @@
|
||||
package pl.szczodrzynski.edziennik.ui.modules.home.cards
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import androidx.core.view.plusAssign
|
||||
import androidx.core.view.setMargins
|
||||
import androidx.lifecycle.Observer
|
||||
import com.mikepenz.iconics.IconicsDrawable
|
||||
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
|
||||
import com.mikepenz.iconics.utils.sizeDp
|
||||
import kotlinx.coroutines.*
|
||||
import pl.szczodrzynski.edziennik.*
|
||||
import pl.szczodrzynski.edziennik.data.db.modules.events.Event
|
||||
@ -22,6 +26,8 @@ import pl.szczodrzynski.edziennik.ui.modules.home.HomeCardAdapter
|
||||
import pl.szczodrzynski.edziennik.ui.modules.home.HomeFragmentV2
|
||||
import pl.szczodrzynski.edziennik.utils.models.Date
|
||||
import pl.szczodrzynski.edziennik.utils.models.Time
|
||||
import pl.szczodrzynski.edziennik.utils.models.Week
|
||||
import pl.szczodrzynski.navlib.colorAttr
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
class HomeTimetableCard(
|
||||
@ -48,6 +54,17 @@ class HomeTimetableCard(
|
||||
private var lessons = listOf<LessonFull>()
|
||||
private var events = listOf<Event>()
|
||||
|
||||
private var bellSyncDiffMillis = 0L
|
||||
private val syncedNow: Time
|
||||
get() = Time.fromMillis(Time.getNow().inMillis + bellSyncDiffMillis)
|
||||
|
||||
private var counterJob: Job? = null
|
||||
private var counterStart: Time? = null
|
||||
private var counterEnd: Time? = null
|
||||
private var subjectSpannable: CharSequence? = null
|
||||
|
||||
private val ignoreCancelled = true
|
||||
|
||||
override fun bind(position: Int, holder: HomeCardAdapter.ViewHolder) {
|
||||
holder.root.removeAllViews()
|
||||
b = CardHomeTimetableBinding.inflate(LayoutInflater.from(holder.root.context))
|
||||
@ -56,6 +73,17 @@ class HomeTimetableCard(
|
||||
}
|
||||
holder.root += b.root
|
||||
|
||||
b.settings.setImageDrawable(IconicsDrawable(activity, CommunityMaterial.Icon2.cmd_settings_outline)
|
||||
.colorAttr(activity, R.attr.colorIcon)
|
||||
.sizeDp(20))
|
||||
|
||||
// get current bell-sync params
|
||||
if (app.appConfig.bellSyncDiff != null) {
|
||||
bellSyncDiffMillis = (app.appConfig.bellSyncDiff.hour * 60 * 60 * 1000 + app.appConfig.bellSyncDiff.minute * 60 * 1000 + app.appConfig.bellSyncDiff.second * 1000).toLong()
|
||||
bellSyncDiffMillis *= app.appConfig.bellSyncMultiplier.toLong()
|
||||
bellSyncDiffMillis *= -1
|
||||
}
|
||||
|
||||
// get all lessons within the search bounds
|
||||
app.db.timetableDao().getBetweenDates(today, searchEnd).observe(fragment, Observer {
|
||||
allLessons = it
|
||||
@ -65,36 +93,177 @@ class HomeTimetableCard(
|
||||
|
||||
private fun update() { launch {
|
||||
val deferred = async(Dispatchers.Default) {
|
||||
// get current bell-sync params
|
||||
var bellSyncDiffMillis: Long = 0
|
||||
if (app.appConfig.bellSyncDiff != null) {
|
||||
bellSyncDiffMillis = (app.appConfig.bellSyncDiff.hour * 60 * 60 * 1000 + app.appConfig.bellSyncDiff.minute * 60 * 1000 + app.appConfig.bellSyncDiff.second * 1000).toLong()
|
||||
bellSyncDiffMillis *= app.appConfig.bellSyncMultiplier.toLong()
|
||||
bellSyncDiffMillis *= -1
|
||||
}
|
||||
// get the current bell-synced time
|
||||
val now = Time.fromMillis(Time.getNow().inMillis + bellSyncDiffMillis)
|
||||
val now = syncedNow
|
||||
|
||||
// search for lessons to display
|
||||
val timetableDate = Date.getToday()
|
||||
var checkedDays = 0
|
||||
lessons = allLessons.filter { it.profileId == profile.id && it.displayDate == timetableDate && it.displayEndTime > now && it.type != Lesson.TYPE_NO_LESSONS }
|
||||
lessons = allLessons.filter {
|
||||
it.profileId == profile.id
|
||||
&& it.displayDate == timetableDate
|
||||
&& it.displayEndTime > now
|
||||
&& it.type != Lesson.TYPE_NO_LESSONS
|
||||
&& !(it.isCancelled && ignoreCancelled)
|
||||
}
|
||||
while ((lessons.isEmpty() || lessons.none {
|
||||
it.displayDate != today || (it.displayDate == today && it.displayEndTime != null && it.displayEndTime!! >= now)
|
||||
}) && checkedDays < 7) {
|
||||
|
||||
timetableDate.stepForward(0, 0, 1)
|
||||
lessons = allLessons.filter { it.profileId == profile.id && it.displayDate == timetableDate && it.type != Lesson.TYPE_NO_LESSONS }
|
||||
lessons = allLessons.filter {
|
||||
it.profileId == profile.id
|
||||
&& it.displayDate == timetableDate
|
||||
&& it.type != Lesson.TYPE_NO_LESSONS
|
||||
&& !(it.isCancelled && ignoreCancelled)
|
||||
}
|
||||
|
||||
checkedDays++
|
||||
}
|
||||
timetableDate
|
||||
}
|
||||
deferred.await()
|
||||
val timetableDate = deferred.await()
|
||||
|
||||
val text = StringBuilder()
|
||||
for (lesson in lessons) {
|
||||
text += lesson.displayStartTime?.stringHM+" "+lesson.displaySubjectName+"\n"
|
||||
val isToday = today == timetableDate
|
||||
|
||||
b.progress.visibility = View.GONE
|
||||
b.counter.visibility = View.GONE
|
||||
|
||||
val now = syncedNow
|
||||
val firstLesson = lessons.firstOrNull()
|
||||
val lastLesson = lessons.lastOrNull()
|
||||
|
||||
if (isToday) {
|
||||
// today
|
||||
b.dayInfo.setText(R.string.home_timetable_today)
|
||||
counterStart = firstLesson?.displayStartTime
|
||||
counterEnd = firstLesson?.displayEndTime
|
||||
val isOngoing = counterStart <= now && now <= counterEnd
|
||||
val lessonRes = if (isOngoing)
|
||||
R.string.home_timetable_lesson_ongoing
|
||||
else
|
||||
R.string.home_timetable_lesson_not_started
|
||||
b.lessonBig.setText(lessonRes, firstLesson.subjectSpannable)
|
||||
firstLesson?.displayClassroom?.let {
|
||||
b.classroom.visibility = View.VISIBLE
|
||||
b.classroom.text = it
|
||||
} ?: run {
|
||||
b.classroom.visibility = View.GONE
|
||||
}
|
||||
|
||||
subjectSpannable = firstLesson.subjectSpannable
|
||||
|
||||
counterJob = startCoroutineTimer(repeatMillis = 1000) {
|
||||
count()
|
||||
}
|
||||
}
|
||||
b.text.text = text.toString()
|
||||
else {
|
||||
val isTomorrow = today.clone().stepForward(0, 0, 1) == timetableDate
|
||||
val dayInfoRes = if (isTomorrow) {
|
||||
// tomorrow
|
||||
R.string.home_timetable_tomorrow
|
||||
}
|
||||
else {
|
||||
val todayWeekStart = today.weekStart
|
||||
val dateWeekStart = timetableDate.weekStart
|
||||
if (todayWeekStart == dateWeekStart) {
|
||||
// this week
|
||||
R.string.home_timetable_date_this_week
|
||||
}
|
||||
else {
|
||||
// future: not this week
|
||||
R.string.home_timetable_date_future
|
||||
}
|
||||
}
|
||||
b.dayInfo.setText(dayInfoRes, Week.getFullDayName(timetableDate.weekDay), timetableDate.formattedString)
|
||||
b.lessonInfo.setText(
|
||||
R.string.home_timetable_lessons_info,
|
||||
lessons.size,
|
||||
firstLesson?.displayStartTime?.stringHM ?: "?",
|
||||
lastLesson?.displayEndTime?.stringHM ?: "?"
|
||||
)
|
||||
|
||||
b.lessonBig.setText(R.string.home_timetable_lesson_first, firstLesson.subjectSpannable)
|
||||
firstLesson?.displayClassroom?.let {
|
||||
b.classroom.visibility = View.VISIBLE
|
||||
b.classroom.text = it
|
||||
} ?: run {
|
||||
b.classroom.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
val text = mutableListOf<CharSequence>(
|
||||
activity.getString(R.string.home_timetable_later)
|
||||
)
|
||||
var first = true
|
||||
for (lesson in lessons) {
|
||||
if (first) { first = false; continue }
|
||||
text += listOf(
|
||||
lesson.displayStartTime?.stringHM,
|
||||
lesson.subjectSpannable
|
||||
).concat(" ")
|
||||
}
|
||||
if (text.size == 1)
|
||||
text += activity.getString(R.string.home_timetable_later_no_lessons)
|
||||
b.nextLessons.text = text.concat("\n")
|
||||
}}
|
||||
|
||||
private val LessonFull?.subjectSpannable: CharSequence
|
||||
get() = if (this == null) "?" else when {
|
||||
isCancelled -> displaySubjectName.asStrikethroughSpannable()
|
||||
isChange -> displaySubjectName.asItalicSpannable()
|
||||
else -> displaySubjectName ?: "?"
|
||||
}
|
||||
|
||||
private fun count() {
|
||||
val counterStart = counterStart
|
||||
val counterEnd = counterEnd
|
||||
if (counterStart == null || counterEnd == null) {
|
||||
// there is no lesson to count
|
||||
b.progress.visibility = View.GONE
|
||||
b.counter.visibility = View.GONE
|
||||
this.counterJob?.cancel()
|
||||
return
|
||||
}
|
||||
|
||||
val now = syncedNow
|
||||
if (now > counterEnd) {
|
||||
// the lesson is already over
|
||||
b.progress.visibility = View.GONE
|
||||
b.counter.visibility = View.GONE
|
||||
this.counterJob?.cancel()
|
||||
this.counterStart = null
|
||||
this.counterEnd = null
|
||||
update() // check for new lessons to display
|
||||
return
|
||||
}
|
||||
|
||||
val isOngoing = counterStart <= now && now <= counterEnd
|
||||
val lessonRes = if (isOngoing)
|
||||
R.string.home_timetable_lesson_ongoing
|
||||
else
|
||||
R.string.home_timetable_lesson_not_started
|
||||
b.lessonBig.setText(lessonRes, subjectSpannable ?: "")
|
||||
|
||||
if (now < counterStart) {
|
||||
// the lesson hasn't yet started
|
||||
b.progress.visibility = View.GONE
|
||||
b.counter.visibility = View.VISIBLE
|
||||
val diff = counterStart - now
|
||||
b.counter.text = activity.timeTill(diff.toInt(), "\n")
|
||||
}
|
||||
else {
|
||||
// the lesson is right now
|
||||
b.progress.visibility = View.VISIBLE
|
||||
b.counter.visibility = View.VISIBLE
|
||||
val lessonLength = counterEnd - counterStart
|
||||
val timePassed = now - counterStart
|
||||
val timeLeft = counterEnd - now
|
||||
b.counter.text = activity.timeLeft(timeLeft.toInt(), "\n")
|
||||
b.progress.max = lessonLength.toInt()
|
||||
b.progress.progress = timePassed.toInt()
|
||||
}
|
||||
}
|
||||
|
||||
override fun unbind(position: Int, holder: HomeCardAdapter.ViewHolder) = Unit
|
||||
}
|
@ -3,6 +3,8 @@ package pl.szczodrzynski.edziennik.utils.models;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Calendar;
|
||||
|
||||
public class Time implements Comparable<Time> {
|
||||
@ -114,6 +116,10 @@ public class Time implements Comparable<Time> {
|
||||
return new Time(c.get(Calendar.HOUR_OF_DAY), c.get(Calendar.MINUTE), c.get(Calendar.SECOND));
|
||||
}
|
||||
|
||||
public long getInUnix() {
|
||||
return getInMillis() / 1000;
|
||||
}
|
||||
|
||||
public int getValue()
|
||||
{
|
||||
return hour * 10000 + minute * 100 + second;
|
||||
@ -202,4 +208,8 @@ public class Time implements Comparable<Time> {
|
||||
result = 31 * result + second;
|
||||
return result;
|
||||
}
|
||||
|
||||
public long minus(@NotNull Time other) {
|
||||
return getInUnix() - other.getInUnix();
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user