[UI/Timetable] Add Timetable settings. (#140)

* [UI/Timetable] Add timetable config dialog and implement UI options.

* [UI/Timetable] Fix reloading timetable after changing config.

* [UI/Timetable] Fix calculating lesson range boundaries.
This commit is contained in:
Kuba Szczodrzyński 2022-10-14 00:25:22 +02:00 committed by GitHub
parent c983c16907
commit 55369eaa8b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 173 additions and 28 deletions

View File

@ -69,4 +69,19 @@ class ProfileConfigUI(private val config: ProfileConfig) {
var messagesGreetingText: String? var messagesGreetingText: String?
get() { mMessagesGreetingText = mMessagesGreetingText ?: config.values["messagesGreetingText"]; return mMessagesGreetingText } get() { mMessagesGreetingText = mMessagesGreetingText ?: config.values["messagesGreetingText"]; return mMessagesGreetingText }
set(value) { config.set("messagesGreetingText", value); mMessagesGreetingText = value } set(value) { config.set("messagesGreetingText", value); mMessagesGreetingText = value }
private var mTimetableShowAttendance: Boolean? = null
var timetableShowAttendance: Boolean
get() { mTimetableShowAttendance = mTimetableShowAttendance ?: config.values.get("timetableShowAttendance", true); return mTimetableShowAttendance ?: true }
set(value) { config.set("timetableShowAttendance", value); mTimetableShowAttendance = value }
private var mTimetableShowEvents: Boolean? = null
var timetableShowEvents: Boolean
get() { mTimetableShowEvents = mTimetableShowEvents ?: config.values.get("timetableShowEvents", true); return mTimetableShowEvents ?: true }
set(value) { config.set("timetableShowEvents", value); mTimetableShowEvents = value }
private var mTimetableTrimHourRange: Boolean? = null
var timetableTrimHourRange: Boolean
get() { mTimetableTrimHourRange = mTimetableTrimHourRange ?: config.values.get("timetableTrimHourRange", false); return mTimetableTrimHourRange ?: false }
set(value) { config.set("timetableTrimHourRange", value); mTimetableTrimHourRange = value }
} }

View File

@ -0,0 +1,44 @@
/*
* Copyright (c) Kuba Szczodrzyński 2022-10-7.
*/
package pl.szczodrzynski.edziennik.ui.dialogs.settings
import android.view.LayoutInflater
import androidx.appcompat.app.AppCompatActivity
import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.databinding.TimetableConfigDialogBinding
import pl.szczodrzynski.edziennik.ext.Intent
import pl.szczodrzynski.edziennik.ext.onClick
import pl.szczodrzynski.edziennik.ui.dialogs.base.ConfigDialog
import pl.szczodrzynski.edziennik.ui.timetable.TimetableFragment
class TimetableConfigDialog(
activity: AppCompatActivity,
reloadOnDismiss: Boolean = true,
onShowListener: ((tag: String) -> Unit)? = null,
onDismissListener: ((tag: String) -> Unit)? = null,
) : ConfigDialog<TimetableConfigDialogBinding>(
activity,
reloadOnDismiss,
onShowListener,
onDismissListener,
) {
override val TAG = "TimetableConfigDialog"
override fun getTitleRes() = R.string.menu_timetable_config
override fun inflate(layoutInflater: LayoutInflater) =
TimetableConfigDialogBinding.inflate(layoutInflater)
private val profileConfig by lazy { app.config.getFor(app.profileId).ui }
override suspend fun loadConfig() {
b.config = profileConfig
}
override suspend fun saveConfig() {
activity.sendBroadcast(Intent(TimetableFragment.ACTION_RELOAD_PAGES))
}
}

View File

@ -36,6 +36,7 @@ import pl.szczodrzynski.edziennik.ui.timetable.TimetableFragment.Companion.DEFAU
import pl.szczodrzynski.edziennik.utils.managers.NoteManager import pl.szczodrzynski.edziennik.utils.managers.NoteManager
import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Time import pl.szczodrzynski.edziennik.utils.models.Time
import pl.szczodrzynski.edziennik.utils.mutableLazy
import java.util.* import java.util.*
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.math.min import kotlin.math.min
@ -71,8 +72,9 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope {
// find SwipeRefreshLayout in the hierarchy // find SwipeRefreshLayout in the hierarchy
private val refreshLayout by lazy { view?.findParentById(R.id.refreshLayout) } private val refreshLayout by lazy { view?.findParentById(R.id.refreshLayout) }
private val profileConfig by lazy { app.config.forProfile().ui }
private val dayView by lazy { private val dayViewDelegate = mutableLazy {
val dayView = DayView(activity, DayViewConfig( val dayView = DayView(activity, DayViewConfig(
startHour = startHour, startHour = startHour,
endHour = endHour, endHour = endHour,
@ -85,8 +87,9 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope {
eventMargin = 2.dp eventMargin = 2.dp
), true) ), true)
dayView.setPadding(10.dp) dayView.setPadding(10.dp)
return@lazy dayView return@mutableLazy dayView
} }
private val dayView by dayViewDelegate
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
activity = (getActivity() as MainActivity?) ?: return null activity = (getActivity() as MainActivity?) ?: return null
@ -173,8 +176,19 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope {
return return
} }
b.scrollView.isVisible = true if (dayViewDelegate.isInitialized())
b.dayFrame.removeView(dayView) b.dayFrame.removeView(dayView)
val lessonsActual = lessons.filter { it.type != Lesson.TYPE_NO_LESSONS }
if (profileConfig.timetableTrimHourRange) {
dayViewDelegate.deinitialize()
// end/start defaults are swapped on purpose
startHour = lessonsActual.minOf { it.displayStartTime?.hour ?: DEFAULT_END_HOUR }
endHour = lessonsActual.maxOf { it.displayEndTime?.hour?.plus(1) ?: DEFAULT_START_HOUR }
}
b.scrollView.isVisible = true
b.dayFrame.addView(dayView, 0) b.dayFrame.addView(dayView, 0)
// Inflate a label view for each hour the day view will display // Inflate a label view for each hour the day view will display
@ -195,7 +209,7 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope {
lessons.forEach { it.showAsUnseen = !it.seen } lessons.forEach { it.showAsUnseen = !it.seen }
buildLessonViews(lessons.filter { it.type != Lesson.TYPE_NO_LESSONS }, events, attendanceList) buildLessonViews(lessonsActual, events, attendanceList)
} }
private fun buildLessonViews(lessons: List<LessonFull>, events: List<EventFull>, attendanceList: List<AttendanceFull>) { private fun buildLessonViews(lessons: List<LessonFull>, events: List<EventFull>, attendanceList: List<AttendanceFull>) {
@ -215,7 +229,10 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope {
val colorSecondary = android.R.attr.textColorSecondary.resolveAttr(activity) val colorSecondary = android.R.attr.textColorSecondary.resolveAttr(activity)
for (lesson in lessons) { for (lesson in lessons) {
val attendance = attendanceList.find { it.startTime == lesson.startTime } val attendance = if (profileConfig.timetableShowAttendance)
attendanceList.find { it.startTime == lesson.startTime }
else
null
val startTime = lesson.displayStartTime ?: continue val startTime = lesson.displayStartTime ?: continue
val endTime = lesson.displayEndTime ?: continue val endTime = lesson.displayEndTime ?: continue
@ -245,23 +262,20 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope {
} }
} }
val eventIcons = listOf(lb.event1, lb.event2, lb.event3)
if (profileConfig.timetableShowEvents) {
val eventList = events.filter { it.time != null && it.time == lesson.displayStartTime }.take(3) val eventList = events.filter { it.time != null && it.time == lesson.displayStartTime }.take(3)
eventList.getOrNull(0).let { for ((i, eventIcon) in eventIcons.withIndex()) {
lb.event1.visibility = if (it == null) View.GONE else View.VISIBLE eventList.getOrNull(i).let {
lb.event1.background = it?.let { eventIcon.isVisible = it != null
eventIcon.background = it?.let {
R.drawable.bg_circle.resolveDrawable(activity).setTintColor(it.eventColor) R.drawable.bg_circle.resolveDrawable(activity).setTintColor(it.eventColor)
} }
} }
eventList.getOrNull(1).let {
lb.event2.visibility = if (it == null) View.GONE else View.VISIBLE
lb.event2.background = it?.let {
R.drawable.bg_circle.resolveDrawable(activity).setTintColor(it.eventColor)
} }
} } else {
eventList.getOrNull(2).let { for (eventIcon in eventIcons) {
lb.event3.visibility = if (it == null) View.GONE else View.VISIBLE eventIcon.visibility = View.GONE
lb.event3.background = it?.let {
R.drawable.bg_circle.resolveDrawable(activity).setTintColor(it.eventColor)
} }
} }

View File

@ -26,6 +26,7 @@ import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.databinding.FragmentTimetableV2Binding import pl.szczodrzynski.edziennik.databinding.FragmentTimetableV2Binding
import pl.szczodrzynski.edziennik.ext.getSchoolYearConstrains import pl.szczodrzynski.edziennik.ext.getSchoolYearConstrains
import pl.szczodrzynski.edziennik.ui.dialogs.settings.TimetableConfigDialog
import pl.szczodrzynski.edziennik.ui.event.EventManualDialog import pl.szczodrzynski.edziennik.ui.event.EventManualDialog
import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem
@ -52,6 +53,7 @@ class TimetableFragment : Fragment(), CoroutineScope {
private var fabShown = false private var fabShown = false
private val items = mutableListOf<Date>() private val items = mutableListOf<Date>()
private val profileConfig by lazy { app.config.forProfile().ui }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
activity = (getActivity() as MainActivity?) ?: return null activity = (getActivity() as MainActivity?) ?: return null
@ -128,8 +130,8 @@ class TimetableFragment : Fragment(), CoroutineScope {
} }
val lessonRanges = app.db.lessonRangeDao().getAllNow(App.profileId) val lessonRanges = app.db.lessonRangeDao().getAllNow(App.profileId)
startHour = lessonRanges.map { it.startTime.hour }.minOrNull() ?: DEFAULT_START_HOUR startHour = lessonRanges.minOfOrNull { it.startTime.hour } ?: DEFAULT_START_HOUR
endHour = lessonRanges.map { it.endTime.hour }.maxOrNull()?.plus(1) ?: DEFAULT_END_HOUR endHour = lessonRanges.maxOfOrNull { it.endTime.hour }?.plus(1) ?: DEFAULT_END_HOUR
} }
deferred.await() deferred.await()
if (!isAdded) if (!isAdded)
@ -208,6 +210,13 @@ class TimetableFragment : Fragment(), CoroutineScope {
activity.bottomSheet.close() activity.bottomSheet.close()
GenerateBlockTimetableDialog(activity) GenerateBlockTimetableDialog(activity)
}), }),
BottomSheetPrimaryItem(true)
.withTitle(R.string.menu_timetable_config)
.withIcon(CommunityMaterial.Icon.cmd_cog_outline)
.withOnClickListener {
activity.bottomSheet.close()
TimetableConfigDialog(activity, false, null, null).show()
},
BottomSheetSeparatorItem(true), BottomSheetSeparatorItem(true),
BottomSheetPrimaryItem(true) BottomSheetPrimaryItem(true)
.withTitle(R.string.menu_mark_as_read) .withTitle(R.string.menu_mark_as_read)

View File

@ -19,7 +19,6 @@ class MutableLazyImpl<T>(initializer: () -> T, lock: Any? = null) {
return synchronized(lock) { return synchronized(lock) {
val typedValue = initializer!!() val typedValue = initializer!!()
_value = typedValue _value = typedValue
initializer = null
typedValue typedValue
} }
} }
@ -29,6 +28,10 @@ class MutableLazyImpl<T>(initializer: () -> T, lock: Any? = null) {
fun isInitialized() = _value !== UNINITIALIZED_VALUE fun isInitialized() = _value !== UNINITIALIZED_VALUE
fun deinitialize() {
_value = UNINITIALIZED_VALUE
}
override fun toString() = if (isInitialized()) _value.toString() else "ChangeableLazy value not initialized yet." override fun toString() = if (isInitialized()) _value.toString() else "ChangeableLazy value not initialized yet."
} }

View File

@ -31,7 +31,7 @@
<TextView <TextView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/agenda_config_appearance" android:text="@string/config_appearance"
android:textAppearance="@style/NavView.TextView.Subtitle" /> android:textAppearance="@style/NavView.TextView.Subtitle" />
<com.google.android.material.checkbox.MaterialCheckBox <com.google.android.material.checkbox.MaterialCheckBox

View File

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kuba Szczodrzyński 2022-10-7.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="config"
type="pl.szczodrzynski.edziennik.config.ProfileConfigUI" />
</data>
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/config_appearance"
android:textAppearance="@style/NavView.TextView.Subtitle" />
<com.google.android.material.checkbox.MaterialCheckBox
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:checked="@={config.timetableShowEvents}"
android:minHeight="32dp"
android:text="@string/timetable_config_show_events" />
<com.google.android.material.checkbox.MaterialCheckBox
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:checked="@={config.timetableShowAttendance}"
android:minHeight="32dp"
android:text="@string/timetable_config_show_attendance" />
<com.google.android.material.checkbox.MaterialCheckBox
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:checked="@={config.timetableTrimHourRange}"
android:minHeight="32dp"
android:text="@string/timetable_config_trim_hour_range" />
</LinearLayout>
</ScrollView>
</layout>

View File

@ -1383,7 +1383,7 @@
<string name="notes_type_grade">Grade</string> <string name="notes_type_grade">Grade</string>
<string name="privacy_policy_dialog_html"><![CDATA[By using the application, you confirm that you have <a href="https://szkolny.eu/privacy-policy">read the Privacy Policy</a> and accept its provisions.<br /><br />The authors of the application are not responsible for the use of the Szkolny.eu application.]]></string> <string name="privacy_policy_dialog_html"><![CDATA[By using the application, you confirm that you have <a href="https://szkolny.eu/privacy-policy">read the Privacy Policy</a> and accept its provisions.<br /><br />The authors of the application are not responsible for the use of the Szkolny.eu application.]]></string>
<string name="login_chooser_version_format">Szkolny.eu v%s\n%s</string> <string name="login_chooser_version_format">Szkolny.eu v%s\n%s</string>
<string name="agenda_config_appearance">Appearance</string> <string name="config_appearance">Appearance</string>
<string name="agenda_config_elearning">Online learning</string> <string name="agenda_config_elearning">Online learning</string>
<string name="event_type_elearning">online lesson</string> <string name="event_type_elearning">online lesson</string>
<string name="menu_messages_config">Messages settings</string> <string name="menu_messages_config">Messages settings</string>

View File

@ -1424,7 +1424,7 @@
<string name="privacy_policy_dialog_html"><![CDATA[Korzystając z aplikacji potwierdzasz <a href="https://szkolny.eu/privacy-policy">przeczytanie Polityki prywatności</a> i akceptujesz jej postanowienia.<br /><br />Autorzy aplikacji nie biorą odpowiedzialności za korzystanie z aplikacji Szkolny.eu.]]></string> <string name="privacy_policy_dialog_html"><![CDATA[Korzystając z aplikacji potwierdzasz <a href="https://szkolny.eu/privacy-policy">przeczytanie Polityki prywatności</a> i akceptujesz jej postanowienia.<br /><br />Autorzy aplikacji nie biorą odpowiedzialności za korzystanie z aplikacji Szkolny.eu.]]></string>
<string name="login_chooser_version_format">Szkolny.eu v%s\n%s</string> <string name="login_chooser_version_format">Szkolny.eu v%s\n%s</string>
<string name="menu_agenda_config">Ustawienia terminarza</string> <string name="menu_agenda_config">Ustawienia terminarza</string>
<string name="agenda_config_appearance">Wygląd</string> <string name="config_appearance">Wygląd</string>
<string name="agenda_config_lesson_changes">Pokazuj zmiany planu lekcji</string> <string name="agenda_config_lesson_changes">Pokazuj zmiany planu lekcji</string>
<string name="agenda_config_teacher_absence">Pokazuj nieobecności nauczycieli</string> <string name="agenda_config_teacher_absence">Pokazuj nieobecności nauczycieli</string>
<string name="agenda_config_compact_mode">Tryb kompaktowy</string> <string name="agenda_config_compact_mode">Tryb kompaktowy</string>
@ -1537,4 +1537,8 @@
<string name="login_summary_account_parent">(rodzic)</string> <string name="login_summary_account_parent">(rodzic)</string>
<string name="grades_subject_unknown">- nieznany przedmiot -</string> <string name="grades_subject_unknown">- nieznany przedmiot -</string>
<string name="grades_subject_unknown_help">{cmd-information-outline} Oceny, których przedmiot nie został podany w dzienniku. Może to być na przykład taki, który nie jest prowadzony w tym roku szkolnym.</string> <string name="grades_subject_unknown_help">{cmd-information-outline} Oceny, których przedmiot nie został podany w dzienniku. Może to być na przykład taki, który nie jest prowadzony w tym roku szkolnym.</string>
<string name="menu_timetable_config">Ustawienia planu lekcji</string>
<string name="timetable_config_show_events">Pokazuj wydarzenia przy lekcjach</string>
<string name="timetable_config_show_attendance">Pokazuj rodzaj obecności na lekcji</string>
<string name="timetable_config_trim_hour_range">Nie pokazuj godzin bez lekcji</string>
</resources> </resources>