From 37f3d76fb89cad39f84bb58f2aae95ac1e75b5c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Mon, 25 Nov 2019 22:15:36 +0100 Subject: [PATCH] [UI] Implement home timetable card. --- app/sampledata/settings/ic_settings.xml | 13 ++ .../pl/szczodrzynski/edziennik/Extensions.kt | 60 +++++- .../data/db/modules/timetable/Lesson.kt | 37 +--- .../modules/home/cards/HomeTimetableCard.kt | 199 ++++++++++++++++-- .../edziennik/utils/models/Time.java | 10 + .../main/res/layout/card_home_timetable.xml | 110 ++++++---- app/src/main/res/values/attrs.xml | 1 + app/src/main/res/values/strings.xml | 10 + app/src/main/res/values/styles.xml | 2 + 9 files changed, 348 insertions(+), 94 deletions(-) create mode 100644 app/sampledata/settings/ic_settings.xml diff --git a/app/sampledata/settings/ic_settings.xml b/app/sampledata/settings/ic_settings.xml new file mode 100644 index 00000000..efaaa4ee --- /dev/null +++ b/app/sampledata/settings/ic_settings.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt b/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt index 761f0dd6..0334cde6 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt @@ -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 listOfNotEmpty(vararg elements: T): List = elements.filterNot { it.isEmpty() } -fun List.concat(delimiter: String? = null): CharSequence { +fun List.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.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.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>() + + 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>() + + 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) } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/modules/timetable/Lesson.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/modules/timetable/Lesson.kt index d032bb77..5cdac97e 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/modules/timetable/Lesson.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/modules/timetable/Lesson.kt @@ -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) -); -*/ +} \ No newline at end of file diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/cards/HomeTimetableCard.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/cards/HomeTimetableCard.kt index 4255f9da..65fe6237 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/cards/HomeTimetableCard.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/cards/HomeTimetableCard.kt @@ -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() private var events = listOf() + 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( + 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 } \ No newline at end of file diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/models/Time.java b/app/src/main/java/pl/szczodrzynski/edziennik/utils/models/Time.java index 1f1d1089..b9b57b72 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/models/Time.java +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/models/Time.java @@ -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