diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 29204aff..7643783a 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -15,6 +15,7 @@ xmlns:android + ^$ @@ -25,6 +26,7 @@ xmlns:.* + ^$ @@ -36,6 +38,7 @@ .*:id + http://schemas.android.com/apk/res/android @@ -46,6 +49,7 @@ .*:name + http://schemas.android.com/apk/res/android @@ -56,6 +60,7 @@ name + ^$ @@ -66,6 +71,7 @@ style + ^$ @@ -76,6 +82,7 @@ .* + ^$ @@ -87,6 +94,7 @@ .* + http://schemas.android.com/apk/res/android @@ -98,6 +106,7 @@ .* + .* diff --git a/.idea/dictionaries/Kuba.xml b/.idea/dictionaries/Kuba.xml index 7910687c..592a5d5e 100644 --- a/.idea/dictionaries/Kuba.xml +++ b/.idea/dictionaries/Kuba.xml @@ -5,6 +5,7 @@ ciasteczko csrf edziennik + elearning gson hebe idziennik diff --git a/app/build.gradle b/app/build.gradle index 72c03e94..c0af6190 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -152,7 +152,8 @@ dependencies { implementation "pl.droidsonroids.retrofit2:converter-jspoon:1.3.2" // Szkolny.eu libraries/forks - implementation "eu.szkolny:agendacalendarview:1799f8ef47" + implementation "eu.szkolny:android-snowfall:1ca9ea2da3" + implementation "eu.szkolny:agendacalendarview:5431f03098" implementation "eu.szkolny:cafebar:5bf0c618de" implementation "eu.szkolny.fslogin:lib:2.0.0" implementation "eu.szkolny:material-about-library:1d5ebaf47c" @@ -180,7 +181,6 @@ dependencies { implementation "com.github.bassaer:chatmessageview:2.0.1" implementation "com.github.CanHub:Android-Image-Cropper:2.2.2" implementation "com.github.ChuckerTeam.Chucker:library:3.0.1" - implementation "com.github.jetradarmobile:android-snowfall:1.2.0" implementation "com.github.wulkanowy.uonet-request-signer:hebe-jvm:a99ca50a31" implementation("com.heinrichreimersoftware:material-intro") { version { strictly "1.5.8" } } implementation "com.hypertrack:hyperlog:0.0.10" diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 4d84d11d..0816fb00 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -32,8 +32,9 @@ -keepnames class pl.szczodrzynski.edziennik.ui.widgets.luckynumber.WidgetLuckyNumberProvider -keepnames class androidx.appcompat.view.menu.MenuBuilder { setHeaderTitleInt(java.lang.CharSequence); } --keepclassmembernames class androidx.appcompat.view.menu.StandardMenuPopup { private *; } -keepnames class androidx.appcompat.view.menu.MenuPopupHelper { showPopup(int, int, boolean, boolean); } +-keepclassmembernames class androidx.appcompat.view.menu.StandardMenuPopup { private *; } +-keepclassmembernames class androidx.appcompat.view.menu.MenuItemImpl { private *; } -keepclassmembernames class com.mikepenz.materialdrawer.widget.MiniDrawerSliderView { private *; } diff --git a/app/src/main/assets/pl-changelog.html b/app/src/main/assets/pl-changelog.html index 2219ad8c..27c85f0f 100644 --- a/app/src/main/assets/pl-changelog.html +++ b/app/src/main/assets/pl-changelog.html @@ -1,13 +1,13 @@ -

Wersja 4.7, 2021-04-07

+

Wersja 4.8, 2021-05-26



diff --git a/app/src/main/cpp/szkolny-signing.cpp b/app/src/main/cpp/szkolny-signing.cpp index f2b1cf8a..9af553cb 100644 --- a/app/src/main/cpp/szkolny-signing.cpp +++ b/app/src/main/cpp/szkolny-signing.cpp @@ -9,7 +9,7 @@ /*secret password - removed for source code publication*/ static toys AES_IV[16] = { - 0xda, 0x9f, 0xd4, 0x2b, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }; + 0x71, 0xcf, 0xdf, 0x13, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }; unsigned char *agony(unsigned int laugh, unsigned char *box, unsigned char *heat); diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt b/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt index a07634a1..8d3b59d1 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt @@ -15,6 +15,7 @@ import android.graphics.drawable.Drawable import android.os.Build import android.os.Bundle import android.text.* +import android.text.style.CharacterStyle import android.text.style.ForegroundColorSpan import android.text.style.StrikethroughSpan import android.text.style.StyleSpan @@ -552,28 +553,46 @@ fun CharSequence?.asBoldSpannable(): Spannable { spannable.setSpan(StyleSpan(Typeface.BOLD), 0, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) return spannable } -fun CharSequence.asSpannable(vararg spans: Any, substring: String? = null, ignoreCase: Boolean = false, ignoreDiacritics: Boolean = false): Spannable { +fun CharSequence.asSpannable( + vararg spans: CharacterStyle, + substring: CharSequence? = null, + ignoreCase: Boolean = false, + ignoreDiacritics: Boolean = false +): Spannable { val spannable = SpannableString(this) - if (substring == null) { - spans.forEach { - spannable.setSpan(it, 0, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - } - } - else if (substring.isNotEmpty()) { - val string = - if (ignoreDiacritics) - this.cleanDiacritics() - else this + substring?.let { substr -> + val string = if (ignoreDiacritics) + this.cleanDiacritics() + else + this + val search = if (ignoreDiacritics) + substr.cleanDiacritics() + else + substr.toString() - var index = string.indexOf(substring, ignoreCase = ignoreCase) - .takeIf { it != -1 } ?: indexOf(substring, ignoreCase = ignoreCase) - while (index >= 0) { - spans.forEach { - spannable.setSpan(it, index, index + substring.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + var index = 0 + do { + index = string.indexOf( + string = search, + startIndex = index, + ignoreCase = ignoreCase + ) + + if (index >= 0) { + spans.forEach { + spannable.setSpan( + CharacterStyle.wrap(it), + index, + index + substring.length, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) + } + index += substring.length.coerceAtLeast(1) } - index = string.indexOf(substring, startIndex = index + 1, ignoreCase = ignoreCase) - .takeIf { it != -1 } ?: indexOf(substring, startIndex = index + 1, ignoreCase = ignoreCase) - } + } while (index >= 0) + + } ?: spans.forEach { + spannable.setSpan(it, 0, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) } return spannable } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt b/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt index 8dd83bf2..2cd4f7b7 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt @@ -21,6 +21,7 @@ import androidx.lifecycle.Observer import androidx.navigation.NavOptions import com.danimahardhika.cafebar.CafeBar import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.jetradarmobile.snowfall.SnowfallView import com.mikepenz.iconics.IconicsDrawable import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial import com.mikepenz.iconics.utils.colorInt @@ -81,12 +82,9 @@ import pl.szczodrzynski.edziennik.ui.modules.settings.ProfileManagerFragment import pl.szczodrzynski.edziennik.ui.modules.settings.SettingsFragment import pl.szczodrzynski.edziennik.ui.modules.timetable.TimetableFragment import pl.szczodrzynski.edziennik.ui.modules.webpush.WebPushFragment -import pl.szczodrzynski.edziennik.utils.SwipeRefreshLayoutNoTouch -import pl.szczodrzynski.edziennik.utils.Themes -import pl.szczodrzynski.edziennik.utils.Utils +import pl.szczodrzynski.edziennik.utils.* import pl.szczodrzynski.edziennik.utils.Utils.d import pl.szczodrzynski.edziennik.utils.Utils.dpToPx -import pl.szczodrzynski.edziennik.utils.appManagerIntentList import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.NavTarget import pl.szczodrzynski.navlib.* @@ -470,9 +468,21 @@ class MainActivity : AppCompatActivity(), CoroutineScope { // IT'S WINTER MY DUDES val today = Date.getToday() - if ((today.month == 12 || today.month == 1) && app.config.ui.snowfall) { + if ((today.month % 11 == 1) && app.config.ui.snowfall) { b.rootFrame.addView(layoutInflater.inflate(R.layout.snowfall, b.rootFrame, false)) } + else if (app.config.ui.eggfall && BigNightUtil().isDataWielkanocyNearDzisiaj()) { + val eggfall = layoutInflater.inflate(R.layout.eggfall, b.rootFrame, false) as SnowfallView + eggfall.setSnowflakeBitmaps(listOf( + BitmapFactory.decodeResource(resources, R.drawable.egg1), + BitmapFactory.decodeResource(resources, R.drawable.egg2), + BitmapFactory.decodeResource(resources, R.drawable.egg3), + BitmapFactory.decodeResource(resources, R.drawable.egg4), + BitmapFactory.decodeResource(resources, R.drawable.egg5), + BitmapFactory.decodeResource(resources, R.drawable.egg6) + )) + b.rootFrame.addView(eggfall) + } // WHAT'S NEW DIALOG if (app.config.appVersion < BuildConfig.VERSION_CODE) { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/config/ConfigSync.kt b/app/src/main/java/pl/szczodrzynski/edziennik/config/ConfigSync.kt index 068400a1..387e5cf7 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/config/ConfigSync.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/config/ConfigSync.kt @@ -6,6 +6,7 @@ package pl.szczodrzynski.edziennik.config import com.google.gson.Gson import com.google.gson.reflect.TypeToken +import pl.szczodrzynski.edziennik.BuildConfig import pl.szczodrzynski.edziennik.config.utils.get import pl.szczodrzynski.edziennik.config.utils.getIntList import pl.szczodrzynski.edziennik.config.utils.set @@ -123,6 +124,19 @@ class ConfigSync(private val config: Config) { private var mRegisterAvailability: Map? = null var registerAvailability: Map - get() { mRegisterAvailability = mRegisterAvailability ?: config.values.get("registerAvailability", null as String?)?.let { it -> gson.fromJson>(it, object: TypeToken>(){}.type) }; return mRegisterAvailability ?: mapOf() } - set(value) { config.setMap("registerAvailability", value); mRegisterAvailability = value } + get() { + val flavor = config.values.get("registerAvailabilityFlavor", null as String?) + if (BuildConfig.FLAVOR != flavor) + return mapOf() + + mRegisterAvailability = mRegisterAvailability ?: config.values.get("registerAvailability", null as String?)?.let { it -> + gson.fromJson(it, object: TypeToken>(){}.type) + } + return mRegisterAvailability ?: mapOf() + } + set(value) { + config.setMap("registerAvailability", value) + config.set("registerAvailabilityFlavor", BuildConfig.FLAVOR) + mRegisterAvailability = value + } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/config/ConfigUI.kt b/app/src/main/java/pl/szczodrzynski/edziennik/config/ConfigUI.kt index 7b39383e..f36652a1 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/config/ConfigUI.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/config/ConfigUI.kt @@ -49,6 +49,11 @@ class ConfigUI(private val config: Config) { get() { mSnowfall = mSnowfall ?: config.values.get("snowfall", false); return mSnowfall ?: false } set(value) { config.set("snowfall", value); mSnowfall = value } + private var mEggfall: Boolean? = null + var eggfall: Boolean + get() { mEggfall = mEggfall ?: config.values.get("eggfall", false); return mEggfall ?: false } + set(value) { config.set("eggfall", value); mEggfall = value } + private var mBottomSheetOpened: Boolean? = null var bottomSheetOpened: Boolean get() { mBottomSheetOpened = mBottomSheetOpened ?: config.values.get("bottomSheetOpened", false); return mBottomSheetOpened ?: false } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfigUI.kt b/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfigUI.kt index 5ce237a0..d40f0dfc 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfigUI.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfigUI.kt @@ -15,8 +15,58 @@ class ProfileConfigUI(private val config: ProfileConfig) { get() { mAgendaViewType = mAgendaViewType ?: config.values.get("agendaViewType", 0); return mAgendaViewType ?: AGENDA_DEFAULT } set(value) { config.set("agendaViewType", value); mAgendaViewType = value } + private var mAgendaCompactMode: Boolean? = null + var agendaCompactMode: Boolean + get() { mAgendaCompactMode = mAgendaCompactMode ?: config.values.get("agendaCompactMode", false); return mAgendaCompactMode ?: false } + set(value) { config.set("agendaCompactMode", value); mAgendaCompactMode = value } + + private var mAgendaGroupByType: Boolean? = null + var agendaGroupByType: Boolean + get() { mAgendaGroupByType = mAgendaGroupByType ?: config.values.get("agendaGroupByType", false); return mAgendaGroupByType ?: false } + set(value) { config.set("agendaGroupByType", value); mAgendaGroupByType = value } + + private var mAgendaLessonChanges: Boolean? = null + var agendaLessonChanges: Boolean + get() { mAgendaLessonChanges = mAgendaLessonChanges ?: config.values.get("agendaLessonChanges", true); return mAgendaLessonChanges ?: true } + set(value) { config.set("agendaLessonChanges", value); mAgendaLessonChanges = value } + + private var mAgendaTeacherAbsence: Boolean? = null + var agendaTeacherAbsence: Boolean + get() { mAgendaTeacherAbsence = mAgendaTeacherAbsence ?: config.values.get("agendaTeacherAbsence", true); return mAgendaTeacherAbsence ?: true } + set(value) { config.set("agendaTeacherAbsence", value); mAgendaTeacherAbsence = value } + + private var mAgendaElearningMark: Boolean? = null + var agendaElearningMark: Boolean + get() { mAgendaElearningMark = mAgendaElearningMark ?: config.values.get("agendaElearningMark", false); return mAgendaElearningMark ?: false } + set(value) { config.set("agendaElearningMark", value); mAgendaElearningMark = value } + + private var mAgendaElearningGroup: Boolean? = null + var agendaElearningGroup: Boolean + get() { mAgendaElearningGroup = mAgendaElearningGroup ?: config.values.get("agendaElearningGroup", true); return mAgendaElearningGroup ?: true } + set(value) { config.set("agendaElearningGroup", value); mAgendaElearningGroup = value } + private var mHomeCards: List? = null var homeCards: List get() { mHomeCards = mHomeCards ?: config.values.get("homeCards", listOf(), HomeCardModel::class.java); return mHomeCards ?: listOf() } set(value) { config.set("homeCards", value); mHomeCards = value } + + private var mMessagesGreetingOnCompose: Boolean? = null + var messagesGreetingOnCompose: Boolean + get() { mMessagesGreetingOnCompose = mMessagesGreetingOnCompose ?: config.values.get("messagesGreetingOnCompose", true); return mMessagesGreetingOnCompose ?: true } + set(value) { config.set("messagesGreetingOnCompose", value); mMessagesGreetingOnCompose = value } + + private var mMessagesGreetingOnReply: Boolean? = null + var messagesGreetingOnReply: Boolean + get() { mMessagesGreetingOnReply = mMessagesGreetingOnReply ?: config.values.get("messagesGreetingOnReply", true); return mMessagesGreetingOnReply ?: true } + set(value) { config.set("messagesGreetingOnReply", value); mMessagesGreetingOnReply = value } + + private var mMessagesGreetingOnForward: Boolean? = null + var messagesGreetingOnForward: Boolean + get() { mMessagesGreetingOnForward = mMessagesGreetingOnForward ?: config.values.get("messagesGreetingOnForward", false); return mMessagesGreetingOnForward ?: false } + set(value) { config.set("messagesGreetingOnForward", value); mMessagesGreetingOnForward = value } + + private var mMessagesGreetingText: String? = null + var messagesGreetingText: String? + get() { mMessagesGreetingText = mMessagesGreetingText ?: config.values["messagesGreetingText"]; return mMessagesGreetingText } + set(value) { config.set("messagesGreetingText", value); mMessagesGreetingText = value } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebMessagesAll.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebMessagesAll.kt index 4e8f900a..5581dcc5 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebMessagesAll.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebMessagesAll.kt @@ -17,6 +17,7 @@ import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.fixName import pl.szczodrzynski.edziennik.singleOrNull import pl.szczodrzynski.edziennik.utils.models.Date +import java.net.URLEncoder class MobidziennikWebMessagesAll(override val data: DataMobidziennik, override val lastSync: Long?, @@ -27,7 +28,8 @@ class MobidziennikWebMessagesAll(override val data: DataMobidziennik, } init { - webGet(TAG, "/dziennik/wyszukiwarkawiadomosci?q=+") { text -> + val query = URLEncoder.encode(data.profile?.studentNameLong ?: "a", "UTF-8") + webGet(TAG, "/dziennik/wyszukiwarkawiadomosci?q=$query") { text -> MobidziennikLuckyNumberExtractor(data, text) val doc = Jsoup.parse(text) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/Signing.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/Signing.kt index 819194d8..a6095449 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/Signing.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/Signing.kt @@ -46,6 +46,6 @@ object Signing { /*fun provideKey(param1: String, param2: Long): ByteArray {*/ fun pleaseStopRightNow(param1: String, param2: Long): ByteArray { - return "$param1.MTIzNDU2Nzg5MDLPrcQX7M===.$param2".sha256() + return "$param1.MTIzNDU2Nzg5MDZ/2nExVD===.$param2".sha256() } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/task/Notifications.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/task/Notifications.kt index 21a5c112..28b5c9e9 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/task/Notifications.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/task/Notifications.kt @@ -10,6 +10,7 @@ import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.data.db.entity.* import pl.szczodrzynski.edziennik.getNotificationTitle import pl.szczodrzynski.edziennik.utils.models.Date +import pl.szczodrzynski.edziennik.utils.models.Week class Notifications(val app: App, val notifications: MutableList, val profiles: List) { companion object { @@ -42,13 +43,22 @@ class Notifications(val app: App, val notifications: MutableList, val text = app.getString( R.string.notification_lesson_change_format, lesson.getDisplayChangeType(app), - if (lesson.displayDate == null) "" else lesson.displayDate!!.formattedString, + lesson.displayDate?.formattedString ?: "", lesson.changeSubjectName ) + val textLong = app.getString( + R.string.notification_lesson_change_long_format, + lesson.getDisplayChangeType(app), + lesson.displayDate?.formattedString ?: "-", + lesson.displayDate?.weekDay?.let { Week.getFullDayName(it) } ?: "-", + lesson.changeSubjectName, + lesson.changeTeacherName + ) notifications += Notification( id = Notification.buildId(lesson.profileId, Notification.TYPE_TIMETABLE_LESSON_CHANGE, lesson.id), title = app.getNotificationTitle(Notification.TYPE_TIMETABLE_LESSON_CHANGE), text = text, + textLong = textLong, type = Notification.TYPE_TIMETABLE_LESSON_CHANGE, profileId = lesson.profileId, profileName = profiles.singleOrNull { it.id == lesson.profileId }?.name, @@ -79,11 +89,21 @@ class Notifications(val app: App, val notifications: MutableList, event.date.formattedString, event.subjectLongName ) + val textLong = app.getString( + R.string.notification_event_long_format, + event.typeName ?: "-", + event.subjectLongName ?: "-", + event.date.formattedString, + Week.getFullDayName(event.date.weekDay), + event.time?.stringHM ?: app.getString(R.string.event_all_day), + event.topic.take(200) + ) val type = if (event.type == Event.TYPE_HOMEWORK) Notification.TYPE_NEW_HOMEWORK else Notification.TYPE_NEW_EVENT notifications += Notification( id = Notification.buildId(event.profileId, type, event.id), title = app.getNotificationTitle(type), text = text, + textLong = textLong, type = type, profileId = event.profileId, profileName = profiles.singleOrNull { it.id == event.profileId }?.name, @@ -102,11 +122,22 @@ class Notifications(val app: App, val notifications: MutableList, event.date.formattedString, event.topic ) + val textLong = app.getString( + R.string.notification_shared_event_long_format, + event.sharedByName, + event.typeName ?: "-", + event.subjectLongName ?: "-", + event.date.formattedString, + Week.getFullDayName(event.date.weekDay), + event.time?.stringHM ?: app.getString(R.string.event_all_day), + event.topic.take(200) + ) val type = if (event.type == Event.TYPE_HOMEWORK) Notification.TYPE_NEW_HOMEWORK else Notification.TYPE_NEW_EVENT notifications += Notification( id = Notification.buildId(event.profileId, type, event.id), title = app.getNotificationTitle(type), text = text, + textLong = textLong, type = type, profileId = event.profileId, profileName = profiles.singleOrNull { it.id == event.profileId }?.name, @@ -130,10 +161,20 @@ class Notifications(val app: App, val notifications: MutableList, gradeName, grade.subjectLongName ) + val textLong = app.getString( + R.string.notification_grade_long_format, + gradeName, + grade.weight.toString(), + grade.subjectLongName ?: "-", + grade.category ?: "-", + grade.description ?: "-", + grade.teacherName ?: "-" + ) notifications += Notification( id = Notification.buildId(grade.profileId, Notification.TYPE_NEW_GRADE, grade.id), title = app.getNotificationTitle(Notification.TYPE_NEW_GRADE), text = text, + textLong = textLong, type = Notification.TYPE_NEW_GRADE, profileId = grade.profileId, profileName = profiles.singleOrNull { it.id == grade.profileId }?.name, @@ -158,10 +199,17 @@ class Notifications(val app: App, val notifications: MutableList, notice.teacherName, Date.fromMillis(notice.addedDate).formattedString ) + val textLong = app.getString( + R.string.notification_notice_long_format, + noticeTypeStr, + notice.teacherName ?: "-", + notice.text.take(200) + ) notifications += Notification( id = Notification.buildId(notice.profileId, Notification.TYPE_NEW_NOTICE, notice.id), title = app.getNotificationTitle(Notification.TYPE_NEW_NOTICE), text = text, + textLong = textLong, type = Notification.TYPE_NEW_NOTICE, profileId = notice.profileId, profileName = profiles.singleOrNull { it.id == notice.profileId }?.name, @@ -193,10 +241,21 @@ class Notifications(val app: App, val notifications: MutableList, attendance.subjectLongName, attendance.date.formattedString ) + val textLong = app.getString( + R.string.notification_attendance_long_format, + attendanceTypeStr, + attendance.date.formattedString, + attendance.startTime?.stringHM ?: "-", + attendance.lessonNumber ?: "-", + attendance.subjectLongName ?: "-", + attendance.teacherName ?: "-", + attendance.lessonTopic ?: "-" + ) notifications += Notification( id = Notification.buildId(attendance.profileId, Notification.TYPE_NEW_ATTENDANCE, attendance.id), title = app.getNotificationTitle(Notification.TYPE_NEW_ATTENDANCE), text = text, + textLong = textLong, type = Notification.TYPE_NEW_ATTENDANCE, profileId = attendance.profileId, profileName = profiles.singleOrNull { it.id == attendance.profileId }?.name, diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/task/PostNotifications.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/task/PostNotifications.kt index 843f2174..6597125a 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/task/PostNotifications.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/task/PostNotifications.kt @@ -8,6 +8,9 @@ import android.util.SparseIntArray import androidx.core.app.NotificationCompat import androidx.core.util.forEach import androidx.core.util.set +import com.mikepenz.iconics.IconicsDrawable +import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial +import com.mikepenz.iconics.utils.* import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.data.db.entity.Notification.Companion.TYPE_SERVER_MESSAGE import pl.szczodrzynski.edziennik.utils.models.Time @@ -107,6 +110,10 @@ class PostNotifications(val app: App, nList: List) { .setContentText(buildSummaryText(summaryCounts)) .setTicker(newNotificationsText) .setSmallIcon(R.drawable.ic_notification) + .setLargeIcon(IconicsDrawable(app).apply { + icon = CommunityMaterial.Icon.cmd_bell_ring_outline + colorRes = R.color.colorPrimary + }.toBitmap()) .setStyle(NotificationCompat.InboxStyle() .also { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { @@ -137,8 +144,11 @@ class PostNotifications(val app: App, nList: List) { .setSubText(if (it.type == TYPE_SERVER_MESSAGE) null else it.title) .setTicker("${it.profileName}: ${it.title}") .setSmallIcon(R.drawable.ic_notification) + .setLargeIcon(IconicsDrawable(app, it.getLargeIcon()).apply { + colorRes = R.color.colorPrimary + }.toBitmap()) .setStyle(NotificationCompat.BigTextStyle() - .bigText(it.text)) + .bigText(it.textLong ?: it.text)) .setWhen(it.addedDate) .addDefaults() .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY) @@ -160,6 +170,10 @@ class PostNotifications(val app: App, nList: List) { .setContentText(buildSummaryText(summaryCounts)) .setTicker(newNotificationsText) .setSmallIcon(R.drawable.ic_notification) + .setLargeIcon(IconicsDrawable(app).apply { + icon = CommunityMaterial.Icon.cmd_bell_ring_outline + colorRes = R.color.colorPrimary + }.toBitmap()) .addDefaults() .setGroupSummary(true) .setContentIntent(summaryIntent) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/AppDb.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/AppDb.kt index e6d7e40e..4547d852 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/AppDb.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/AppDb.kt @@ -43,7 +43,7 @@ import pl.szczodrzynski.edziennik.data.db.migration.* LibrusLesson::class, TimetableManual::class, Metadata::class -], version = 91) +], version = 93) @TypeConverters( ConverterTime::class, ConverterDate::class, @@ -176,7 +176,9 @@ abstract class AppDb : RoomDatabase() { Migration88(), Migration89(), Migration90(), - Migration91() + Migration91(), + Migration92(), + Migration93() ).allowMainThreadQueries().build() } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/EventTypeDao.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/EventTypeDao.kt index 28be6d65..d5006970 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/EventTypeDao.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/EventTypeDao.kt @@ -9,30 +9,9 @@ import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query -import pl.szczodrzynski.edziennik.R -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_CLASS_EVENT import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_DEFAULT -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_ESSAY -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_EXAM -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_EXCURSION -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_HOMEWORK -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_INFORMATION -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_PROJECT -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_PT_MEETING -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_READING -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_SHORT_QUIZ -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_CLASS_EVENT -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_DEFAULT -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_ESSAY -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_EXAM -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_EXCURSION -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_HOMEWORK -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_INFORMATION -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_PROJECT -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_PT_MEETING -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_READING -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_SHORT_QUIZ import pl.szczodrzynski.edziennik.data.db.entity.EventType +import pl.szczodrzynski.edziennik.data.db.entity.EventType.Companion.SOURCE_DEFAULT @Dao abstract class EventTypeDao { @@ -58,19 +37,18 @@ abstract class EventTypeDao { abstract val allNow: List fun addDefaultTypes(context: Context, profileId: Int): List { - val typeList = listOf( - EventType(profileId, TYPE_HOMEWORK, context.getString(R.string.event_type_homework), COLOR_HOMEWORK), - EventType(profileId, TYPE_DEFAULT, context.getString(R.string.event_other), COLOR_DEFAULT), - EventType(profileId, TYPE_EXAM, context.getString(R.string.event_exam), COLOR_EXAM), - EventType(profileId, TYPE_SHORT_QUIZ, context.getString(R.string.event_short_quiz), COLOR_SHORT_QUIZ), - EventType(profileId, TYPE_ESSAY, context.getString(R.string.event_essay), COLOR_ESSAY), - EventType(profileId, TYPE_PROJECT, context.getString(R.string.event_project), COLOR_PROJECT), - EventType(profileId, TYPE_PT_MEETING, context.getString(R.string.event_pt_meeting), COLOR_PT_MEETING), - EventType(profileId, TYPE_EXCURSION, context.getString(R.string.event_excursion), COLOR_EXCURSION), - EventType(profileId, TYPE_READING, context.getString(R.string.event_reading), COLOR_READING), - EventType(profileId, TYPE_CLASS_EVENT, context.getString(R.string.event_class_event), COLOR_CLASS_EVENT), - EventType(profileId, TYPE_INFORMATION, context.getString(R.string.event_information), COLOR_INFORMATION) - ) + var order = 100 + val colorMap = EventType.getTypeColorMap() + val typeList = EventType.getTypeNameMap().map { (id, name) -> + EventType( + profileId = profileId, + id = id, + name = context.getString(name), + color = colorMap[id] ?: COLOR_DEFAULT, + order = order++, + source = SOURCE_DEFAULT + ) + } addAll(typeList) return typeList } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/TimetableDao.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/TimetableDao.kt index c7cbee12..8ce9ed9b 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/TimetableDao.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/TimetableDao.kt @@ -84,6 +84,8 @@ abstract class TimetableDao : BaseDao { "LIMIT 1") fun getBetweenDates(dateFrom: Date, dateTo: Date) = getRaw("$QUERY WHERE (type != 3 AND date >= '${dateFrom.stringY_m_d}' AND date <= '${dateTo.stringY_m_d}') OR ((type = 3 OR type = 1) AND oldDate >= '${dateFrom.stringY_m_d}' AND oldDate <= '${dateTo.stringY_m_d}') $ORDER_BY") + fun getChanges(profileId: Int) = + getRaw("$QUERY WHERE timetable.profileId = $profileId AND $IS_CHANGED $ORDER_BY") // GET ALL - NOW fun getAllNow(profileId: Int) = diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Event.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Event.kt index 0dcaa83e..3e59cc17 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Event.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Event.kt @@ -45,6 +45,7 @@ open class Event( var addedDate: Long = System.currentTimeMillis() ) : Keepable() { companion object { + const val TYPE_ELEARNING = -5L const val TYPE_UNDEFINED = -2L const val TYPE_HOMEWORK = -1L const val TYPE_DEFAULT = 0L @@ -57,7 +58,7 @@ open class Event( const val TYPE_READING = 7L const val TYPE_CLASS_EVENT = 8L const val TYPE_INFORMATION = 9L - const val TYPE_TEACHER_ABSENCE = 10L + const val COLOR_ELEARNING = 0xfff57f17.toInt() const val COLOR_HOMEWORK = 0xff795548.toInt() const val COLOR_DEFAULT = 0xffffc107.toInt() const val COLOR_EXAM = 0xfff44336.toInt() @@ -69,7 +70,6 @@ open class Event( const val COLOR_READING = 0xFFFFEB3B.toInt() const val COLOR_CLASS_EVENT = 0xff388e3c.toInt() const val COLOR_INFORMATION = 0xff039be5.toInt() - const val COLOR_TEACHER_ABSENCE = 0xff039be5.toInt() } @ColumnInfo(name = "eventAddedManually") @@ -116,14 +116,7 @@ open class Event( var showAsUnseen: Boolean? = null val startTimeCalendar: Calendar - get() = Calendar.getInstance().also { it.set( - date.year, - date.month - 1, - date.day, - time?.hour ?: 0, - time?.minute ?: 0, - time?.second ?: 0 - ) } + get() = date.getAsCalendar(time) val endTimeCalendar: Calendar get() = startTimeCalendar.also { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/EventType.java b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/EventType.java deleted file mode 100644 index bf981e84..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/EventType.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (c) Kacper Ziubryniewicz 2020-1-6 - */ - -package pl.szczodrzynski.edziennik.data.db.entity; - -import android.graphics.Color; - -import androidx.room.ColumnInfo; -import androidx.room.Entity; - -@Entity(tableName = "eventTypes", - primaryKeys = {"profileId", "eventType"}) -public class EventType { - public int profileId; - - @ColumnInfo(name = "eventType") - public long id; - - @ColumnInfo(name = "eventTypeName") - public String name; - @ColumnInfo(name = "eventTypeColor") - public int color; - - public EventType(int profileId, long id, String name, int color) { - this.profileId = profileId; - this.id = id; - this.name = name; - this.color = color; - } - - public EventType(int profileId, int id, String name, String color) { - this(profileId, id, name, Color.parseColor(color)); - } -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/EventType.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/EventType.kt new file mode 100644 index 00000000..7753b38a --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/EventType.kt @@ -0,0 +1,89 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-4-19. + */ +package pl.szczodrzynski.edziennik.data.db.entity + +import androidx.room.ColumnInfo +import androidx.room.Entity +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_CLASS_EVENT +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_DEFAULT +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_ELEARNING +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_ESSAY +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_EXAM +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_EXCURSION +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_HOMEWORK +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_INFORMATION +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_PROJECT +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_PT_MEETING +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_READING +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_SHORT_QUIZ +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_CLASS_EVENT +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_DEFAULT +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_ELEARNING +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_ESSAY +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_EXAM +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_EXCURSION +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_HOMEWORK +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_INFORMATION +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_PROJECT +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_PT_MEETING +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_READING +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_SHORT_QUIZ + +@Entity( + tableName = "eventTypes", + primaryKeys = ["profileId", "eventType"] +) +class EventType( + val profileId: Int, + + @ColumnInfo(name = "eventType") + val id: Long, + + @ColumnInfo(name = "eventTypeName") + val name: String, + @ColumnInfo(name = "eventTypeColor") + val color: Int, + @ColumnInfo(name = "eventTypeOrder") + var order: Int = id.toInt(), + @ColumnInfo(name = "eventTypeSource") + val source: Int = SOURCE_REGISTER +) { + companion object { + const val SOURCE_DEFAULT = 0 + const val SOURCE_REGISTER = 1 + const val SOURCE_CUSTOM = 2 + const val SOURCE_SHARED = 3 + + fun getTypeColorMap() = mapOf( + TYPE_ELEARNING to COLOR_ELEARNING, + TYPE_HOMEWORK to COLOR_HOMEWORK, + TYPE_DEFAULT to COLOR_DEFAULT, + TYPE_EXAM to COLOR_EXAM, + TYPE_SHORT_QUIZ to COLOR_SHORT_QUIZ, + TYPE_ESSAY to COLOR_ESSAY, + TYPE_PROJECT to COLOR_PROJECT, + TYPE_PT_MEETING to COLOR_PT_MEETING, + TYPE_EXCURSION to COLOR_EXCURSION, + TYPE_READING to COLOR_READING, + TYPE_CLASS_EVENT to COLOR_CLASS_EVENT, + TYPE_INFORMATION to COLOR_INFORMATION + ) + + fun getTypeNameMap() = mapOf( + TYPE_ELEARNING to R.string.event_type_elearning, + TYPE_HOMEWORK to R.string.event_type_homework, + TYPE_DEFAULT to R.string.event_other, + TYPE_EXAM to R.string.event_exam, + TYPE_SHORT_QUIZ to R.string.event_short_quiz, + TYPE_ESSAY to R.string.event_essay, + TYPE_PROJECT to R.string.event_project, + TYPE_PT_MEETING to R.string.event_pt_meeting, + TYPE_EXCURSION to R.string.event_excursion, + TYPE_READING to R.string.event_reading, + TYPE_CLASS_EVENT to R.string.event_class_event, + TYPE_INFORMATION to R.string.event_information + ) + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Notification.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Notification.kt index 1e8b8e0e..0a049216 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Notification.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Notification.kt @@ -10,6 +10,8 @@ import android.content.Intent import androidx.room.Entity import androidx.room.PrimaryKey import com.google.gson.JsonObject +import com.mikepenz.iconics.typeface.IIcon +import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial import pl.szczodrzynski.edziennik.MainActivity @Entity(tableName = "notifications") @@ -19,6 +21,7 @@ data class Notification( val title: String, val text: String, + val textLong: String? = null, val type: Int, @@ -96,4 +99,19 @@ data class Notification( fillIntent(intent) return PendingIntent.getActivity(context, id.toInt(), intent, PendingIntent.FLAG_ONE_SHOT) } + + fun getLargeIcon(): IIcon = when (type) { + TYPE_TIMETABLE_LESSON_CHANGE -> CommunityMaterial.Icon3.cmd_timetable + TYPE_NEW_GRADE -> CommunityMaterial.Icon3.cmd_numeric_5_box_outline + TYPE_NEW_EVENT -> CommunityMaterial.Icon.cmd_calendar_outline + TYPE_NEW_HOMEWORK -> CommunityMaterial.Icon3.cmd_notebook_outline + TYPE_NEW_SHARED_EVENT -> CommunityMaterial.Icon.cmd_calendar_outline + TYPE_NEW_SHARED_HOMEWORK -> CommunityMaterial.Icon3.cmd_notebook_outline + TYPE_NEW_MESSAGE -> CommunityMaterial.Icon.cmd_email_outline + TYPE_NEW_NOTICE -> CommunityMaterial.Icon.cmd_emoticon_outline + TYPE_NEW_ATTENDANCE -> CommunityMaterial.Icon.cmd_calendar_remove_outline + TYPE_LUCKY_NUMBER -> CommunityMaterial.Icon.cmd_emoticon_excited_outline + TYPE_NEW_ANNOUNCEMENT -> CommunityMaterial.Icon.cmd_bullhorn_outline + else -> CommunityMaterial.Icon.cmd_bell_ring_outline + } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Profile.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Profile.kt index 9e52dc81..b3586a79 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Profile.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Profile.kt @@ -129,6 +129,9 @@ open class Profile( val isParent get() = accountName != null + val accountOwnerName + get() = accountName ?: studentNameLong + val registerName get() = when (loginStoreType) { LOGIN_TYPE_LIBRUS -> "librus" diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/MessageFull.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/MessageFull.kt index 3fdcdd0d..96103f38 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/MessageFull.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/MessageFull.kt @@ -30,7 +30,7 @@ class MessageFull( @Ignore var filterWeight = 0 @Ignore - var searchHighlightText: String? = null + var searchHighlightText: CharSequence? = null // metadata var seen = false diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration92.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration92.kt new file mode 100644 index 00000000..4b36c0e9 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration92.kt @@ -0,0 +1,60 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-4-15. + */ + +package pl.szczodrzynski.edziennik.data.db.migration + +import android.content.ContentValues +import android.database.sqlite.SQLiteDatabase +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_ELEARNING +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_ELEARNING +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_INFORMATION +import pl.szczodrzynski.edziennik.data.db.entity.EventType.Companion.SOURCE_DEFAULT +import pl.szczodrzynski.edziennik.data.db.entity.EventType.Companion.SOURCE_REGISTER +import pl.szczodrzynski.edziennik.getInt + +class Migration92 : Migration(91, 92) { + override fun migrate(database: SupportSQLiteDatabase) { + // make eventTypeName not nullable + database.execSQL("ALTER TABLE eventTypes RENAME TO _eventTypes;") + database.execSQL("CREATE TABLE eventTypes (" + + "profileId INTEGER NOT NULL, " + + "eventType INTEGER NOT NULL, " + + "eventTypeName TEXT NOT NULL, " + + "eventTypeColor INTEGER NOT NULL, " + + "PRIMARY KEY(profileId,eventType)" + + ");") + database.execSQL("INSERT INTO eventTypes " + + "(profileId, eventType, eventTypeName, eventTypeColor) " + + "SELECT profileId, eventType, eventTypeName, eventTypeColor " + + "FROM _eventTypes;") + database.execSQL("DROP TABLE _eventTypes;") + + // add columns for order and source + database.execSQL("ALTER TABLE eventTypes ADD COLUMN eventTypeOrder INTEGER NOT NULL DEFAULT 0;") + database.execSQL("ALTER TABLE eventTypes ADD COLUMN eventTypeSource INTEGER NOT NULL DEFAULT 0;") + + // migrate existing types to show correct order and source + database.execSQL("UPDATE eventTypes SET eventTypeOrder = eventType + 102;") + database.execSQL("UPDATE eventTypes SET eventTypeSource = $SOURCE_REGISTER WHERE eventType > $TYPE_INFORMATION;") + + // add new e-learning type + val cursor = database.query("SELECT profileId FROM profiles;") + cursor.use { + while (it.moveToNext()) { + val values = ContentValues().apply { + put("profileId", it.getInt("profileId")) + put("eventType", TYPE_ELEARNING) + put("eventTypeName", "lekcja online") + put("eventTypeColor", COLOR_ELEARNING) + put("eventTypeOrder", 100) + put("eventTypeSource", SOURCE_DEFAULT) + } + + database.insert("eventTypes", SQLiteDatabase.CONFLICT_REPLACE, values) + } + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration93.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration93.kt new file mode 100644 index 00000000..9d5df5b3 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration93.kt @@ -0,0 +1,15 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-5-26. + */ + +package pl.szczodrzynski.edziennik.data.db.migration + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +class Migration93 : Migration(92, 93) { + override fun migrate(database: SupportSQLiteDatabase) { + // notifications - long text + database.execSQL("ALTER TABLE notifications ADD COLUMN textLong TEXT DEFAULT NULL;") + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/MessagesConfigDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/MessagesConfigDialog.kt new file mode 100644 index 00000000..e42bf61f --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/MessagesConfigDialog.kt @@ -0,0 +1,66 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-4-14. + */ + +package pl.szczodrzynski.edziennik.ui.dialogs + +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.MainActivity +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.databinding.MessagesConfigDialogBinding + +class MessagesConfigDialog( + private val activity: AppCompatActivity, + private val reloadOnDismiss: Boolean = true, + private val onShowListener: ((tag: String) -> Unit)? = null, + private val onDismissListener: ((tag: String) -> Unit)? = null +) { + companion object { + const val TAG = "MessagesConfigDialog" + } + + private val app by lazy { activity.application as App } + private val config by lazy { app.config.ui } + private val profileConfig by lazy { app.config.forProfile().ui } + + private lateinit var b: MessagesConfigDialogBinding + private lateinit var dialog: AlertDialog + + init { run { + if (activity.isFinishing) + return@run + b = MessagesConfigDialogBinding.inflate(activity.layoutInflater) + onShowListener?.invoke(TAG) + dialog = MaterialAlertDialogBuilder(activity) + .setTitle(R.string.menu_messages_config) + .setView(b.root) + .setPositiveButton(R.string.ok) { dialog, _ -> dialog.dismiss() } + .setOnDismissListener { + saveConfig() + onDismissListener?.invoke(TAG) + if (reloadOnDismiss) (activity as? MainActivity)?.reloadTarget() + } + .create() + loadConfig() + dialog.show() + }} + + private fun loadConfig() { + b.config = profileConfig + + b.greetingText.setText( + profileConfig.messagesGreetingText ?: "\n\nZ poważaniem\n${app.profile.accountOwnerName}" + ) + } + + private fun saveConfig() { + val greetingText = b.greetingText.text?.toString()?.trim() + if (greetingText.isNullOrEmpty()) + profileConfig.messagesGreetingText = null + else + profileConfig.messagesGreetingText = "\n\n$greetingText" + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/agenda/AgendaConfigDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/agenda/AgendaConfigDialog.kt new file mode 100644 index 00000000..150d7d65 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/agenda/AgendaConfigDialog.kt @@ -0,0 +1,93 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-4-10. + */ + +package pl.szczodrzynski.edziennik.ui.dialogs.agenda + +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.db.entity.Profile +import pl.szczodrzynski.edziennik.data.db.entity.Profile.Companion.REGISTRATION_ENABLED +import pl.szczodrzynski.edziennik.databinding.DialogConfigAgendaBinding +import pl.szczodrzynski.edziennik.ui.dialogs.sync.RegistrationConfigDialog +import java.util.* + +class AgendaConfigDialog( + private val activity: AppCompatActivity, + private val reloadOnDismiss: Boolean = true, + private val onShowListener: ((tag: String) -> Unit)? = null, + private val onDismissListener: ((tag: String) -> Unit)? = null +) { + companion object { + const val TAG = "AgendaConfigDialog" + } + + private val app by lazy { activity.application as App } + private val config by lazy { app.config.ui } + private val profileConfig by lazy { app.config.forProfile().ui } + + private lateinit var b: DialogConfigAgendaBinding + private lateinit var dialog: AlertDialog + + init { run { + if (activity.isFinishing) + return@run + b = DialogConfigAgendaBinding.inflate(activity.layoutInflater) + onShowListener?.invoke(TAG) + dialog = MaterialAlertDialogBuilder(activity) + .setTitle(R.string.menu_agenda_config) + .setView(b.root) + .setPositiveButton(R.string.ok) { dialog, _ -> dialog.dismiss() } + .setOnDismissListener { + saveConfig() + onDismissListener?.invoke(TAG) + if (reloadOnDismiss) (activity as? MainActivity)?.reloadTarget() + } + .create() + loadConfig() + dialog.show() + }} + + private fun loadConfig() { + b.config = profileConfig + b.isAgendaMode = profileConfig.agendaViewType == Profile.AGENDA_DEFAULT + + b.eventSharingEnabled.isChecked = app.profile.enableSharedEvents + && app.profile.registration == REGISTRATION_ENABLED + b.eventSharingEnabled.onChange { _, isChecked -> + if (isChecked && app.profile.registration != REGISTRATION_ENABLED) { + b.eventSharingEnabled.isChecked = false + val dialog = RegistrationConfigDialog(activity, app.profile, onChangeListener = { enabled -> + b.eventSharingEnabled.isChecked = enabled + setEventSharingEnabled(enabled) + }, onShowListener, onDismissListener) + dialog.showEnableDialog() + return@onChange + } + setEventSharingEnabled(isChecked) + } + } + + private fun setEventSharingEnabled(enabled: Boolean) { + if (enabled == app.profile.enableSharedEvents) + return + app.profile.enableSharedEvents = enabled + app.profileSave() + MaterialAlertDialogBuilder(activity) + .setTitle(R.string.event_sharing) + .setMessage( + if (enabled) + R.string.settings_register_shared_events_dialog_enabled_text + else + R.string.settings_register_shared_events_dialog_disabled_text + ) + .setPositiveButton(R.string.ok, null) + .show() + } + + private fun saveConfig() { + + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/day/DayDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/day/DayDialog.kt index e5b7768d..74ba0030 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/day/DayDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/day/DayDialog.kt @@ -7,7 +7,7 @@ package pl.szczodrzynski.edziennik.ui.dialogs.day import android.view.View import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity -import androidx.lifecycle.Observer +import androidx.core.view.isVisible import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.dialog.MaterialAlertDialogBuilder import kotlinx.coroutines.* @@ -19,6 +19,10 @@ import pl.szczodrzynski.edziennik.ui.dialogs.event.EventListAdapter import pl.szczodrzynski.edziennik.ui.dialogs.event.EventManualDialog import pl.szczodrzynski.edziennik.ui.dialogs.lessonchange.LessonChangeDialog import pl.szczodrzynski.edziennik.ui.dialogs.teacherabsence.TeacherAbsenceDialog +import pl.szczodrzynski.edziennik.ui.modules.agenda.lessonchanges.LessonChangesEvent +import pl.szczodrzynski.edziennik.ui.modules.agenda.lessonchanges.LessonChangesEventRenderer +import pl.szczodrzynski.edziennik.ui.modules.agenda.teacherabsence.TeacherAbsenceEvent +import pl.szczodrzynski.edziennik.ui.modules.agenda.teacherabsence.TeacherAbsenceEventRenderer import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Time @@ -29,6 +33,7 @@ class DayDialog( val activity: AppCompatActivity, val profileId: Int, val date: Date, + val eventTypeId: Long? = null, val onShowListener: ((tag: String) -> Unit)? = null, val onDismissListener: ((tag: String) -> Unit)? = null ) : CoroutineScope { @@ -109,38 +114,51 @@ class DayDialog( } lessonChanges.ifNotEmpty { - b.lessonChangeContainer.root.visibility = View.VISIBLE - b.lessonChangeContainer.lessonChangeCount.text = it.size.toString() + LessonChangesEventRenderer().render( + b.lessonChanges, LessonChangesEvent( + profileId = profileId, + date = date, + count = it.size, + showBadge = false + ) + ) - b.lessonChangeLayout.onClick { + b.lessonChangesFrame.onClick { LessonChangeDialog( - activity, - profileId, - date, - onShowListener = onShowListener, - onDismissListener = onDismissListener + activity, + profileId, + date, + onShowListener = onShowListener, + onDismissListener = onDismissListener ) } } + b.lessonChangesFrame.isVisible = lessonChanges.isNotEmpty() val teacherAbsences = withContext(Dispatchers.Default) { app.db.teacherAbsenceDao().getAllByDateNow(profileId, date) } teacherAbsences.ifNotEmpty { - b.teacherAbsenceContainer.root.visibility = View.VISIBLE - b.teacherAbsenceContainer.teacherAbsenceCount.text = it.size.toString() + TeacherAbsenceEventRenderer().render( + b.teacherAbsence, TeacherAbsenceEvent( + profileId = profileId, + date = date, + count = it.size + ) + ) - b.teacherAbsenceLayout.onClick { + b.teacherAbsenceFrame.onClick { TeacherAbsenceDialog( - activity, - profileId, - date, - onShowListener = onShowListener, - onDismissListener = onDismissListener + activity, + profileId, + date, + onShowListener = onShowListener, + onDismissListener = onDismissListener ) } } + b.teacherAbsenceFrame.isVisible = teacherAbsences.isNotEmpty() adapter = EventListAdapter( activity, @@ -169,8 +187,12 @@ class DayDialog( } ) - app.db.eventDao().getAllByDate(profileId, date).observe(activity, Observer { events -> - adapter.items = events + app.db.eventDao().getAllByDate(profileId, date).observe(activity) { events -> + adapter.items = if (eventTypeId != null) + events.filter { it.type == eventTypeId } + else + events + if (b.eventsView.adapter == null) { b.eventsView.adapter = adapter b.eventsView.apply { @@ -189,6 +211,6 @@ class DayDialog( b.eventsView.visibility = View.GONE b.eventsNoData.visibility = View.VISIBLE } - }) + } }} } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventDetailsDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventDetailsDialog.kt index b41da04b..c80f0c63 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventDetailsDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventDetailsDialog.kt @@ -32,7 +32,7 @@ import kotlin.coroutines.CoroutineContext class EventDetailsDialog( val activity: AppCompatActivity, - val event: EventFull, + var event: EventFull, val onShowListener: ((tag: String) -> Unit)? = null, val onDismissListener: ((tag: String) -> Unit)? = null ) : CoroutineScope { @@ -46,6 +46,8 @@ class EventDetailsDialog( private var removeEventDialog: AlertDialog? = null private val eventShared = event.sharedBy != null private val eventOwn = event.sharedBy == "self" + private val manager + get() = app.eventManager private val job = Job() override val coroutineContext: CoroutineContext @@ -92,6 +94,10 @@ class EventDetailsDialog( b.eventShared = eventShared b.eventOwn = eventOwn + if (!event.seen) { + manager.markAsSeen(event) + } + val bullet = " • " val colorSecondary = android.R.attr.textColorSecondary.resolveAttr(activity) @@ -100,6 +106,8 @@ class EventDetailsDialog( } catch (_: Exception) {} + manager.setLegendText(b.legend, event) + b.typeColor.background?.setTintColor(event.eventColor) b.details = mutableListOf( @@ -135,6 +143,7 @@ class EventDetailsDialog( launch(Dispatchers.Default) { app.db.eventDao().replace(event) } + update() b.checkDoneButton.isChecked = true } .setNegativeButton(R.string.cancel, null) @@ -145,6 +154,7 @@ class EventDetailsDialog( launch(Dispatchers.Default) { app.db.eventDao().replace(event) } + update() } } b.checkDoneButton.attachToastHint(R.string.hint_mark_as_done) @@ -156,6 +166,14 @@ class EventDetailsDialog( activity, event.profileId, editingEvent = event, + onSaveListener = { + if (it == null) { + dialog.dismiss() + return@EventManualDialog + } + event = it + update() + }, onShowListener = onShowListener, onDismissListener = onDismissListener ) @@ -199,10 +217,14 @@ class EventDetailsDialog( } b.downloadButton.attachToastHint(R.string.hint_download_again) + BetterLink.attach(b.topic, onActionSelected = dialog::dismiss) - b.topic.text = event.topic - BetterLink.attach(b.topic) { - dialog.dismiss() + event.teacherName?.let { name -> + BetterLink.attach( + b.teacherName, + teachers = mapOf(event.teacherId to name), + onActionSelected = dialog::dismiss + ) } if (event.homeworkBody == null && !event.addedManually && event.type == Event.TYPE_HOMEWORK) { @@ -220,10 +242,7 @@ class EventDetailsDialog( b.bodyTitle.isVisible = true b.bodyProgressBar.isVisible = false b.body.isVisible = true - b.body.text = event.homeworkBody - BetterLink.attach(b.body) { - dialog.dismiss() - } + BetterLink.attach(b.body, onActionSelected = dialog::dismiss) } if (event.attachmentIds.isNullOrEmpty() || event.attachmentNames.isNullOrEmpty()) { @@ -322,8 +341,6 @@ class EventDetailsDialog( removeEventDialog?.dismiss() dialog.dismiss() Toast.makeText(activity, R.string.removed, Toast.LENGTH_SHORT).show() - if (activity is MainActivity && activity.navTargetId == MainActivity.DRAWER_ITEM_AGENDA) - activity.reloadTarget() } private fun openInCalendar() { launch { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventListAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventListAdapter.kt index 34cc6a2d..9bb199ea 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventListAdapter.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventListAdapter.kt @@ -33,7 +33,8 @@ class EventListAdapter( ) : RecyclerView.Adapter(), CoroutineScope { private val app = context.applicationContext as App - private val manager = app.eventManager + private val manager + get() = app.eventManager private val job = Job() override val coroutineContext: CoroutineContext @@ -67,7 +68,7 @@ class EventListAdapter( b.simpleMode = simpleMode - b.topic.text = event.topic + manager.setEventTopic(b.topic, event, showType = false) b.topic.maxLines = if (simpleMode) 2 else 3 b.details.text = mutableListOf( @@ -102,8 +103,6 @@ class EventListAdapter( } b.editButton.attachToastHint(R.string.hint_edit_event) - b.isDone.isVisible = event.isDone - if (event.showAsUnseen == null) event.showAsUnseen = !event.seen diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventManualDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventManualDialog.kt index 97e34009..da684520 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventManualDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventManualDialog.kt @@ -4,8 +4,6 @@ package pl.szczodrzynski.edziennik.ui.dialogs.event -import android.graphics.PorterDuff -import android.graphics.PorterDuffColorFilter import android.view.View import android.widget.Toast import androidx.appcompat.app.AlertDialog @@ -20,23 +18,18 @@ import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode import pl.szczodrzynski.edziennik.* -import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_AGENDA import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask import pl.szczodrzynski.edziennik.data.api.events.ApiTaskAllFinishedEvent import pl.szczodrzynski.edziennik.data.api.events.ApiTaskErrorEvent import pl.szczodrzynski.edziennik.data.api.events.ApiTaskFinishedEvent import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi -import pl.szczodrzynski.edziennik.data.db.entity.Event -import pl.szczodrzynski.edziennik.data.db.entity.EventType -import pl.szczodrzynski.edziennik.data.db.entity.Metadata -import pl.szczodrzynski.edziennik.data.db.entity.Profile +import pl.szczodrzynski.edziennik.data.db.entity.* import pl.szczodrzynski.edziennik.data.db.full.EventFull import pl.szczodrzynski.edziennik.data.db.full.LessonFull import pl.szczodrzynski.edziennik.databinding.DialogEventManualV2Binding import pl.szczodrzynski.edziennik.ui.dialogs.sync.RegistrationConfigDialog import pl.szczodrzynski.edziennik.ui.modules.views.TimeDropdown.Companion.DISPLAY_LESSONS import pl.szczodrzynski.edziennik.utils.Anim -import pl.szczodrzynski.edziennik.utils.TextInputDropDown import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Time import kotlin.coroutines.CoroutineContext @@ -49,6 +42,7 @@ class EventManualDialog( val defaultTime: Time? = null, val defaultType: Long? = null, val editingEvent: EventFull? = null, + val onSaveListener: ((event: EventFull?) -> Unit)? = null, val onShowListener: ((tag: String) -> Unit)? = null, val onDismissListener: ((tag: String) -> Unit)? = null ) : CoroutineScope { @@ -323,57 +317,41 @@ class EventManualDialog( selectDefault(defaultLesson?.displayTeacherId) } + with (b.typeDropdown) { + db = app.db + profileId = this@EventManualDialog.profileId + loadItems() + selectDefault(editingEvent?.type) + selectDefault(defaultType) - val deferred = async(Dispatchers.Default) { - // get the event type list - var eventTypes = app.db.eventTypeDao().getAllNow(profileId) - - if (eventTypes.none { it.id in -1L..10L }) { - eventTypes = app.db.eventTypeDao().addDefaultTypes(activity, profileId) + onTypeSelected = { + b.typeColor.background.setTintColor(it.color) + customColor = null } - - b.typeDropdown.clear() - b.typeDropdown += eventTypes.map { TextInputDropDown.Item(it.id, it.name, tag = it) } - } - deferred.await() - - b.typeDropdown.isEnabled = true - - defaultType?.let { - b.typeDropdown.select(it) } - b.typeDropdown.selected?.let { item -> - customColor = (item.tag as EventType).color - } - - // copy IDs from event being edited + // copy data from event being edited editingEvent?.let { b.topic.setText(it.topic) - b.typeDropdown.select(it.type)?.let { item -> - customColor = (item.tag as EventType).color - } - if (it.color != null && it.color != -1) + if (it.color != -1) customColor = it.color } + b.typeColor.background.setTintColor( + customColor + ?: b.typeDropdown.getSelected()?.color + ?: Event.COLOR_DEFAULT + ) + // copy IDs from the LessonFull defaultLesson?.let { b.teamDropdown.select(it.displayTeamId) } - b.typeDropdown.setOnChangeListener { - b.typeColor.background.colorFilter = PorterDuffColorFilter((it.tag as EventType).color, PorterDuff.Mode.SRC_ATOP) - customColor = null - return@setOnChangeListener true - } - - (customColor ?: Event.COLOR_DEFAULT).let { - b.typeColor.background.colorFilter = PorterDuffColorFilter(it, PorterDuff.Mode.SRC_ATOP) - } - b.typeColor.onClick { - val currentColor = (b.typeDropdown.selected?.tag as EventType?)?.color ?: Event.COLOR_DEFAULT + val currentColor = customColor + ?: b.typeDropdown.getSelected()?.color + ?: Event.COLOR_DEFAULT val colorPickerDialog = ColorPickerDialog.newBuilder() .setColor(currentColor) .create() @@ -381,7 +359,7 @@ class EventManualDialog( object : ColorPickerDialogListener { override fun onDialogDismissed(dialogId: Int) {} override fun onColorSelected(dialogId: Int, color: Int) { - b.typeColor.background.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP) + b.typeColor.background.setTintColor(color) customColor = color } }) @@ -416,11 +394,11 @@ class EventManualDialog( private fun saveEvent() { val date = b.dateDropdown.getSelected() as? Date val timeSelected = b.timeDropdown.getSelected() - val teamId = b.teamDropdown.getSelected() as? Long - val type = b.typeDropdown.selected?.id + val team = b.teamDropdown.getSelected() + val type = b.typeDropdown.getSelected() val topic = b.topic.text?.toString() - val subjectId = b.subjectDropdown.getSelected() as? Long - val teacherId = b.teacherDropdown.getSelected() + val subject = b.subjectDropdown.getSelected() as? Subject + val teacher = b.teacherDropdown.getSelected() val share = b.shareSwitch.isChecked @@ -451,7 +429,7 @@ class EventManualDialog( isError = true } - if (share && teamId == null) { + if (share && team == null) { b.teamDropdown.error = app.getString(R.string.dialog_event_manual_team_choose) if (!isError) b.teamDropdown.parent.requestChildFocus(b.teamDropdown, b.teamDropdown) isError = true @@ -487,10 +465,10 @@ class EventManualDialog( time = startTime, topic = topic, color = customColor, - type = type ?: Event.TYPE_DEFAULT, - teacherId = teacherId ?: -1, - subjectId = subjectId ?: -1, - teamId = teamId ?: -1, + type = type?.id ?: Event.TYPE_DEFAULT, + teacherId = teacher?.id ?: -1, + subjectId = subject?.id ?: -1, + teamId = team?.id ?: -1, addedDate = editingEvent?.addedDate ?: System.currentTimeMillis() ).also { it.addedManually = true @@ -498,7 +476,7 @@ class EventManualDialog( val metadataObject = Metadata( profileId, - when (type) { + when (type?.id) { Event.TYPE_HOMEWORK -> Metadata.TYPE_HOMEWORK else -> Metadata.TYPE_EVENT }, @@ -597,10 +575,14 @@ class EventManualDialog( } } + onSaveListener?.invoke(eventObject.withMetadata(metadataObject).also { + it.subjectLongName = (b.subjectDropdown.getSelected() as? Subject)?.longName + it.teacherName = b.teacherDropdown.getSelected()?.fullName + it.teamName = b.teamDropdown.getSelected()?.name + it.typeName = b.typeDropdown.getSelected()?.name + }) dialog.dismiss() Toast.makeText(activity, R.string.saved, Toast.LENGTH_SHORT).show() - if (activity is MainActivity && activity.navTargetId == DRAWER_ITEM_AGENDA) - activity.reloadTarget() } private fun finishRemoving() { editingEvent ?: return @@ -611,9 +593,8 @@ class EventManualDialog( } removeEventDialog?.dismiss() + onSaveListener?.invoke(null) dialog.dismiss() Toast.makeText(activity, R.string.removed, Toast.LENGTH_SHORT).show() - if (activity is MainActivity && activity.navTargetId == DRAWER_ITEM_AGENDA) - activity.reloadTarget() } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/grade/GradeDetailsDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/grade/GradeDetailsDialog.kt index 5b9579ed..a42f9e2f 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/grade/GradeDetailsDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/grade/GradeDetailsDialog.kt @@ -14,6 +14,7 @@ import pl.szczodrzynski.edziennik.databinding.DialogGradeDetailsBinding import pl.szczodrzynski.edziennik.onClick import pl.szczodrzynski.edziennik.setTintColor import pl.szczodrzynski.edziennik.ui.modules.grades.GradesAdapter +import pl.szczodrzynski.edziennik.utils.BetterLink import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration import kotlin.coroutines.CoroutineContext @@ -68,6 +69,14 @@ class GradeDetailsDialog( GradesConfigDialog(activity, reloadOnDismiss = true) } + grade.teacherName?.let { name -> + BetterLink.attach( + b.teacherName, + teachers = mapOf(grade.teacherId to name), + onActionSelected = dialog::dismiss + ) + } + launch { val historyList = withContext(Dispatchers.Default) { app.db.gradeDao().getByParentIdNow(App.profileId, grade.id) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/timetable/LessonDetailsDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/timetable/LessonDetailsDialog.kt index 5896def0..9b43f9be 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/timetable/LessonDetailsDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/timetable/LessonDetailsDialog.kt @@ -25,6 +25,7 @@ import pl.szczodrzynski.edziennik.ui.dialogs.event.EventDetailsDialog import pl.szczodrzynski.edziennik.ui.dialogs.event.EventListAdapter import pl.szczodrzynski.edziennik.ui.dialogs.event.EventManualDialog import pl.szczodrzynski.edziennik.ui.modules.timetable.TimetableFragment +import pl.szczodrzynski.edziennik.utils.BetterLink import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Week @@ -49,7 +50,8 @@ class LessonDetailsDialog( get() = job + Dispatchers.Main private lateinit var adapter: EventListAdapter - private val manager by lazy { app.timetableManager } + private val manager + get() = app.timetableManager init { run { if (activity.isFinishing) @@ -216,5 +218,19 @@ class LessonDetailsDialog( b.eventsNoData.visibility = View.VISIBLE } }) + + lesson.displayTeacherName?.let { name -> + lesson.displayTeacherId ?: return@let + BetterLink.attach( + b.teacherNameView, + teachers = mapOf(lesson.displayTeacherId!! to name), + onActionSelected = dialog::dismiss + ) + BetterLink.attach( + b.oldTeacherNameView, + teachers = mapOf(lesson.displayTeacherId!! to name), + onActionSelected = dialog::dismiss + ) + } } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/AgendaFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/AgendaFragment.kt index 367b00da..8d228dcc 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/AgendaFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/AgendaFragment.kt @@ -12,10 +12,6 @@ import android.widget.Toast import androidx.databinding.ViewDataBinding import androidx.fragment.app.Fragment import com.applandeo.materialcalendarview.EventDay -import com.github.tibolte.agendacalendarview.CalendarPickerController -import com.github.tibolte.agendacalendarview.models.BaseCalendarEvent -import com.github.tibolte.agendacalendarview.models.CalendarEvent -import com.github.tibolte.agendacalendarview.models.IDayItem import com.mikepenz.iconics.IconicsDrawable import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial import com.mikepenz.iconics.utils.colorInt @@ -29,17 +25,9 @@ import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.data.db.entity.Profile import pl.szczodrzynski.edziennik.databinding.FragmentAgendaCalendarBinding import pl.szczodrzynski.edziennik.databinding.FragmentAgendaDefaultBinding +import pl.szczodrzynski.edziennik.ui.dialogs.agenda.AgendaConfigDialog import pl.szczodrzynski.edziennik.ui.dialogs.day.DayDialog import pl.szczodrzynski.edziennik.ui.dialogs.event.EventManualDialog -import pl.szczodrzynski.edziennik.ui.dialogs.lessonchange.LessonChangeDialog -import pl.szczodrzynski.edziennik.ui.dialogs.teacherabsence.TeacherAbsenceDialog -import pl.szczodrzynski.edziennik.ui.modules.agenda.lessonchange.LessonChangeCounter -import pl.szczodrzynski.edziennik.ui.modules.agenda.lessonchange.LessonChangeEvent -import pl.szczodrzynski.edziennik.ui.modules.agenda.lessonchange.LessonChangeEventRenderer -import pl.szczodrzynski.edziennik.ui.modules.agenda.teacherabsence.TeacherAbsenceCounter -import pl.szczodrzynski.edziennik.ui.modules.agenda.teacherabsence.TeacherAbsenceEvent -import pl.szczodrzynski.edziennik.ui.modules.agenda.teacherabsence.TeacherAbsenceEventRenderer -import pl.szczodrzynski.edziennik.utils.Colors import pl.szczodrzynski.edziennik.utils.Themes import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem @@ -59,7 +47,8 @@ class AgendaFragment : Fragment(), CoroutineScope { get() = job + Dispatchers.Main private var type: Int = Profile.AGENDA_DEFAULT - private var actualDate: Date? = null + + private var agendaDefault: AgendaFragmentDefault? = null override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { if (getActivity() == null || context == null) return null @@ -82,38 +71,61 @@ class AgendaFragment : Fragment(), CoroutineScope { .withTitle(R.string.menu_add_event) .withDescription(R.string.menu_add_event_desc) .withIcon(SzkolnyFont.Icon.szf_calendar_plus_outline) - .withOnClickListener(View.OnClickListener { + .withOnClickListener { activity.bottomSheet.close() - EventManualDialog(activity, app.profileId, defaultDate = actualDate) - }), + EventManualDialog( + activity, + app.profileId, + defaultDate = AgendaFragmentDefault.selectedDate + ) + }, + BottomSheetPrimaryItem(true) + .withTitle(R.string.menu_agenda_config) + .withIcon(CommunityMaterial.Icon.cmd_cog_outline) + .withOnClickListener { + activity.bottomSheet.close() + AgendaConfigDialog(activity, true, null, null) + }, BottomSheetPrimaryItem(true) .withTitle(R.string.menu_agenda_change_view) .withIcon(if (type == Profile.AGENDA_DEFAULT) CommunityMaterial.Icon.cmd_calendar_outline else CommunityMaterial.Icon2.cmd_format_list_bulleted_square) - .withOnClickListener(View.OnClickListener { + .withOnClickListener { activity.bottomSheet.close() - type = if (type == Profile.AGENDA_DEFAULT) Profile.AGENDA_CALENDAR else Profile.AGENDA_DEFAULT + type = + if (type == Profile.AGENDA_DEFAULT) Profile.AGENDA_CALENDAR else Profile.AGENDA_DEFAULT app.config.forProfile().ui.agendaViewType = type activity.reloadTarget() - }), + }, BottomSheetSeparatorItem(true), BottomSheetPrimaryItem(true) .withTitle(R.string.menu_mark_as_read) .withIcon(CommunityMaterial.Icon.cmd_eye_check_outline) - .withOnClickListener(View.OnClickListener { launch { - activity.bottomSheet.close() - withContext(Dispatchers.Default) { - App.db.metadataDao().setAllSeen(app.profileId, Metadata.TYPE_EVENT, true) + .withOnClickListener { + launch { + activity.bottomSheet.close() + withContext(Dispatchers.Default) { + App.db.metadataDao() + .setAllSeen(app.profileId, Metadata.TYPE_EVENT, true) + } + Toast.makeText( + activity, + R.string.main_menu_mark_as_read_success, + Toast.LENGTH_SHORT + ).show() } - Toast.makeText(activity, R.string.main_menu_mark_as_read_success, Toast.LENGTH_SHORT).show() - }}) + } ) activity.navView.bottomBar.fabEnable = true activity.navView.bottomBar.fabExtendedText = getString(R.string.add) activity.navView.bottomBar.fabIcon = CommunityMaterial.Icon3.cmd_plus - activity.navView.setFabOnClickListener(View.OnClickListener { - EventManualDialog(activity, app.profileId, defaultDate = actualDate) - }) + activity.navView.setFabOnClickListener { + EventManualDialog( + activity, + app.profileId, + defaultDate = AgendaFragmentDefault.selectedDate + ) + } activity.gainAttention() activity.gainAttentionFAB() @@ -129,143 +141,8 @@ class AgendaFragment : Fragment(), CoroutineScope { return@launch delay(500) - val eventList = mutableListOf() - - val minDate = Calendar.getInstance().apply { - add(Calendar.MONTH, -2) - set(Calendar.DAY_OF_MONTH, 1) - } - val maxDate = Calendar.getInstance().apply { add(Calendar.MONTH, 2) } - - /** - * LESSON CHANGES - */ - if (!isAdded) - return@launch - - val lessons = withContext(Dispatchers.Default) { app.db.timetableDao().getChangesNow(app.profileId) } - val lessonChangeCounters = mutableListOf() - - lessons.forEach { lesson -> - lessonChangeCounters.firstOrNull { it.lessonChangeDate == lesson.displayDate }?.let { - it.lessonChangeCount += 1 - } ?: run { - lessonChangeCounters.add(LessonChangeCounter( - lesson.displayDate ?: return@forEach, - 1 - )) - } - } - - lessonChangeCounters.forEach { counter -> - eventList.add(LessonChangeEvent( - counter.lessonChangeDate.inMillis, - 0xff78909c.toInt(), - Colors.legibleTextColor(0xff78909c.toInt()), - counter.startTime, - counter.endTime, - app.profileId, - counter.lessonChangeDate, - counter.lessonChangeCount - )) - } - - /** - * TEACHER ABSENCES - */ - if (!isAdded) - return@launch - - val showTeacherAbsences = app.profile.getStudentData("showTeacherAbsences", true) - - if (showTeacherAbsences) { - val teacherAbsenceList = withContext(Dispatchers.Default) { app.db.teacherAbsenceDao().getAllNow(app.profileId) } - val teacherAbsenceCounters = mutableListOf() - - teacherAbsenceList.forEach { absence -> - val date = absence.dateFrom.clone() - - while (date <= absence.dateTo) { - teacherAbsenceCounters.firstOrNull { it.teacherAbsenceDate == date }?.let { - it.teacherAbsenceCount += 1 - } ?: run { - teacherAbsenceCounters.add(TeacherAbsenceCounter(date.clone(), 1)) - } - - date.stepForward(0, 0, 1) - } - } - - teacherAbsenceCounters.forEach { counter -> - eventList.add(TeacherAbsenceEvent( - counter.teacherAbsenceDate.inMillis, - 0xffff1744.toInt(), - Colors.legibleTextColor(0xffff1744.toInt()), - counter.startTime, - counter.endTime, - app.profileId, - counter.teacherAbsenceDate, - counter.teacherAbsenceCount - )) - } - } - - /** - * EVENTS - */ - if (!isAdded) - return@launch - - val events = withContext(Dispatchers.Default) { app.db.eventDao().getAllNow(app.profileId) } - val unreadEventDates = mutableSetOf() - - events.forEach { event -> - eventList.add(BaseCalendarEvent( - "${event.typeName ?: "wydarzenie"} - ${event.topic}", - "", - (if (event.time == null) getString(R.string.agenda_event_all_day) else event.time!!.stringHM) + - (event.subjectLongName?.let { ", $it" } ?: "") + - (event.teacherName?.let { ", $it" } ?: "") + - (event.teamName?.let { ", $it" } ?: ""), - event.eventColor, - Colors.legibleTextColor(event.eventColor), - event.startTimeCalendar, - event.endTimeCalendar, - event.time == null, - event.id, - !event.seen - )) - - if (!event.seen) unreadEventDates.add(event.date.value) - } - - b.agendaDefaultView.init(eventList, minDate, maxDate, Locale.getDefault(), object : CalendarPickerController { - override fun onDaySelected(dayItem: IDayItem?) {} - - override fun onScrollToDate(calendar: Calendar) { this@AgendaFragment.launch { - val date = Date.fromCalendar(calendar) - actualDate = date - - // Mark as read scrolled date - if (date.value in unreadEventDates) { - withContext(Dispatchers.Default) { app.db.eventDao().setSeenByDate(app.profileId, date, true) } - unreadEventDates.remove(date.value) - } - }} - - override fun onEventSelected(event: CalendarEvent) { - val date = Date.fromCalendar(event.instanceDay) - - when (event) { - is BaseCalendarEvent -> DayDialog(activity, app.profileId, date) - is LessonChangeEvent -> LessonChangeDialog(activity, app.profileId, date) - is TeacherAbsenceEvent -> TeacherAbsenceDialog(activity, app.profileId, date) - } - } - - }, LessonChangeEventRenderer(), TeacherAbsenceEventRenderer()) - - b.progressBar.visibility = View.GONE + agendaDefault = AgendaFragmentDefault(activity, app, b) + agendaDefault?.initView(this@AgendaFragment) }}} private fun createCalendarAgendaView() { (b as? FragmentAgendaCalendarBinding)?.let { b -> launch { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/AgendaFragmentDefault.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/AgendaFragmentDefault.kt new file mode 100644 index 00000000..9167fb9a --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/AgendaFragmentDefault.kt @@ -0,0 +1,310 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-4-8. + */ + +package pl.szczodrzynski.edziennik.ui.modules.agenda + +import android.util.SparseIntArray +import android.widget.AbsListView +import android.widget.AbsListView.OnScrollListener +import androidx.core.util.forEach +import androidx.core.util.set +import androidx.core.view.isVisible +import com.github.tibolte.agendacalendarview.CalendarManager +import com.github.tibolte.agendacalendarview.CalendarPickerController +import com.github.tibolte.agendacalendarview.agenda.AgendaAdapter +import com.github.tibolte.agendacalendarview.models.BaseCalendarEvent +import com.github.tibolte.agendacalendarview.models.CalendarEvent +import com.github.tibolte.agendacalendarview.models.IDayItem +import kotlinx.coroutines.* +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.MainActivity +import pl.szczodrzynski.edziennik.data.db.full.EventFull +import pl.szczodrzynski.edziennik.databinding.FragmentAgendaDefaultBinding +import pl.szczodrzynski.edziennik.ui.dialogs.day.DayDialog +import pl.szczodrzynski.edziennik.ui.dialogs.event.EventDetailsDialog +import pl.szczodrzynski.edziennik.ui.dialogs.lessonchange.LessonChangeDialog +import pl.szczodrzynski.edziennik.ui.dialogs.teacherabsence.TeacherAbsenceDialog +import pl.szczodrzynski.edziennik.ui.modules.agenda.event.AgendaEvent +import pl.szczodrzynski.edziennik.ui.modules.agenda.event.AgendaEventGroup +import pl.szczodrzynski.edziennik.ui.modules.agenda.event.AgendaEventGroupRenderer +import pl.szczodrzynski.edziennik.ui.modules.agenda.event.AgendaEventRenderer +import pl.szczodrzynski.edziennik.ui.modules.agenda.lessonchanges.LessonChangesEvent +import pl.szczodrzynski.edziennik.ui.modules.agenda.lessonchanges.LessonChangesEventRenderer +import pl.szczodrzynski.edziennik.ui.modules.agenda.teacherabsence.TeacherAbsenceEvent +import pl.szczodrzynski.edziennik.ui.modules.agenda.teacherabsence.TeacherAbsenceEventRenderer +import pl.szczodrzynski.edziennik.utils.models.Date +import java.util.* + +class AgendaFragmentDefault( + private val activity: MainActivity, + private val app: App, + private val b: FragmentAgendaDefaultBinding +) : OnScrollListener, CoroutineScope { + companion object { + var selectedDate: Date = Date.getToday() + } + + override val coroutineContext = Job() + Dispatchers.Main + + private val unreadDates = mutableSetOf() + private val events = mutableListOf() + private var isInitialized = false + private val profileConfig by lazy { app.config.forProfile().ui } + + private val listView + get() = b.agendaDefaultView.agendaView.agendaListView + private val adapter + get() = listView.adapter as? AgendaAdapter + private val manager + get() = CalendarManager.getInstance() + + private var scrollState = OnScrollListener.SCROLL_STATE_IDLE + private var updatePending = false + private var notifyPending = false + override fun onScrollStateChanged(view: AbsListView?, newScrollState: Int) { + b.agendaDefaultView.agendaScrollListener.onScrollStateChanged(view, scrollState) + scrollState = newScrollState + if (updatePending) updateData() + if (notifyPending) notifyDataSetChanged() + } + + override fun onScroll( + view: AbsListView?, + firstVisibleItem: Int, + visibleItemCount: Int, + totalItemCount: Int + ) = b.agendaDefaultView.agendaScrollListener.onScroll( + view, + firstVisibleItem, + visibleItemCount, + totalItemCount + ) + + /** + * Mark the data as needing update, either after 1 second (when + * not scrolling) or 1 second after scrolling stops. + */ + private fun updateData() = launch { + if (scrollState == OnScrollListener.SCROLL_STATE_IDLE) { + updatePending = false + delay(1000) + notifyDataSetChanged() + } else updatePending = true + } + + /** + * Notify the adapter about changes, either instantly or after + * scrolling stops. + */ + private fun notifyDataSetChanged() { + if (scrollState == OnScrollListener.SCROLL_STATE_IDLE) { + notifyPending = false + adapter?.notifyDataSetChanged() + } else notifyPending = true + } + + suspend fun initView(fragment: AgendaFragment) { + isInitialized = false + + withContext(Dispatchers.Default) { + if (profileConfig.agendaLessonChanges) + addLessonChanges(events) + + if (profileConfig.agendaTeacherAbsence) + addTeacherAbsence(events) + } + + app.db.eventDao().getAll(app.profileId).observe(fragment) { + addEvents(events, it) + if (isInitialized) + updateView() + else + initViewPriv() + } + } + + private fun initViewPriv() { + val dateStart = app.profile.dateSemester1Start.asCalendar + val dateEnd = app.profile.dateYearEnd.asCalendar + + val isCompactMode = profileConfig.agendaCompactMode + + b.agendaDefaultView.init( + events, + dateStart, + dateEnd, + Locale.getDefault(), + object : CalendarPickerController { + override fun onDaySelected(dayItem: IDayItem) { + val c = Calendar.getInstance() + c.time = dayItem.date + if (c.timeInMillis == selectedDate.inMillis) { + DayDialog(activity, app.profileId, selectedDate) + } + } + + override fun onEventSelected(event: CalendarEvent) { + val date = Date.fromCalendar(event.instanceDay) + + when (event) { + is AgendaEvent -> EventDetailsDialog(activity, event.event) + is LessonChangesEvent -> LessonChangeDialog(activity, app.profileId, date) + is TeacherAbsenceEvent -> TeacherAbsenceDialog( + activity, + app.profileId, + date + ) + is AgendaEventGroup -> DayDialog(activity, app.profileId, date, eventTypeId = event.typeId) + is BaseCalendarEvent -> if (event.isPlaceHolder) + DayDialog(activity, app.profileId, date) + } + + if (event is BaseEvent && event.showItemBadge) { + val unreadCount = manager.events.count { + it.instanceDay.equals(event.instanceDay) && it.showBadge + } + // only clicked event is unread, remove the day badge + if (unreadCount == 1 && event.showBadge) { + event.dayReference.showBadge = false + unreadDates.remove(date.value) + } + setAsRead(event) + } + } + + override fun onScrollToDate(calendar: Calendar) { + selectedDate = Date.fromCalendar(calendar) + + // Mark as read scrolled date + if (selectedDate.value in unreadDates) { + setAsRead(calendar) + activity.launch(Dispatchers.Default) { + app.db.eventDao().setSeenByDate(app.profileId, selectedDate, true) + } + unreadDates.remove(selectedDate.value) + } + } + }, + AgendaEventRenderer(app.eventManager, isCompactMode), + AgendaEventGroupRenderer(), + LessonChangesEventRenderer(), + TeacherAbsenceEventRenderer() + ) + + listView.setOnScrollListener(this) + + isInitialized = true + b.progressBar.isVisible = false + } + + private fun updateView() { + manager.events.clear() + manager.loadEvents(events, BaseCalendarEvent()) + + adapter?.updateEvents(manager.events) + //listView.scrollToCurrentDate(selectedDate.asCalendar) + } + + private fun setAsRead(date: Calendar) { + // get all events matching the date + val events = manager.events.filter { + if (it.instanceDay.equals(date) && it.showBadge && it is AgendaEvent) { + // hide the day badge for the date + it.dayReference.showBadge = false + return@filter true + } + false + } + // set this date's events as read + setAsRead(*events.toTypedArray()) + } + + private fun setAsRead(vararg event: CalendarEvent) { + // hide per-event badges + for (e in event) { + events.firstOrNull { + it == e + }?.showBadge = false + e.showBadge = false + } + + listView.setOnScrollListener(this) + updateData() + } + + private fun addEvents( + events: MutableList, + eventList: List + ) { + events.removeAll { it is AgendaEvent || it is AgendaEventGroup } + + if (!profileConfig.agendaGroupByType) { + events += eventList.map { + if (!it.seen) + unreadDates.add(it.date.value) + AgendaEvent(it) + } + return + } + + eventList.groupBy { + it.date.value to it.type + }.forEach { (_, list) -> + val event = list.first() + if (list.size == 1) { + if (!event.seen) + unreadDates.add(event.date.value) + events += AgendaEvent(event) + } else { + events.add(0, AgendaEventGroup( + profileId = event.profileId, + date = event.date, + typeId = event.type, + typeName = event.typeName ?: "-", + typeColor = event.typeColor ?: event.eventColor, + count = list.size, + showBadge = list.any { !it.seen } + )) + } + } + } + + private fun addLessonChanges(events: MutableList) { + val lessons = app.db.timetableDao().getChangesNow(app.profileId) + + val grouped = lessons.groupBy { + it.displayDate + } + + events += grouped.mapNotNull { (date, changes) -> + LessonChangesEvent( + app.profileId, + date = date ?: return@mapNotNull null, + count = changes.size, + showBadge = changes.any { !it.seen } + ) + } + } + + private fun addTeacherAbsence(events: MutableList) { + val teacherAbsence = app.db.teacherAbsenceDao().getAllNow(app.profileId) + + val countMap = SparseIntArray() + + for (absence in teacherAbsence) { + while (absence.dateFrom <= absence.dateTo) { + countMap[absence.dateFrom.value] += 1 + absence.dateFrom.stepForward(0, 0, 1) + } + } + + countMap.forEach { dateInt, count -> + events += TeacherAbsenceEvent( + app.profileId, + date = Date.fromValue(dateInt), + count = count + ) + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/BaseEvent.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/BaseEvent.kt new file mode 100644 index 00000000..62569504 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/BaseEvent.kt @@ -0,0 +1,66 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-4-9. + */ + +package pl.szczodrzynski.edziennik.ui.modules.agenda + +import com.github.tibolte.agendacalendarview.models.CalendarEvent +import com.github.tibolte.agendacalendarview.models.IDayItem +import com.github.tibolte.agendacalendarview.models.IWeekItem +import java.util.* + +open class BaseEvent( + private val id: Long, + private val time: Calendar, + private val color: Int, + private var showBadge: Boolean, + var showItemBadge: Boolean = showBadge +) : CalendarEvent { + + override fun copy() = BaseEvent(id, time, color, showBadge) + + private lateinit var date: Calendar + override fun getInstanceDay() = date + override fun setInstanceDay(value: Calendar) { + date = value + } + + private lateinit var dayReference: IDayItem + override fun getDayReference() = dayReference + override fun setDayReference(value: IDayItem) { + dayReference = value + } + + private lateinit var weekReference: IWeekItem + override fun getWeekReference() = weekReference + override fun setWeekReference(value: IWeekItem) { + weekReference = value + } + + override fun getShowBadge() = showBadge + override fun setShowBadge(value: Boolean) { + showBadge = value + showItemBadge = value + } + + override fun getId() = id + override fun getStartTime() = time + override fun getEndTime() = time + override fun getTitle() = "" + override fun getDescription() = "" + override fun getLocation() = "" + override fun getColor() = color + override fun getTextColor() = 0 + override fun isPlaceholder() = false + override fun isAllDay() = false + + override fun setId(value: Long) = Unit + override fun setStartTime(value: Calendar) = Unit + override fun setEndTime(value: Calendar) = Unit + override fun setTitle(value: String) = Unit + override fun setDescription(value: String) = Unit + override fun setLocation(value: String) = Unit + override fun setTextColor(value: Int) = Unit + override fun setPlaceholder(value: Boolean) = Unit + override fun setAllDay(value: Boolean) = Unit +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/event/AgendaEvent.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/event/AgendaEvent.kt new file mode 100644 index 00000000..e6ba6e23 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/event/AgendaEvent.kt @@ -0,0 +1,20 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-4-8. + */ + +package pl.szczodrzynski.edziennik.ui.modules.agenda.event + +import pl.szczodrzynski.edziennik.data.db.full.EventFull +import pl.szczodrzynski.edziennik.ui.modules.agenda.BaseEvent + +class AgendaEvent( + val event: EventFull, + showBadge: Boolean = !event.seen +) : BaseEvent( + id = event.id, + time = event.startTimeCalendar, + color = event.eventColor, + showBadge = showBadge +) { + override fun copy() = AgendaEvent(event, showBadge) +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/event/AgendaEventGroup.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/event/AgendaEventGroup.kt new file mode 100644 index 00000000..3ef4eb47 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/event/AgendaEventGroup.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-4-10. + */ + +package pl.szczodrzynski.edziennik.ui.modules.agenda.event + +import pl.szczodrzynski.edziennik.ui.modules.agenda.BaseEvent +import pl.szczodrzynski.edziennik.utils.models.Date + +class AgendaEventGroup( + val profileId: Int, + val date: Date, + val typeId: Long, + val typeName: String, + val typeColor: Int, + val count: Int, + showBadge: Boolean +) : BaseEvent( + id = date.value.toLong(), + time = date.asCalendar, + color = typeColor, + showBadge = showBadge +) { + override fun copy() = AgendaEventGroup(profileId, date, typeId, typeName, typeColor, count, showBadge) +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/event/AgendaEventGroupRenderer.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/event/AgendaEventGroupRenderer.kt new file mode 100644 index 00000000..2a1f9af8 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/event/AgendaEventGroupRenderer.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-4-10. + */ + +package pl.szczodrzynski.edziennik.ui.modules.agenda.event + +import android.view.View +import androidx.core.view.isVisible +import com.github.tibolte.agendacalendarview.render.EventRenderer +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.databinding.AgendaWrappedGroupBinding +import pl.szczodrzynski.edziennik.resolveAttr +import pl.szczodrzynski.edziennik.setTintColor +import pl.szczodrzynski.edziennik.utils.Colors + +class AgendaEventGroupRenderer : EventRenderer() { + + override fun render(view: View, event: AgendaEventGroup) { + val b = AgendaWrappedGroupBinding.bind(view).item + + b.card.foreground.setTintColor(event.color) + b.card.background.setTintColor(event.color) + b.name.text = event.typeName + b.name.setTextColor(Colors.legibleTextColor(event.color)) + b.count.text = event.count.toString() + b.count.background.setTintColor(android.R.attr.colorBackground.resolveAttr(view.context)) + + b.badge.isVisible = event.showItemBadge + } + + override fun getEventLayout(): Int = R.layout.agenda_wrapped_group +} + diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/event/AgendaEventRenderer.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/event/AgendaEventRenderer.kt new file mode 100644 index 00000000..2e857cd3 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/event/AgendaEventRenderer.kt @@ -0,0 +1,81 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-4-8. + */ + +package pl.szczodrzynski.edziennik.ui.modules.agenda.event + +import android.annotation.SuppressLint +import android.view.View +import android.widget.FrameLayout +import android.widget.TextView +import androidx.core.view.isVisible +import com.github.tibolte.agendacalendarview.render.EventRenderer +import com.mikepenz.iconics.view.IconicsTextView +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.databinding.AgendaWrappedEventBinding +import pl.szczodrzynski.edziennik.databinding.AgendaWrappedEventCompactBinding +import pl.szczodrzynski.edziennik.join +import pl.szczodrzynski.edziennik.resolveAttr +import pl.szczodrzynski.edziennik.setTintColor +import pl.szczodrzynski.edziennik.utils.Colors +import pl.szczodrzynski.edziennik.utils.managers.EventManager + +class AgendaEventRenderer( + val manager: EventManager, + val isCompact: Boolean +) : EventRenderer() { + + @SuppressLint("SetTextI18n") + override fun render(view: View, aEvent: AgendaEvent) { + if (isCompact) { + val b = AgendaWrappedEventCompactBinding.bind(view).item + bindView(aEvent, b.card, b.title, null, b.badgeBackground, b.badge) + } else { + val b = AgendaWrappedEventBinding.bind(view).item + bindView(aEvent, b.card, b.title, b.subtitle, b.badgeBackground, b.badge) + } + } + + private fun bindView( + aEvent: AgendaEvent, + card: FrameLayout, + title: IconicsTextView, + subtitle: TextView?, + badgeBackground: View, + badge: View + ) { + val event = aEvent.event + + val textColor = Colors.legibleTextColor(event.eventColor) + + val timeText = if (event.time == null) + card.context.getString(R.string.agenda_event_all_day) + else + event.time!!.stringHM + + val eventSubtitle = listOfNotNull( + timeText, + event.subjectLongName, + event.teacherName, + event.teamName + ).join(", ") + + card.foreground.setTintColor(event.eventColor) + card.background.setTintColor(event.eventColor) + manager.setEventTopic(title, event, doneIconColor = textColor) + title.setTextColor(textColor) + subtitle?.text = eventSubtitle + subtitle?.setTextColor(textColor) + + badgeBackground.isVisible = aEvent.showItemBadge + badgeBackground.background.setTintColor( + android.R.attr.colorBackground.resolveAttr(card.context) + ) + badge.isVisible = aEvent.showItemBadge + } + + override fun getEventLayout() = if (isCompact) + R.layout.agenda_wrapped_event_compact + else + R.layout.agenda_wrapped_event +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/lessonchange/LessonChangeCounter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/lessonchange/LessonChangeCounter.kt deleted file mode 100644 index 2b264ede..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/lessonchange/LessonChangeCounter.kt +++ /dev/null @@ -1,19 +0,0 @@ -package pl.szczodrzynski.edziennik.ui.modules.agenda.lessonchange - -import pl.szczodrzynski.edziennik.utils.models.Date -import java.util.* - -class LessonChangeCounter( - val lessonChangeDate: Date, - var lessonChangeCount: Int -) { - val startTime: Calendar - get() = Calendar.getInstance().apply { - set(lessonChangeDate.year, lessonChangeDate.month - 1, lessonChangeDate.day, 10, 0, 0) - } - - val endTime: Calendar - get() = Calendar.getInstance().apply { - timeInMillis = startTime.timeInMillis + (45 * 60 * 1000) - } -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/lessonchange/LessonChangeEvent.java b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/lessonchange/LessonChangeEvent.java deleted file mode 100644 index 077c2206..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/lessonchange/LessonChangeEvent.java +++ /dev/null @@ -1,243 +0,0 @@ -package pl.szczodrzynski.edziennik.ui.modules.agenda.lessonchange; - -import com.github.tibolte.agendacalendarview.models.CalendarEvent; -import com.github.tibolte.agendacalendarview.models.IDayItem; -import com.github.tibolte.agendacalendarview.models.IWeekItem; - -import java.util.Calendar; - -import pl.szczodrzynski.edziennik.utils.models.Date; - -public class LessonChangeEvent implements CalendarEvent { - - /** - * Id of the event. - */ - private long mId; - /** - * Color to be displayed in the agenda view. - */ - private int mColor; - /** - * Text color displayed on the background color - */ - private int mTextColor; - /** - * Calendar instance helping sorting the events per section in the agenda view. - */ - private Calendar mInstanceDay; - /** - * Start time of the event. - */ - private Calendar mStartTime; - /** - * End time of the event. - */ - private Calendar mEndTime; - /** - * References to a DayItem instance for that event, used to link interaction between the - * calendar view and the agenda view. - */ - private IDayItem mDayReference; - /** - * References to a WeekItem instance for that event, used to link interaction between the - * calendar view and the agenda view. - */ - private IWeekItem mWeekReference; - - - private int profileId; - private Date lessonChangeDate; - private int lessonChangeCount; - - public LessonChangeEvent(LessonChangeEvent calendarEvent) { - this.mId = calendarEvent.getId(); - this.mColor = calendarEvent.getColor(); - this.mTextColor = calendarEvent.getTextColor(); - this.mStartTime = calendarEvent.getStartTime(); - this.mEndTime = calendarEvent.getEndTime(); - this.profileId = calendarEvent.getProfileId(); - this.lessonChangeDate = calendarEvent.getLessonChangeDate(); - this.lessonChangeCount = calendarEvent.getLessonChangeCount(); - } - - public LessonChangeEvent(long mId, int mColor, int mTextColor, Calendar mStartTime, Calendar mEndTime, int profileId, Date lessonChangeDate, int lessonChangeCount) { - this.mId = mId; - this.mColor = mColor; - this.mTextColor = mTextColor; - this.mStartTime = mStartTime; - this.mEndTime = mEndTime; - this.profileId = profileId; - this.lessonChangeDate = lessonChangeDate; - this.lessonChangeCount = lessonChangeCount; - } - - public int getProfileId() { - return profileId; - } - - public Date getLessonChangeDate() { - return lessonChangeDate; - } - - public int getLessonChangeCount() { - return lessonChangeCount; - } - - public void setProfileId(int profileId) { - this.profileId = profileId; - } - - public void setLessonChangeDate(Date lessonChangeDate) { - this.lessonChangeDate = lessonChangeDate; - } - - public void setLessonChangeCount(int lessonChangeCount) { - this.lessonChangeCount = lessonChangeCount; - } - - @Override - public void setPlaceholder(boolean placeholder) { - - } - - @Override - public boolean isPlaceholder() { - return false; - } - - @Override - public String getLocation() { - return null; - } - - @Override - public void setLocation(String mLocation) { - - } - - @Override - public long getId() { - return mId; - } - - @Override - public void setId(long mId) { - this.mId = mId; - } - - @Override - public boolean getShowBadge() { - return false; - } - - @Override - public void setShowBadge(boolean mShowBadge) { - - } - - @Override - public int getTextColor() { - return mTextColor; - } - - @Override - public void setTextColor(int mTextColor) { - this.mTextColor = mTextColor; - } - - @Override - public String getDescription() { - return null; - } - - @Override - public void setDescription(String mDescription) { - - } - - @Override - public boolean isAllDay() { - return false; - } - - @Override - public void setAllDay(boolean allDay) { - - } - - @Override - public Calendar getStartTime() { - return mStartTime; - } - - @Override - public void setStartTime(Calendar mStartTime) { - this.mStartTime = mStartTime; - } - - @Override - public Calendar getEndTime() { - return mEndTime; - } - - @Override - public void setEndTime(Calendar mEndTime) { - this.mEndTime = mEndTime; - } - - @Override - public String getTitle() { - return null; - } - - @Override - public void setTitle(String mTitle) { - - } - - @Override - public Calendar getInstanceDay() { - return mInstanceDay; - } - - @Override - public void setInstanceDay(Calendar mInstanceDay) { - this.mInstanceDay = mInstanceDay; - this.mInstanceDay.set(Calendar.HOUR, 0); - this.mInstanceDay.set(Calendar.MINUTE, 0); - this.mInstanceDay.set(Calendar.SECOND, 0); - this.mInstanceDay.set(Calendar.MILLISECOND, 0); - this.mInstanceDay.set(Calendar.AM_PM, 0); - } - - @Override - public IDayItem getDayReference() { - return mDayReference; - } - - @Override - public void setDayReference(IDayItem mDayReference) { - this.mDayReference = mDayReference; - } - - @Override - public IWeekItem getWeekReference() { - return mWeekReference; - } - - @Override - public void setWeekReference(IWeekItem mWeekReference) { - this.mWeekReference = mWeekReference; - } - - @Override - public CalendarEvent copy() { - return new LessonChangeEvent(this); - } - - @Override - public int getColor() { - return mColor; - } -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/lessonchange/LessonChangeEventRenderer.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/lessonchange/LessonChangeEventRenderer.kt deleted file mode 100644 index e2306039..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/lessonchange/LessonChangeEventRenderer.kt +++ /dev/null @@ -1,21 +0,0 @@ -package pl.szczodrzynski.edziennik.ui.modules.agenda.lessonchange - -import android.view.View -import android.widget.TextView -import androidx.cardview.widget.CardView -import com.github.tibolte.agendacalendarview.render.EventRenderer -import pl.szczodrzynski.edziennik.R - -class LessonChangeEventRenderer : EventRenderer() { - override fun render(view: View?, event: LessonChangeEvent) { - val card = view?.findViewById(R.id.lesson_change_card) - val changeText = view?.findViewById(R.id.lesson_change_text) - val changeCount = view?.findViewById(R.id.lessonChangeCount) - card?.setCardBackgroundColor(event.color) - changeText?.setTextColor(event.textColor) - changeCount?.setTextColor(event.textColor) - changeCount?.text = event.lessonChangeCount.toString() - } - - override fun getEventLayout(): Int = R.layout.agenda_event_lesson_change -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/lessonchanges/LessonChangesEvent.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/lessonchanges/LessonChangesEvent.kt new file mode 100644 index 00000000..b38d8829 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/lessonchanges/LessonChangesEvent.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-4-8. + */ + +package pl.szczodrzynski.edziennik.ui.modules.agenda.lessonchanges + +import pl.szczodrzynski.edziennik.ui.modules.agenda.BaseEvent +import pl.szczodrzynski.edziennik.utils.models.Date + +class LessonChangesEvent( + val profileId: Int, + val date: Date, + val count: Int, + showBadge: Boolean +) : BaseEvent( + id = date.value.toLong(), + time = date.asCalendar, + color = 0xff78909c.toInt(), + showBadge = false, + showItemBadge = showBadge +) { + override fun copy() = LessonChangesEvent(profileId, date, count, showItemBadge) + + override fun getShowBadge() = false +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/lessonchanges/LessonChangesEventRenderer.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/lessonchanges/LessonChangesEventRenderer.kt new file mode 100644 index 00000000..5f66cc27 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/lessonchanges/LessonChangesEventRenderer.kt @@ -0,0 +1,55 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-4-8. + */ + +package pl.szczodrzynski.edziennik.ui.modules.agenda.lessonchanges + +import android.view.View +import androidx.core.view.isVisible +import com.github.tibolte.agendacalendarview.render.EventRenderer +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.databinding.AgendaCounterItemBinding +import pl.szczodrzynski.edziennik.databinding.AgendaWrappedCounterBinding +import pl.szczodrzynski.edziennik.resolveAttr +import pl.szczodrzynski.edziennik.setTintColor +import pl.szczodrzynski.edziennik.utils.Colors + +class LessonChangesEventRenderer : EventRenderer() { + + override fun render(view: View, event: LessonChangesEvent) { + val b = AgendaWrappedCounterBinding.bind(view).item + val textColor = Colors.legibleTextColor(event.color) + + b.card.foreground.setTintColor(event.color) + b.card.background.setTintColor(event.color) + b.name.setText(R.string.agenda_lesson_changes) + b.name.setTextColor(textColor) + b.count.text = event.count.toString() + b.count.setTextColor(textColor) + + b.badgeBackground.isVisible = event.showItemBadge + b.badgeBackground.background.setTintColor( + android.R.attr.colorBackground.resolveAttr(view.context) + ) + b.badge.isVisible = event.showItemBadge + } + + fun render(b: AgendaCounterItemBinding, event: LessonChangesEvent) { + val textColor = Colors.legibleTextColor(event.color) + + b.card.foreground.setTintColor(event.color) + b.card.background.setTintColor(event.color) + b.name.setText(R.string.agenda_lesson_changes) + b.name.setTextColor(textColor) + b.count.text = event.count.toString() + b.count.setTextColor(textColor) + + b.badgeBackground.isVisible = event.showItemBadge + b.badgeBackground.background.setTintColor( + android.R.attr.colorBackground.resolveAttr(b.root.context) + ) + b.badge.isVisible = event.showItemBadge + } + + override fun getEventLayout(): Int = R.layout.agenda_wrapped_counter +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/teacherabsence/TeacherAbsenceCounter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/teacherabsence/TeacherAbsenceCounter.kt deleted file mode 100644 index 71dea025..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/teacherabsence/TeacherAbsenceCounter.kt +++ /dev/null @@ -1,19 +0,0 @@ -package pl.szczodrzynski.edziennik.ui.modules.agenda.teacherabsence - -import pl.szczodrzynski.edziennik.utils.models.Date -import java.util.* - -class TeacherAbsenceCounter ( - val teacherAbsenceDate: Date, - var teacherAbsenceCount: Int = 0 -) { - val startTime: Calendar - get() = Calendar.getInstance().apply { - set(teacherAbsenceDate.year, teacherAbsenceDate.month - 1, teacherAbsenceDate.day, 10, 0, 0) - } - - val endTime: Calendar - get() = Calendar.getInstance().apply { - timeInMillis = startTime.timeInMillis + (45 * 60 * 1000) - } -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/teacherabsence/TeacherAbsenceEvent.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/teacherabsence/TeacherAbsenceEvent.kt index fc1c28db..b0b01f50 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/teacherabsence/TeacherAbsenceEvent.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/teacherabsence/TeacherAbsenceEvent.kt @@ -1,188 +1,21 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-4-8. + */ + package pl.szczodrzynski.edziennik.ui.modules.agenda.teacherabsence -import com.github.tibolte.agendacalendarview.models.CalendarEvent -import com.github.tibolte.agendacalendarview.models.IDayItem -import com.github.tibolte.agendacalendarview.models.IWeekItem +import pl.szczodrzynski.edziennik.ui.modules.agenda.BaseEvent import pl.szczodrzynski.edziennik.utils.models.Date -import java.util.* -class TeacherAbsenceEvent : CalendarEvent { - /** - * Id of the event. - */ - private var mId: Long = 0 - /** - * Color to be displayed in the agenda view. - */ - private var mColor: Int = 0 - /** - * Text color displayed on the background color - */ - private var mTextColor: Int = 0 - /** - * Calendar instance helping sorting the events per section in the agenda view. - */ - private var mInstanceDay: Calendar? = null - /** - * Start time of the event. - */ - private var mStartTime: Calendar? = null - /** - * End time of the event. - */ - private var mEndTime: Calendar? = null - /** - * References to a DayItem instance for that event, used to link interaction between the - * calendar view and the agenda view. - */ - private var mDayReference: IDayItem? = null - /** - * References to a WeekItem instance for that event, used to link interaction between the - * calendar view and the agenda view. - */ - private var mWeekReference: IWeekItem? = null - - - private var profileId: Int = 0 - var teacherAbsenceDate: Date? = null - var teacherAbsenceCount: Int = 0 - - constructor(calendarEvent: TeacherAbsenceEvent) { - this.mId = calendarEvent.id - this.mColor = calendarEvent.color - this.mTextColor = calendarEvent.textColor - this.mStartTime = calendarEvent.startTime - this.mEndTime = calendarEvent.endTime - this.profileId = calendarEvent.profileId - this.teacherAbsenceDate = calendarEvent.teacherAbsenceDate - this.teacherAbsenceCount = calendarEvent.teacherAbsenceCount - } - - constructor(mId: Long, mColor: Int, mTextColor: Int, mStartTime: Calendar, mEndTime: Calendar, profileId: Int, teacherAbsenceDate: Date, teacherAbsenceCount: Int) { - this.mId = mId - this.mColor = mColor - this.mTextColor = mTextColor - this.mStartTime = mStartTime - this.mEndTime = mEndTime - this.profileId = profileId - this.teacherAbsenceDate = teacherAbsenceDate - this.teacherAbsenceCount = teacherAbsenceCount - } - - override fun setPlaceholder(placeholder: Boolean) { - - } - - override fun isPlaceholder(): Boolean { - return false - } - - override fun getLocation(): String? { - return null - } - - override fun setLocation(mLocation: String) { - - } - - override fun getId(): Long { - return mId - } - - override fun setId(mId: Long) { - this.mId = mId - } - - override fun getShowBadge(): Boolean { - return false - } - - override fun setShowBadge(mShowBadge: Boolean) { - - } - - override fun getTextColor(): Int { - return mTextColor - } - - override fun setTextColor(mTextColor: Int) { - this.mTextColor = mTextColor - } - - override fun getDescription(): String? { - return null - } - - override fun setDescription(mDescription: String) { - - } - - override fun isAllDay(): Boolean { - return false - } - - override fun setAllDay(allDay: Boolean) { - - } - - override fun getStartTime(): Calendar? { - return mStartTime - } - - override fun setStartTime(mStartTime: Calendar) { - this.mStartTime = mStartTime - } - - override fun getEndTime(): Calendar? { - return mEndTime - } - - override fun setEndTime(mEndTime: Calendar) { - this.mEndTime = mEndTime - } - - override fun getTitle(): String? { - return null - } - - override fun setTitle(mTitle: String) { - - } - - override fun getInstanceDay(): Calendar? { - return mInstanceDay - } - - override fun setInstanceDay(mInstanceDay: Calendar) { - this.mInstanceDay = mInstanceDay - this.mInstanceDay!!.set(Calendar.HOUR, 0) - this.mInstanceDay!!.set(Calendar.MINUTE, 0) - this.mInstanceDay!!.set(Calendar.SECOND, 0) - this.mInstanceDay!!.set(Calendar.MILLISECOND, 0) - this.mInstanceDay!!.set(Calendar.AM_PM, 0) - } - - override fun getDayReference(): IDayItem? { - return mDayReference - } - - override fun setDayReference(mDayReference: IDayItem) { - this.mDayReference = mDayReference - } - - override fun getWeekReference(): IWeekItem? { - return mWeekReference - } - - override fun setWeekReference(mWeekReference: IWeekItem) { - this.mWeekReference = mWeekReference - } - - override fun copy(): CalendarEvent { - return TeacherAbsenceEvent(this) - } - - override fun getColor(): Int { - return mColor - } +class TeacherAbsenceEvent( + val profileId: Int, + val date: Date, + val count: Int +) : BaseEvent( + id = date.value.toLong(), + time = date.asCalendar, + color = 0xffff1744.toInt(), + showBadge = false +) { + override fun copy() = TeacherAbsenceEvent(profileId, date, count) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/teacherabsence/TeacherAbsenceEventRenderer.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/teacherabsence/TeacherAbsenceEventRenderer.kt index b437769b..8d70f941 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/teacherabsence/TeacherAbsenceEventRenderer.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/teacherabsence/TeacherAbsenceEventRenderer.kt @@ -1,21 +1,48 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-4-8. + */ + package pl.szczodrzynski.edziennik.ui.modules.agenda.teacherabsence import android.view.View -import android.widget.TextView -import androidx.cardview.widget.CardView +import androidx.core.view.isVisible import com.github.tibolte.agendacalendarview.render.EventRenderer import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.databinding.AgendaCounterItemBinding +import pl.szczodrzynski.edziennik.databinding.AgendaWrappedCounterBinding +import pl.szczodrzynski.edziennik.setTintColor +import pl.szczodrzynski.edziennik.utils.Colors class TeacherAbsenceEventRenderer : EventRenderer() { - override fun render(view: View?, event: TeacherAbsenceEvent) { - val card = view?.findViewById(R.id.teacherAbsenceCard) - val changeText = view?.findViewById(R.id.teacherAbsenceText) - val changeCount = view?.findViewById(R.id.teacherAbsenceCount) - card?.setCardBackgroundColor(event.color) - changeText?.setTextColor(event.textColor) - changeCount?.setTextColor(event.textColor) - changeCount?.text = event.teacherAbsenceCount.toString() + + override fun render(view: View, event: TeacherAbsenceEvent) { + val b = AgendaWrappedCounterBinding.bind(view).item + val textColor = Colors.legibleTextColor(event.color) + + b.card.foreground.setTintColor(event.color) + b.card.background.setTintColor(event.color) + b.name.setText(R.string.agenda_teacher_absence) + b.name.setTextColor(textColor) + b.count.text = event.count.toString() + b.count.setTextColor(textColor) + + b.badgeBackground.isVisible = false + b.badge.isVisible = false } - override fun getEventLayout(): Int = R.layout.agenda_event_teacher_absence + fun render(b: AgendaCounterItemBinding, event: TeacherAbsenceEvent) { + val textColor = Colors.legibleTextColor(event.color) + + b.card.foreground.setTintColor(event.color) + b.card.background.setTintColor(event.color) + b.name.setText(R.string.agenda_teacher_absence) + b.name.setTextColor(textColor) + b.count.text = event.count.toString() + b.count.setTextColor(textColor) + + b.badgeBackground.isVisible = false + b.badge.isVisible = false + } + + override fun getEventLayout(): Int = R.layout.agenda_wrapped_counter } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceDetailsDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceDetailsDialog.kt index 9e6c14d6..5ee6bdc3 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceDetailsDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceDetailsDialog.kt @@ -16,6 +16,7 @@ import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.data.db.full.AttendanceFull import pl.szczodrzynski.edziennik.databinding.AttendanceDetailsDialogBinding import pl.szczodrzynski.edziennik.setTintColor +import pl.szczodrzynski.edziennik.utils.BetterLink import kotlin.coroutines.CoroutineContext class AttendanceDetailsDialog( @@ -60,5 +61,13 @@ class AttendanceDetailsDialog( b.attendanceName.background.setTintColor(attendanceColor) b.attendanceIsCounted.setText(if (attendance.isCounted) R.string.yes else R.string.no) + + attendance.teacherName?.let { name -> + BetterLink.attach( + b.teacherName, + teachers = mapOf(attendance.teacherId to name), + onActionSelected = dialog::dismiss + ) + } }} } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceListFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceListFragment.kt index 8e68cdbb..523dd09c 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceListFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceListFragment.kt @@ -41,7 +41,8 @@ class AttendanceListFragment : LazyFragment(), CoroutineScope { get() = job + Dispatchers.Main // local/private variables go here - private val manager by lazy { app.attendanceManager } + private val manager + get() = app.attendanceManager private var viewType = AttendanceFragment.VIEW_DAYS private var expandSubjectId = 0L diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceSummaryFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceSummaryFragment.kt index 5f61020b..39282325 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceSummaryFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceSummaryFragment.kt @@ -47,7 +47,8 @@ class AttendanceSummaryFragment : LazyFragment(), CoroutineScope { get() = job + Dispatchers.Main // local/private variables go here - private val manager by lazy { app.attendanceManager } + private val manager + get() = app.attendanceManager private var expandSubjectId = 0L private var attendance = listOf() diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/behaviour/NoticesAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/behaviour/NoticesAdapter.kt index a3288da2..61ac01f1 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/behaviour/NoticesAdapter.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/behaviour/NoticesAdapter.kt @@ -21,6 +21,7 @@ import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_MOBIDZIENNIK import pl.szczodrzynski.edziennik.data.db.entity.Notice import pl.szczodrzynski.edziennik.data.db.full.NoticeFull +import pl.szczodrzynski.edziennik.utils.BetterLink import pl.szczodrzynski.edziennik.utils.Utils.bs import pl.szczodrzynski.edziennik.utils.models.Date @@ -83,6 +84,14 @@ class NoticesAdapter//getting the context and product list with constructor } else { holder.noticesItemReason.background = null } + + BetterLink.attach(holder.noticesItemReason) + + notice.teacherName?.let { name -> + BetterLink.attach(holder.noticesItemTeacherName, teachers = mapOf( + notice.teacherId to name + )) + } } override fun getItemCount(): Int { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/GradesAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/GradesAdapter.kt index 46c75d8d..086f073d 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/GradesAdapter.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/GradesAdapter.kt @@ -40,7 +40,8 @@ class GradesAdapter( } private val app = activity.applicationContext as App - private val manager = app.gradesManager + private val manager + get() = app.gradesManager private val job = Job() override val coroutineContext: CoroutineContext diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/GradesListFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/GradesListFragment.kt index 63521e97..63c935ff 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/GradesListFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/GradesListFragment.kt @@ -48,9 +48,12 @@ class GradesListFragment : Fragment(), CoroutineScope { get() = job + Dispatchers.Main // local/private variables go here - private val manager by lazy { app.gradesManager } - private val dontCountEnabled by lazy { manager.dontCountEnabled } - private val dontCountGrades by lazy { manager.dontCountGrades } + private val manager + get() = app.gradesManager + private val dontCountEnabled + get() = manager.dontCountEnabled + private val dontCountGrades + get() = manager.dontCountGrades private var expandSubjectId = 0L override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginEggsFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginEggsFragment.kt index 0c11bb5a..a6b688b2 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginEggsFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginEggsFragment.kt @@ -95,8 +95,10 @@ class LoginEggsFragment : Fragment(), CoroutineScope { anim.interpolator = AccelerateDecelerateInterpolator() anim.duration = 10 anim.fillAfter = true - activity.getRootView().startAnimation(anim) - nav.navigate(R.id.loginPrizeFragment, null, activity.navOptions) + activity.runOnUiThread { + activity.getRootView().startAnimation(anim) + nav.navigate(R.id.loginPrizeFragment, null, activity.navOptions) + } } }, "EggInterface") loadUrl("https://szkolny.eu/game/runner.html") diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessageFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessageFragment.kt index 30f40c30..e147dea6 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessageFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessageFragment.kt @@ -30,10 +30,12 @@ import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_RECEIVED import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_SENT import pl.szczodrzynski.edziennik.data.db.full.MessageFull import pl.szczodrzynski.edziennik.databinding.MessageFragmentBinding +import pl.szczodrzynski.edziennik.ui.dialogs.MessagesConfigDialog import pl.szczodrzynski.edziennik.utils.Anim import pl.szczodrzynski.edziennik.utils.BetterLink import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Time +import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem import pl.szczodrzynski.navlib.colorAttr import kotlin.coroutines.CoroutineContext import kotlin.math.min @@ -64,10 +66,20 @@ class MessageFragment : Fragment(), CoroutineScope { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { if (!isAdded) return + activity.bottomSheet.prependItem( + BottomSheetPrimaryItem(true) + .withTitle(R.string.menu_messages_config) + .withIcon(CommunityMaterial.Icon.cmd_cog_outline) + .withOnClickListener { + activity.bottomSheet.close() + MessagesConfigDialog(activity, false, null, null) + } + ) + b.closeButton.setImageDrawable( IconicsDrawable(activity, CommunityMaterial.Icon3.cmd_window_close).apply { colorAttr(activity, android.R.attr.textColorSecondary) - sizeDp = 16 + sizeDp = 24 } ) b.closeButton.setOnClickListener { activity.navigateUp() } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesAdapter.kt index f8ccb491..aded9f4a 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesAdapter.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesAdapter.kt @@ -1,11 +1,8 @@ package pl.szczodrzynski.edziennik.ui.modules.messages import android.graphics.Typeface -import android.text.Editable -import android.text.TextWatcher import android.view.LayoutInflater import android.view.ViewGroup -import android.widget.Filter import android.widget.Filterable import androidx.appcompat.app.AppCompatActivity import androidx.recyclerview.widget.RecyclerView @@ -13,22 +10,19 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import pl.szczodrzynski.edziennik.App -import pl.szczodrzynski.edziennik.cleanDiacritics -import pl.szczodrzynski.edziennik.data.db.entity.Message import pl.szczodrzynski.edziennik.data.db.entity.Teacher import pl.szczodrzynski.edziennik.data.db.full.MessageFull import pl.szczodrzynski.edziennik.ui.modules.grades.viewholder.BindableViewHolder import pl.szczodrzynski.edziennik.ui.modules.messages.models.MessagesSearch +import pl.szczodrzynski.edziennik.ui.modules.messages.utils.MessagesFilter import pl.szczodrzynski.edziennik.ui.modules.messages.viewholder.MessageViewHolder import pl.szczodrzynski.edziennik.ui.modules.messages.viewholder.SearchViewHolder -import java.util.* import kotlin.coroutines.CoroutineContext -import kotlin.math.min class MessagesAdapter( - val activity: AppCompatActivity, - val teachers: List, - val onItemClick: ((item: MessageFull) -> Unit)? = null + val activity: AppCompatActivity, + val teachers: List, + val onItemClick: ((item: MessageFull) -> Unit)? = null ) : RecyclerView.Adapter(), CoroutineScope, Filterable { companion object { private const val TAG = "MessagesAdapter" @@ -43,41 +37,10 @@ class MessagesAdapter( override val coroutineContext: CoroutineContext get() = job + Dispatchers.Main - var items = mutableListOf() - var allItems = mutableListOf() + var items = listOf() + var allItems = listOf() val typefaceNormal: Typeface by lazy { Typeface.create(Typeface.DEFAULT, Typeface.NORMAL) } val typefaceBold: Typeface by lazy { Typeface.create(Typeface.DEFAULT, Typeface.BOLD) } - private val comparator by lazy { Comparator { o1: Any, o2: Any -> - if (o1 !is MessageFull || o2 !is MessageFull) - return@Comparator 0 - when { - // standard sorting - o1.filterWeight > o2.filterWeight -> return@Comparator 1 - o1.filterWeight < o2.filterWeight -> return@Comparator -1 - else -> when { - // reversed sorting - o1.addedDate > o2.addedDate -> return@Comparator -1 - o1.addedDate < o2.addedDate -> return@Comparator 1 - else -> return@Comparator 0 - } - } - }} - - val textWatcher by lazy { - object : TextWatcher { - override fun afterTextChanged(s: Editable?) { - getFilter().filter(s.toString()) - } - override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { - /*items.getOrNull(0)?.let { - if (it is MessagesSearch) { - it.searchText = s?.toString() ?: "" - } - }*/ - } - } - } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { val inflater = LayoutInflater.from(parent.context) @@ -103,138 +66,16 @@ class MessagesAdapter( return when { - holder is MessageViewHolder && item is MessageFull -> holder.onBind(activity, app, item, position, this) - holder is SearchViewHolder && item is MessagesSearch -> holder.onBind(activity, app, item, position, this) + holder is MessageViewHolder + && item is MessageFull -> holder.onBind(activity, app, item, position, this) + holder is SearchViewHolder + && item is MessagesSearch -> holder.onBind(activity, app, item, position, this) } } + private val messagesFilter by lazy { + MessagesFilter(this) + } override fun getItemCount() = items.size - override fun getFilter() = filter - private var prevCount = -1 - private val filter by lazy { object : Filter() { - override fun performFiltering(prefix: CharSequence?): FilterResults { - val results = FilterResults() - - if (prevCount == -1) - prevCount = allItems.size - - if (prefix.isNullOrEmpty()) { - allItems.forEach { - if (it is MessageFull) - it.searchHighlightText = null - } - results.values = allItems.toList() - results.count = allItems.size - return results - } - - val items = mutableListOf() - val prefixString = prefix.toString() - - allItems.forEach { - if (it !is MessageFull) { - items.add(it) - return@forEach - } - it.filterWeight = 100 - it.searchHighlightText = null - - var weight: Int - if (it.type == Message.TYPE_SENT) { - it.recipients?.forEach { recipient -> - weight = getMatchWeight(recipient.fullName, prefixString) - if (weight != 100) { - if (weight == 3) - weight = 31 - it.filterWeight = min(it.filterWeight, 10 + weight) - } - } - } - else { - weight = getMatchWeight(it.senderName, prefixString) - if (weight != 100) { - if (weight == 3) - weight = 31 - it.filterWeight = min(it.filterWeight, 10 + weight) - } - } - - - weight = getMatchWeight(it.subject, prefixString) - if (weight != 100) { - if (weight == 3) - weight = 22 - it.filterWeight = min(it.filterWeight, 20 + weight) - } - - if (it.filterWeight != 100) { - it.searchHighlightText = prefixString - items.add(it) - } - } - - Collections.sort(items, comparator) - results.values = items - results.count = items.size - return results - } - - override fun publishResults(constraint: CharSequence?, results: FilterResults) { - results.values?.let { items = it as MutableList } - // do not re-bind the search box - val count = results.count - 1 - - // this tries to update every item except the search field - when { - count > prevCount -> { - notifyItemRangeInserted(prevCount + 1, count - prevCount) - notifyItemRangeChanged(1, prevCount) - } - count < prevCount -> { - notifyItemRangeRemoved(prevCount + 1, prevCount - count) - notifyItemRangeChanged(1, count) - } - else -> { - notifyItemRangeChanged(1, count) - } - } - - /*if (prevCount != count) { - items.getOrNull(0)?.let { - if (it is MessagesSearch) { - it.count = count - notifyItemChanged(0) - } - } - }*/ - - prevCount = count - } - }} - - private fun getMatchWeight(name: CharSequence?, prefix: String): Int { - if (name == null) - return 100 - - val nameClean = name.cleanDiacritics() - - // First match against the whole, non-split value - if (nameClean.startsWith(prefix, ignoreCase = true) || name.startsWith(prefix, ignoreCase = true)) { - return 1 - } else { - // check if prefix matches any of the words - val words = nameClean.split(" ").toTypedArray() + name.split(" ").toTypedArray() - for (word in words) { - if (word.startsWith(prefix, ignoreCase = true)) { - return 2 - } - } - } - // finally check if the prefix matches any part of the name - if (nameClean.contains(prefix, ignoreCase = true) || name.contains(prefix, ignoreCase = true)) { - return 3 - } - - return 100 - } + override fun getFilter() = messagesFilter } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesFragment.kt index add36a3c..1849d2f8 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesFragment.kt @@ -12,7 +12,9 @@ import kotlinx.coroutines.Job import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.data.db.entity.Message import pl.szczodrzynski.edziennik.databinding.MessagesFragmentBinding +import pl.szczodrzynski.edziennik.ui.dialogs.MessagesConfigDialog import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.FragmentLazyPagerAdapter +import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem import kotlin.coroutines.CoroutineContext class MessagesFragment : Fragment(), CoroutineScope { @@ -100,9 +102,19 @@ class MessagesFragment : Fragment(), CoroutineScope { fabIcon = CommunityMaterial.Icon3.cmd_pencil_outline } - setFabOnClickListener(View.OnClickListener { + bottomSheet.prependItem( + BottomSheetPrimaryItem(true) + .withTitle(R.string.menu_messages_config) + .withIcon(CommunityMaterial.Icon.cmd_cog_outline) + .withOnClickListener { + activity.bottomSheet.close() + MessagesConfigDialog(activity, false, null, null) + } + ) + + setFabOnClickListener { activity.loadTarget(MainActivity.TARGET_MESSAGES_COMPOSE) - }) + } } activity.gainAttentionFAB() diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesListFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesListFragment.kt index 2e4a30b8..32761515 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesListFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesListFragment.kt @@ -33,6 +33,7 @@ class MessagesListFragment : LazyFragment(), CoroutineScope { private lateinit var app: App private lateinit var activity: MainActivity private lateinit var b: MessagesListFragmentBinding + private var adapter: MessagesAdapter? = null private val job: Job = Job() override val coroutineContext: CoroutineContext @@ -53,21 +54,22 @@ class MessagesListFragment : LazyFragment(), CoroutineScope { val messageType = arguments.getInt("messageType", Message.TYPE_RECEIVED) var topPosition = arguments.getInt("topPosition", NO_POSITION) var bottomPosition = arguments.getInt("bottomPosition", NO_POSITION) + val searchText = arguments.getString("searchText", "") teachers = withContext(Dispatchers.Default) { app.db.teacherDao().getAllNow(App.profileId) } - val adapter = MessagesAdapter(activity, teachers) { + adapter = MessagesAdapter(activity, teachers) { activity.loadTarget(MainActivity.TARGET_MESSAGES_DETAILS, Bundle( "messageId" to it.id )) } - app.db.messageDao().getAllByType(App.profileId, messageType).observe(this@MessagesListFragment, Observer { items -> + app.db.messageDao().getAllByType(App.profileId, messageType).observe(this@MessagesListFragment, Observer { messages -> if (!isAdded) return@Observer - items.forEach { message -> + messages.forEach { message -> message.recipients?.removeAll { it.profileId != message.profileId } message.recipients?.forEach { recipient -> if (recipient.fullName == null) { @@ -77,13 +79,22 @@ class MessagesListFragment : LazyFragment(), CoroutineScope { } // load & configure the adapter - adapter.items = items.toMutableList() - adapter.items.add(0, MessagesSearch().also { - it.count = items.size + val items = messages.toMutableList() + items.add(0, MessagesSearch().also { + it.searchText = searchText }) - adapter.allItems = adapter.items.toMutableList() + + adapter?.items = items + adapter?.allItems = items + if (items.isNotNullNorEmpty() && b.list.adapter == null) { - b.list.adapter = adapter + if (searchText.isNotBlank()) + adapter?.filter?.filter(searchText) { + b.list.adapter = adapter + } + else + b.list.adapter = adapter + b.list.apply { setHasFixedSize(true) layoutManager = LinearLayoutManager(context) @@ -92,7 +103,7 @@ class MessagesListFragment : LazyFragment(), CoroutineScope { addOnScrollListener(onScrollListener) } } - adapter.notifyDataSetChanged() + setSwipeToRefresh(messageType in Message.TYPE_RECEIVED..Message.TYPE_SENT && items.isNullOrEmpty()) (b.list.layoutManager as? LinearLayoutManager)?.let { layoutManager -> @@ -119,10 +130,15 @@ class MessagesListFragment : LazyFragment(), CoroutineScope { override fun onDestroy() { super.onDestroy() - if (!isAdded) return + if (!isAdded) + return + val layoutManager = (b.list.layoutManager as? LinearLayoutManager) + val searchItem = adapter?.items?.firstOrNull { it is MessagesSearch } as? MessagesSearch + onPageDestroy?.invoke(position, Bundle( - "topPosition" to (b.list.layoutManager as? LinearLayoutManager)?.findFirstVisibleItemPosition(), - "bottomPosition" to (b.list.layoutManager as? LinearLayoutManager)?.findLastCompletelyVisibleItemPosition() + "topPosition" to layoutManager?.findFirstVisibleItemPosition(), + "bottomPosition" to layoutManager?.findLastCompletelyVisibleItemPosition(), + "searchText" to searchItem?.searchText?.toString() )) } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/compose/MessagesComposeFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/compose/MessagesComposeFragment.kt index 942876f2..091dabef 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/compose/MessagesComposeFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/compose/MessagesComposeFragment.kt @@ -4,6 +4,7 @@ package pl.szczodrzynski.edziennik.ui.modules.messages.compose +import android.annotation.SuppressLint import android.content.Context import android.graphics.Typeface import android.graphics.drawable.BitmapDrawable @@ -43,6 +44,7 @@ import pl.szczodrzynski.edziennik.data.db.entity.LoginStore import pl.szczodrzynski.edziennik.data.db.entity.Teacher import pl.szczodrzynski.edziennik.data.db.full.MessageFull import pl.szczodrzynski.edziennik.databinding.MessagesComposeFragmentBinding +import pl.szczodrzynski.edziennik.ui.dialogs.MessagesConfigDialog import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesUtils import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesUtils.getProfileImage import pl.szczodrzynski.edziennik.utils.Colors @@ -50,6 +52,7 @@ import pl.szczodrzynski.edziennik.utils.Themes import pl.szczodrzynski.edziennik.utils.Themes.getPrimaryTextColor import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Time +import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem import pl.szczodrzynski.navlib.elevateSurface import kotlin.coroutines.CoroutineContext import kotlin.text.replace @@ -67,6 +70,10 @@ class MessagesComposeFragment : Fragment(), CoroutineScope { override val coroutineContext: CoroutineContext get() = job + Dispatchers.Main + private val profileConfig by lazy { app.config.forProfile().ui } + private val greetingText + get() = profileConfig.messagesGreetingText ?: "\n\nZ poważaniem\n${app.profile.accountOwnerName}" + private var teachers = mutableListOf() override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { @@ -131,6 +138,16 @@ class MessagesComposeFragment : Fragment(), CoroutineScope { } }*/ + activity.bottomSheet.prependItem( + BottomSheetPrimaryItem(true) + .withTitle(R.string.menu_messages_config) + .withIcon(CommunityMaterial.Icon.cmd_cog_outline) + .withOnClickListener { + activity.bottomSheet.close() + MessagesConfigDialog(activity, false, null, null) + } + ) + launch { delay(100) getRecipientList() @@ -290,7 +307,7 @@ class MessagesComposeFragment : Fragment(), CoroutineScope { b.recipients.setIllegalCharacterIdentifier { c -> c.toString().matches("[\\n;:_ ]".toRegex()) } - b.recipients.setOnChipRemoveListener { _ -> + b.recipients.setOnChipRemoveListener { b.recipients.setSelection(b.recipients.text.length) } @@ -318,14 +335,15 @@ class MessagesComposeFragment : Fragment(), CoroutineScope { fabExtendedText = getString(R.string.messages_compose_send) fabIcon = CommunityMaterial.Icon3.cmd_send_outline - setFabOnClickListener(View.OnClickListener { + setFabOnClickListener { sendMessage() - }) + } } activity.gainAttentionFAB() } + @SuppressLint("SetTextI18n") private fun updateRecipientList(list: List) { launch { withContext(Dispatchers.Default) { teachers = list.sortedBy { it.fullName }.toMutableList() @@ -344,10 +362,14 @@ class MessagesComposeFragment : Fragment(), CoroutineScope { val adapter = MessagesComposeSuggestionAdapter(activity, teachers) b.recipients.setAdapter(adapter) + if (profileConfig.messagesGreetingOnCompose) + b.text.setText(greetingText) + handleReplyMessage() + handleMailToIntent() }} - private fun handleReplyMessage() { launch { + private fun handleReplyMessage() = launch { val replyMessage = arguments?.getString("message") if (replyMessage != null) { val chipList = mutableListOf() @@ -369,8 +391,10 @@ class MessagesComposeFragment : Fragment(), CoroutineScope { if (arguments?.getString("type") == "reply") { // add greeting text - span.replace(0, 0, "\n\nZ poważaniem,\n${app.profile.accountName - ?: app.profile.studentNameLong ?: ""}\n\n\n") + if (profileConfig.messagesGreetingOnReply) + span.replace(0, 0, "$greetingText\n\n\n") + else + span.replace(0, 0, "\n\n") teachers.firstOrNull { it.id == msg.senderId }?.let { teacher -> teacher.image = getProfileImage(48, 24, 16, 12, 1, teacher.fullName) @@ -378,7 +402,12 @@ class MessagesComposeFragment : Fragment(), CoroutineScope { } subject = "Re: ${msg.subject}" } else { - span.replace(0, 0, "\n\n") + // add greeting text + if (profileConfig.messagesGreetingOnForward) + span.replace(0, 0, "$greetingText\n\n\n") + else + span.replace(0, 0, "\n\n") + subject = "Fwd: ${msg.subject}" } body = MessagesUtils.htmlToSpannable(activity, msg.body @@ -400,7 +429,23 @@ class MessagesComposeFragment : Fragment(), CoroutineScope { else { b.recipients.requestFocus() } - }} + } + + private fun handleMailToIntent() { + val teacherId = arguments?.getLong("messageRecipientId") + if (teacherId == 0L) + return + + val chipList = mutableListOf() + teachers.firstOrNull { it.id == teacherId }?.let { teacher -> + teacher.image = getProfileImage(48, 24, 16, 12, 1, teacher.fullName) + chipList += ChipInfo(teacher.fullName, teacher) + } + b.recipients.addTextWithChips(chipList) + + val subject = arguments?.getString("messageSubject") + b.subject.setText(subject ?: return) + } private fun sendMessage() { b.recipientsLayout.error = null diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/models/MessagesSearch.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/models/MessagesSearch.kt index ad206c62..71ed0486 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/models/MessagesSearch.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/models/MessagesSearch.kt @@ -5,7 +5,5 @@ package pl.szczodrzynski.edziennik.ui.modules.messages.models class MessagesSearch { - var isFocused = false - var searchText = "" - var count = 0 + var searchText: CharSequence = "" } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/utils/MessagesComparator.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/utils/MessagesComparator.kt new file mode 100644 index 00000000..2279b600 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/utils/MessagesComparator.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-4-14. + */ + +package pl.szczodrzynski.edziennik.ui.modules.messages.utils + +import pl.szczodrzynski.edziennik.data.db.full.MessageFull + +class MessagesComparator : Comparator { + + override fun compare(o1: Any?, o2: Any?): Int { + if (o1 !is MessageFull || o2 !is MessageFull) + return 0 + + return when { + // standard sorting + o1.filterWeight > o2.filterWeight -> 1 + o1.filterWeight < o2.filterWeight -> -1 + else -> when { + // reversed sorting + o1.addedDate > o2.addedDate -> -1 + o1.addedDate < o2.addedDate -> 1 + else -> 0 + } + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/utils/MessagesFilter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/utils/MessagesFilter.kt new file mode 100644 index 00000000..202db386 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/utils/MessagesFilter.kt @@ -0,0 +1,149 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-4-14. + */ + +package pl.szczodrzynski.edziennik.ui.modules.messages.utils + +import android.widget.Filter +import pl.szczodrzynski.edziennik.cleanDiacritics +import pl.szczodrzynski.edziennik.data.db.entity.Message +import pl.szczodrzynski.edziennik.data.db.full.MessageFull +import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesAdapter +import java.util.* +import kotlin.math.min + +class MessagesFilter( + private val adapter: MessagesAdapter +) : Filter() { + companion object { + private const val NO_MATCH = 1000 + } + + private val comparator = MessagesComparator() + private var prevCount = -1 + + private val allItems + get() = adapter.allItems + + private fun getMatchWeight(name: CharSequence?, prefix: CharSequence): Int { + if (name == null) + return NO_MATCH + + val prefixClean = prefix.cleanDiacritics() + val nameClean = name.cleanDiacritics() + + return when { + // First match against the whole, non-split value + nameClean.startsWith(prefixClean, ignoreCase = true) -> 1 + // check if prefix matches any of the words + nameClean.split(" ").any { + it.startsWith(prefixClean, ignoreCase = true) + } -> 2 + // finally check if the prefix matches any part of the name + nameClean.contains(prefixClean, ignoreCase = true) -> 3 + + else -> NO_MATCH + } + } + + override fun performFiltering(prefix: CharSequence?): FilterResults { + val results = FilterResults() + + if (prevCount == -1) + prevCount = allItems.size + + if (prefix.isNullOrBlank()) { + allItems.forEach { + if (it is MessageFull) + it.searchHighlightText = null + } + results.values = allItems.toList() + results.count = allItems.size + return results + } + + val items = mutableListOf() + + allItems.forEach { + if (it !is MessageFull) { + items.add(it) + return@forEach + } + it.filterWeight = NO_MATCH + it.searchHighlightText = null + + var weight: Int + // weights 11..13 and 110 + if (it.type == Message.TYPE_SENT) { + it.recipients?.forEach { recipient -> + weight = getMatchWeight(recipient.fullName, prefix) + if (weight != NO_MATCH) { + if (weight == 3) + weight = 100 + it.filterWeight = min(it.filterWeight, 10 + weight) + } + } + } else { + weight = getMatchWeight(it.senderName, prefix) + if (weight != NO_MATCH) { + if (weight == 3) + weight = 100 + it.filterWeight = min(it.filterWeight, 10 + weight) + } + } + + // weights 21..23 and 120 + weight = getMatchWeight(it.subject, prefix) + if (weight != NO_MATCH) { + if (weight == 3) + weight = 100 + it.filterWeight = min(it.filterWeight, 20 + weight) + } + + // weights 31..33 and 130 + weight = getMatchWeight(it.body, prefix) + if (weight != NO_MATCH) { + if (weight == 3) + weight = 100 + it.filterWeight = min(it.filterWeight, 30 + weight) + } + + if (it.filterWeight != NO_MATCH) { + it.searchHighlightText = prefix + items.add(it) + } + } + + Collections.sort(items, comparator) + results.values = items + results.count = items.size + return results + } + + override fun publishResults(constraint: CharSequence?, results: FilterResults) { + results.values?.let { + adapter.items = it as MutableList + } + // do not re-bind the search box + val count = results.count - 1 + + // this tries to update every item except the search field + with(adapter) { + when { + count > prevCount -> { + notifyItemRangeInserted(prevCount + 1, count - prevCount) + notifyItemRangeChanged(1, prevCount) + } + count < prevCount -> { + notifyItemRangeRemoved(prevCount + 1, prevCount - count) + notifyItemRangeChanged(1, count) + } + else -> { + notifyItemRangeChanged(1, count) + } + } + } + + prevCount = count + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/utils/SearchTextWatcher.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/utils/SearchTextWatcher.kt new file mode 100644 index 00000000..7103ff29 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/utils/SearchTextWatcher.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-4-14. + */ + +package pl.szczodrzynski.edziennik.ui.modules.messages.utils + +import android.text.Editable +import android.text.TextWatcher +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.databinding.MessagesListItemSearchBinding +import pl.szczodrzynski.edziennik.ui.modules.messages.models.MessagesSearch + +class SearchTextWatcher( + private val b: MessagesListItemSearchBinding, + private val filter: MessagesFilter, + private val item: MessagesSearch +) : TextWatcher { + + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit + + override fun afterTextChanged(s: Editable?) { + item.searchText = s ?: "" + filter.filter(s) { count -> + if (s.isNullOrBlank()) + b.searchLayout.helperText = " " + else + b.searchLayout.helperText = + b.root.context.getString(R.string.messages_search_results, count - 1) + } + } + + override fun equals(other: Any?): Boolean { + return other is SearchTextWatcher + } + + override fun hashCode(): Int { + var result = b.hashCode() + result = 31 * result + filter.hashCode() + result = 31 * result + item.hashCode() + return result + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/viewholder/MessageViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/viewholder/MessageViewHolder.kt index 62789a0c..a466d08b 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/viewholder/MessageViewHolder.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/viewholder/MessageViewHolder.kt @@ -22,17 +22,21 @@ import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesUtils import pl.szczodrzynski.edziennik.utils.models.Date class MessageViewHolder( - inflater: LayoutInflater, - parent: ViewGroup, - val b: MessagesListItemBinding = MessagesListItemBinding.inflate(inflater, parent, false) + inflater: LayoutInflater, + parent: ViewGroup, + val b: MessagesListItemBinding = MessagesListItemBinding.inflate(inflater, parent, false) ) : RecyclerView.ViewHolder(b.root), BindableViewHolder { companion object { private const val TAG = "MessageViewHolder" } - override fun onBind(activity: AppCompatActivity, app: App, item: MessageFull, position: Int, adapter: MessagesAdapter) { - val manager = app.gradesManager - + override fun onBind( + activity: AppCompatActivity, + app: App, + item: MessageFull, + position: Int, + adapter: MessagesAdapter + ) { b.messageSubject.text = item.subject b.messageDate.text = Date.fromMillis(item.addedDate).formattedStringShort b.messageAttachmentImage.isVisible = item.hasAttachments @@ -55,15 +59,17 @@ class MessageViewHolder( b.messageProfileBackground.setImageBitmap(messageInfo.profileImage) b.messageSender.text = messageInfo.profileName - item.searchHighlightText?.let { highlight -> + item.searchHighlightText?.toString()?.let { highlight -> val colorHighlight = R.attr.colorControlHighlight.resolveAttr(activity) b.messageSubject.text = b.messageSubject.text.asSpannable( - StyleSpan(Typeface.BOLD), BackgroundColorSpan(colorHighlight), - substring = highlight, ignoreCase = true, ignoreDiacritics = true) + StyleSpan(Typeface.BOLD), BackgroundColorSpan(colorHighlight), + substring = highlight, ignoreCase = true, ignoreDiacritics = true + ) b.messageSender.text = b.messageSender.text.asSpannable( - StyleSpan(Typeface.BOLD), BackgroundColorSpan(colorHighlight), - substring = highlight, ignoreCase = true, ignoreDiacritics = true) + StyleSpan(Typeface.BOLD), BackgroundColorSpan(colorHighlight), + substring = highlight, ignoreCase = true, ignoreDiacritics = true + ) } adapter.onItemClick?.let { listener -> diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/viewholder/SearchViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/viewholder/SearchViewHolder.kt index 5adb1984..a0a7d69c 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/viewholder/SearchViewHolder.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/viewholder/SearchViewHolder.kt @@ -9,38 +9,43 @@ import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity import androidx.recyclerview.widget.RecyclerView import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.databinding.MessagesListItemSearchBinding import pl.szczodrzynski.edziennik.ui.modules.grades.viewholder.BindableViewHolder import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesAdapter import pl.szczodrzynski.edziennik.ui.modules.messages.models.MessagesSearch +import pl.szczodrzynski.edziennik.ui.modules.messages.utils.SearchTextWatcher class SearchViewHolder( - inflater: LayoutInflater, - parent: ViewGroup, - val b: MessagesListItemSearchBinding = MessagesListItemSearchBinding.inflate(inflater, parent, false) + inflater: LayoutInflater, + parent: ViewGroup, + val b: MessagesListItemSearchBinding = MessagesListItemSearchBinding.inflate( + inflater, + parent, + false + ) ) : RecyclerView.ViewHolder(b.root), BindableViewHolder { companion object { private const val TAG = "SearchViewHolder" } - override fun onBind(activity: AppCompatActivity, app: App, item: MessagesSearch, position: Int, adapter: MessagesAdapter) { - b.searchEdit.removeTextChangedListener(adapter.textWatcher) - b.searchEdit.addTextChangedListener(adapter.textWatcher) + override fun onBind( + activity: AppCompatActivity, + app: App, + item: MessagesSearch, + position: Int, + adapter: MessagesAdapter + ) { + val watcher = SearchTextWatcher(b, adapter.filter, item) + b.searchEdit.removeTextChangedListener(watcher) - /*b.searchEdit.setOnKeyboardListener(object : TextInputKeyboardEdit.KeyboardListener { - override fun onStateChanged(keyboardEditText: TextInputKeyboardEdit, showing: Boolean) { - item.isFocused = showing - } - })*/ + if (adapter.items.isEmpty() || adapter.items.size == adapter.allItems.size) + b.searchLayout.helperText = " " + else + b.searchLayout.helperText = + b.root.context.getString(R.string.messages_search_results, adapter.items.size - 1) + b.searchEdit.setText(item.searchText) - /*if (b.searchEdit.text.toString() != item.searchText) { - b.searchEdit.setText(item.searchText) - b.searchEdit.setSelection(item.searchText.length) - }*/ - - //b.searchLayout.helperText = app.getString(R.string.messages_search_results, item.count) - - /*if (item.isFocused && !b.searchEdit.isFocused) - b.searchEdit.requestFocus()*/ + b.searchEdit.addTextChangedListener(watcher) } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/notifications/NotificationsAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/notifications/NotificationsAdapter.kt index 8df33a8c..6c0fe067 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/notifications/NotificationsAdapter.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/notifications/NotificationsAdapter.kt @@ -4,6 +4,8 @@ import android.view.LayoutInflater import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity import androidx.recyclerview.widget.RecyclerView +import com.mikepenz.iconics.IconicsDrawable +import com.mikepenz.iconics.utils.colorRes import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -43,6 +45,10 @@ class NotificationsAdapter( val date = Date.fromMillis(item.addedDate).formattedString val colorSecondary = android.R.attr.textColorSecondary.resolveAttr(activity) + b.notificationIcon.background = IconicsDrawable(app, item.getLargeIcon()).apply { + colorRes = R.color.colorPrimary + } + b.title.text = item.text b.profileDate.text = listOf( item.profileName ?: "", diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/cards/SettingsRegisterCard.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/cards/SettingsRegisterCard.kt index 36239083..8d3de849 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/cards/SettingsRegisterCard.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/cards/SettingsRegisterCard.kt @@ -12,6 +12,8 @@ import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.after import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_LIBRUS import pl.szczodrzynski.edziennik.data.db.entity.Profile.Companion.REGISTRATION_ENABLED +import pl.szczodrzynski.edziennik.ui.dialogs.MessagesConfigDialog +import pl.szczodrzynski.edziennik.ui.dialogs.agenda.AgendaConfigDialog import pl.szczodrzynski.edziennik.ui.dialogs.bell.BellSyncConfigDialog import pl.szczodrzynski.edziennik.ui.dialogs.grade.GradesConfigDialog import pl.szczodrzynski.edziennik.ui.dialogs.settings.AttendanceConfigDialog @@ -58,6 +60,13 @@ class SettingsRegisterCard(util: SettingsUtil) : SettingsCard(util) { } override fun getItems() = listOfNotNull( + util.createActionItem( + text = R.string.menu_agenda_config, + icon = CommunityMaterial.Icon.cmd_calendar_outline + ) { + AgendaConfigDialog(activity, reloadOnDismiss = false) + }, + util.createActionItem( text = R.string.menu_grades_config, icon = CommunityMaterial.Icon3.cmd_numeric_5_box_outline @@ -65,6 +74,13 @@ class SettingsRegisterCard(util: SettingsUtil) : SettingsCard(util) { GradesConfigDialog(activity, reloadOnDismiss = false) }, + util.createActionItem( + text = R.string.menu_messages_config, + icon = CommunityMaterial.Icon.cmd_calendar_outline + ) { + MessagesConfigDialog(activity, reloadOnDismiss = false) + }, + util.createActionItem( text = R.string.menu_attendance_config, icon = CommunityMaterial.Icon.cmd_calendar_remove_outline diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/cards/SettingsThemeCard.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/cards/SettingsThemeCard.kt index 17b108e5..a009279d 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/cards/SettingsThemeCard.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/cards/SettingsThemeCard.kt @@ -12,6 +12,7 @@ import pl.szczodrzynski.edziennik.ui.dialogs.settings.MiniMenuConfigDialog import pl.szczodrzynski.edziennik.ui.dialogs.settings.ThemeChooserDialog import pl.szczodrzynski.edziennik.ui.modules.settings.SettingsCard import pl.szczodrzynski.edziennik.ui.modules.settings.SettingsUtil +import pl.szczodrzynski.edziennik.utils.BigNightUtil import pl.szczodrzynski.edziennik.utils.Themes import pl.szczodrzynski.edziennik.utils.models.Date @@ -36,6 +37,18 @@ class SettingsThemeCard(util: SettingsUtil) : SettingsCard(util) { } else null, + if (BigNightUtil().isDataWielkanocyNearDzisiaj()) // cool klasa for utility to dzień wielkanocy + util.createPropertyItem( + text = R.string.settings_theme_eggfall_text, + subText = R.string.settings_theme_eggfall_subtext, + icon = CommunityMaterial.Icon.cmd_egg_easter, + value = configGlobal.ui.eggfall + ) { _, it -> + configGlobal.ui.eggfall = it + activity.recreate() + } + else null, + util.createActionItem( text = R.string.settings_theme_theme_text, subText = Themes.getThemeNameRes(), diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/TimetableDayFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/TimetableDayFragment.kt index e6464d7d..3fc9ce83 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/TimetableDayFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/TimetableDayFragment.kt @@ -5,16 +5,16 @@ package pl.szczodrzynski.edziennik.ui.modules.timetable import android.os.Bundle -import android.view.Gravity import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.FrameLayout -import android.widget.ProgressBar import android.widget.TextView import androidx.asynclayoutinflater.view.AsyncLayoutInflater +import androidx.core.view.isVisible +import androidx.core.view.marginTop import androidx.core.view.setPadding -import androidx.lifecycle.Observer +import androidx.core.view.updateLayoutParams import com.linkedin.android.tachyon.DayView import com.linkedin.android.tachyon.DayViewConfig import kotlinx.coroutines.* @@ -24,14 +24,15 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask import pl.szczodrzynski.edziennik.data.db.entity.Lesson import pl.szczodrzynski.edziennik.data.db.full.EventFull import pl.szczodrzynski.edziennik.data.db.full.LessonFull +import pl.szczodrzynski.edziennik.databinding.TimetableDayFragmentBinding import pl.szczodrzynski.edziennik.databinding.TimetableLessonBinding import pl.szczodrzynski.edziennik.databinding.TimetableNoTimetableBinding import pl.szczodrzynski.edziennik.ui.dialogs.timetable.LessonDetailsDialog import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyFragment import pl.szczodrzynski.edziennik.ui.modules.timetable.TimetableFragment.Companion.DEFAULT_END_HOUR import pl.szczodrzynski.edziennik.ui.modules.timetable.TimetableFragment.Companion.DEFAULT_START_HOUR -import pl.szczodrzynski.edziennik.utils.ListenerScrollView import pl.szczodrzynski.edziennik.utils.models.Date +import pl.szczodrzynski.edziennik.utils.models.Time import java.util.* import kotlin.coroutines.CoroutineContext import kotlin.math.min @@ -44,75 +45,66 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope { private lateinit var app: App private lateinit var activity: MainActivity private lateinit var inflater: AsyncLayoutInflater + private lateinit var b: TimetableDayFragmentBinding private val job: Job = Job() override val coroutineContext: CoroutineContext get() = job + Dispatchers.Main + private var timeIndicatorJob: Job? = null + private lateinit var date: Date private var startHour = DEFAULT_START_HOUR private var endHour = DEFAULT_END_HOUR private var firstEventMinute = 24 * 60 + private var paddingTop = 0 - private val manager by lazy { app.timetableManager } + private val manager + get() = app.timetableManager // find SwipeRefreshLayout in the hierarchy private val refreshLayout by lazy { view?.findParentById(R.id.refreshLayout) } - // the day ScrollView - private val dayScrollDelegate = lazy { - val dayScroll = ListenerScrollView(context!!) - dayScroll.layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) - dayScroll.setOnRefreshLayoutEnabledListener { enabled -> - refreshLayout?.isEnabled = enabled - } - dayScroll - } - private val dayScroll by dayScrollDelegate - // the lesson DayView + private val dayView by lazy { - val dayView = DayView(context!!, DayViewConfig( - startHour = startHour, - endHour = endHour, - dividerHeight = 1.dp, - halfHourHeight = 60.dp, - hourDividerColor = R.attr.hourDividerColor.resolveAttr(context), - halfHourDividerColor = R.attr.halfHourDividerColor.resolveAttr(context), - hourLabelWidth = 40.dp, - hourLabelMarginEnd = 10.dp, - eventMargin = 2.dp + val dayView = DayView(activity, DayViewConfig( + startHour = startHour, + endHour = endHour, + dividerHeight = 1.dp, + halfHourHeight = 60.dp, + hourDividerColor = R.attr.hourDividerColor.resolveAttr(context), + halfHourDividerColor = R.attr.halfHourDividerColor.resolveAttr(context), + hourLabelWidth = 40.dp, + hourLabelMarginEnd = 10.dp, + eventMargin = 2.dp ), true) dayView.setPadding(10.dp) - dayScroll.layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) - dayScroll.addView(dayView) - dayView + return@lazy dayView } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { activity = (getActivity() as MainActivity?) ?: return null context ?: return null app = activity.application as App - this.inflater = AsyncLayoutInflater(context!!) + this.inflater = AsyncLayoutInflater(requireContext()) + date = arguments?.getInt("date")?.let { Date.fromValue(it) } ?: Date.getToday() startHour = arguments?.getInt("startHour") ?: DEFAULT_START_HOUR endHour = arguments?.getInt("endHour") ?: DEFAULT_END_HOUR - return FrameLayout(activity).apply { - layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) - addView(ProgressBar(activity).apply { - layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER) - }) - } + + b = TimetableDayFragmentBinding.inflate(inflater, null, false) + return b.root } override fun onPageCreated(): Boolean { // observe lesson database - app.db.timetableDao().getAllForDate(App.profileId, date).observe(this, Observer { lessons -> + app.db.timetableDao().getAllForDate(App.profileId, date).observe(this) { lessons -> launch { val events = withContext(Dispatchers.Default) { app.db.eventDao().getAllByDateNow(App.profileId, date) } processLessonList(lessons, events) } - }) + } return true } @@ -120,9 +112,10 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope { private fun processLessonList(lessons: List, events: List) { // no lessons - timetable not downloaded yet if (lessons.isEmpty()) { - inflater.inflate(R.layout.timetable_no_timetable, view as FrameLayout?) { view, _, parent -> - parent?.removeAllViews() - parent?.addView(view) + inflater.inflate(R.layout.timetable_no_timetable, b.root) { view, _, _ -> + b.root.removeAllViews() + b.root.addView(view) + val b = TimetableNoTimetableBinding.bind(view) val weekStart = date.weekStart.stringY_m_d b.noTimetableSync.onClick { @@ -143,9 +136,9 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope { } // one lesson indicating a day without lessons if (lessons.size == 1 && lessons[0].type == Lesson.TYPE_NO_LESSONS) { - inflater.inflate(R.layout.timetable_no_lessons, view as FrameLayout?) { view, _, parent -> - parent?.removeAllViews() - parent?.addView(view) + inflater.inflate(R.layout.timetable_no_lessons, b.root) { view, _, _ -> + b.root.removeAllViews() + b.root.addView(view) } return } @@ -157,12 +150,12 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope { return } - // clear the root view and add the ScrollView - (view as FrameLayout?)?.removeAllViews() - (view as FrameLayout?)?.addView(dayScroll) + b.scrollView.isVisible = true + b.dayFrame.removeView(b.dayView) + b.dayFrame.addView(dayView, 0) // Inflate a label view for each hour the day view will display - val hourLabelViews = ArrayList() + val hourLabelViews = mutableListOf() for (i in dayView.startHour..dayView.endHour) { if (!isAdded) continue @@ -171,6 +164,11 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope { hourLabelViews.add(hourLabelView) } dayView.setHourLabelViews(hourLabelViews) + // measure dayView top padding needed for the timeIndicator + hourLabelViews.getOrNull(0)?.let { + it.measure(0, 0) + paddingTop = it.measuredHeight / 2 + dayView.paddingTop + } lessons.forEach { it.showAsUnseen = !it.seen } @@ -201,8 +199,12 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope { // Try to recycle an existing event view if there are enough left, otherwise inflate // a new one - val eventView = (if (remaining > 0) recycled?.get(--remaining) else layoutInflater.inflate(R.layout.timetable_lesson, dayView, false)) - ?: continue + val eventView = + (if (remaining > 0) recycled?.get(--remaining) else layoutInflater.inflate( + R.layout.timetable_lesson, + dayView, + false + )) ?: continue val lb = TimetableLessonBinding.bind(eventView) eventViews += eventView @@ -290,16 +292,50 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope { eventTimeRanges.add(DayView.EventTimeRange(startMinute, endMinute)) } + updateTimeIndicator() + dayView.setEventViews(eventViews, eventTimeRanges) val firstEventTop = (firstEventMinute - dayView.startHour * 60) * dayView.minuteHeight - dayScroll.scrollTo(0, firstEventTop.toInt()) + b.scrollView.scrollTo(0, firstEventTop.toInt()) + + b.progressBar.isVisible = false + } + + private fun updateTimeIndicator() { + val time = Time.getNow() + val isTimeInView = + date == Date.getToday() && time.hour in dayView.startHour..dayView.endHour + + b.timeIndicator.isVisible = isTimeInView + b.timeIndicatorMarker.isVisible = isTimeInView + if (isTimeInView) { + val startTime = Time(dayView.startHour, 0, 0) + val seconds = time.inSeconds - startTime.inSeconds * 1f + b.timeIndicator.updateLayoutParams { + topMargin = (seconds * dayView.minuteHeight / 60f).toInt() + paddingTop + } + b.timeIndicatorMarker.updateLayoutParams { + topMargin = b.timeIndicator.marginTop - (16.dp / 2) + (1.dp / 2) + } + } + + if (timeIndicatorJob == null) { + timeIndicatorJob = startCoroutineTimer(repeatMillis = 30000) { + updateTimeIndicator() + } + } } override fun onResume() { super.onResume() - if (dayScrollDelegate.isInitialized()) { - val firstEventTop = (firstEventMinute - dayView.startHour * 60) * dayView.minuteHeight - dayScroll.scrollTo(0, firstEventTop.toInt()) - } + val firstEventTop = (firstEventMinute - dayView.startHour * 60) * dayView.minuteHeight + b.scrollView.scrollTo(0, firstEventTop.toInt()) + updateTimeIndicator() + } + + override fun onPause() { + super.onPause() + timeIndicatorJob?.cancel() + timeIndicatorJob = null } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/DateDropdown.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/DateDropdown.kt index 92bf0515..6c96c352 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/DateDropdown.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/DateDropdown.kt @@ -175,7 +175,7 @@ class DateDropdown : TextInputDropDown { } } - fun pickerDialog() { + private fun pickerDialog() { val date = getSelected() as? Date ?: Date.getToday() MaterialDatePicker.Builder.datePicker() diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/EventTypeDropdown.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/EventTypeDropdown.kt new file mode 100644 index 00000000..878264c6 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/EventTypeDropdown.kt @@ -0,0 +1,95 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-4-14. + */ + +package pl.szczodrzynski.edziennik.ui.modules.views + +import android.content.Context +import android.util.AttributeSet +import com.mikepenz.iconics.IconicsDrawable +import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial +import com.mikepenz.iconics.utils.colorInt +import com.mikepenz.iconics.utils.sizeDp +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import pl.szczodrzynski.edziennik.data.db.AppDb +import pl.szczodrzynski.edziennik.data.db.entity.EventType +import pl.szczodrzynski.edziennik.utils.TextInputDropDown + +class EventTypeDropdown : TextInputDropDown { + constructor(context: Context) : super(context) + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) + constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) + + lateinit var db: AppDb + var profileId: Int = 0 + var onTypeSelected: ((eventType: EventType) -> Unit)? = null + + override fun create(context: Context) { + super.create(context) + isEnabled = false + } + + suspend fun loadItems() { + val types = withContext(Dispatchers.Default) { + val list = mutableListOf() + + var types = db.eventTypeDao().getAllNow(profileId) + + if (types.none { it.id in -1L..10L }) { + types = db.eventTypeDao().addDefaultTypes(context, profileId) + } + + list += types.map { + Item(it.id, it.name, tag = it, icon = IconicsDrawable(context).apply { + icon = CommunityMaterial.Icon.cmd_circle + sizeDp = 24 + colorInt = it.color + }) + } + + list + } + + clear().append(types) + isEnabled = true + + setOnChangeListener { + when (it.tag) { + is EventType -> { + // selected an event type + onTypeSelected?.invoke(it.tag) + true + } + else -> false + } + } + } + + /** + * Select an event type by the [typeId]. + */ + fun selectType(typeId: Long) = select(typeId) + + /** + * Select an event type by the [typeId] **if it's not selected yet**. + */ + fun selectDefault(typeId: Long?) { + if (typeId == null || selected != null) + return + selectType(typeId) + } + + /** + * Get the currently selected event type. + * ### Returns: + * - null if no valid type is selected + * - [EventType] - the selected event type + */ + fun getSelected(): EventType? { + return when (selected?.tag) { + is EventType -> selected?.tag as EventType + else -> null + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/SubjectDropdown.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/SubjectDropdown.kt index 06e01225..4285ec7d 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/SubjectDropdown.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/SubjectDropdown.kt @@ -15,6 +15,7 @@ import kotlinx.coroutines.withContext import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.crc16 import pl.szczodrzynski.edziennik.data.db.AppDb +import pl.szczodrzynski.edziennik.data.db.entity.Subject import pl.szczodrzynski.edziennik.ui.dialogs.input import pl.szczodrzynski.edziennik.utils.TextInputDropDown @@ -40,7 +41,7 @@ class SubjectDropdown : TextInputDropDown { var showNoSubject = true var showCustomSubject = false var customSubjectName = "" - var onSubjectSelected: ((subjectId: Long?) -> Unit)? = null + var onSubjectSelected: ((subject: Subject?) -> Unit)? = null var onCustomSubjectSelected: ((subjectName: String) -> Unit)? = null override fun create(context: Context) { @@ -73,7 +74,7 @@ class SubjectDropdown : TextInputDropDown { list += subjects.map { Item( it.id, it.longName, - tag = it.id + tag = it ) } list @@ -91,10 +92,11 @@ class SubjectDropdown : TextInputDropDown { } -1L -> { // no subject + deselect() onSubjectSelected?.invoke(null) - true + false } - is Long -> { + is Subject -> { // selected a subject onSubjectSelected?.invoke(it.tag) true @@ -104,7 +106,7 @@ class SubjectDropdown : TextInputDropDown { } } - fun customNameDialog() { + private fun customNameDialog() { activity ?: return MaterialAlertDialogBuilder(activity!!) .setTitle("Własny przedmiot") @@ -127,32 +129,37 @@ class SubjectDropdown : TextInputDropDown { .show() } - fun selectSubject(subjectId: Long) { - if (select(subjectId) == null) - select(Item( - subjectId, - "nieznany przedmiot ($subjectId)", - tag = subjectId - )) + /** + * Select a subject by the [subjectId]. + */ + fun selectSubject(subjectId: Long): Item? { + if (subjectId == -1L) { + deselect() + return null + } + return select(subjectId) } - fun selectDefault(subjectId: Long?) { + /** + * Select a subject by the [subjectId] **if it's not selected yet**. + */ + fun selectDefault(subjectId: Long?): Item? { if (subjectId == null || selected != null) - return - selectSubject(subjectId) + return null + return selectSubject(subjectId) } /** * Get the currently selected subject. * ### Returns: * - null if no valid subject is selected - * - [Long] - the selected subject's ID + * - [Subject] - the selected subject * - [String] - a custom subject name entered, if [showCustomSubject] == true */ fun getSelected(): Any? { return when (selected?.tag) { -1L -> null - is Long -> selected?.tag as Long + is Subject -> selected?.tag as Subject is String -> selected?.tag as String else -> null } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/TeacherDropdown.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/TeacherDropdown.kt index 6411426b..23053359 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/TeacherDropdown.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/TeacherDropdown.kt @@ -5,13 +5,12 @@ package pl.szczodrzynski.edziennik.ui.modules.views import android.content.Context -import android.content.ContextWrapper import android.util.AttributeSet -import androidx.appcompat.app.AppCompatActivity import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.data.db.AppDb +import pl.szczodrzynski.edziennik.data.db.entity.Teacher import pl.szczodrzynski.edziennik.utils.TextInputDropDown class TeacherDropdown : TextInputDropDown { @@ -19,22 +18,10 @@ class TeacherDropdown : TextInputDropDown { constructor(context: Context, attrs: AttributeSet) : super(context, attrs) constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) - private val activity: AppCompatActivity? - get() { - var context: Context? = context ?: return null - if (context is AppCompatActivity) return context - while (context is ContextWrapper) { - if (context is AppCompatActivity) - return context - context = context.baseContext - } - return null - } - lateinit var db: AppDb var profileId: Int = 0 var showNoTeacher = true - var onTeacherSelected: ((teacherId: Long?) -> Unit)? = null + var onTeacherSelected: ((teacher: Teacher?) -> Unit)? = null override fun create(context: Context) { super.create(context) @@ -58,7 +45,7 @@ class TeacherDropdown : TextInputDropDown { list += teachers.map { Item( it.id, it.fullName, - tag = it.id + tag = it ) } list @@ -71,10 +58,11 @@ class TeacherDropdown : TextInputDropDown { when (it.tag) { -1L -> { // no teacher + deselect() onTeacherSelected?.invoke(null) - true + false } - is Long -> { + is Teacher -> { // selected a teacher onTeacherSelected?.invoke(it.tag) true @@ -84,31 +72,36 @@ class TeacherDropdown : TextInputDropDown { } } - fun selectTeacher(teacherId: Long) { - if (select(teacherId) == null) - select(Item( - teacherId, - "nieznany nauczyciel ($teacherId)", - tag = teacherId - )) + /** + * Select a teacher by the [teacherId]. + */ + fun selectTeacher(teacherId: Long): Item? { + if (teacherId == -1L) { + deselect() + return null + } + return select(teacherId) } - fun selectDefault(teacherId: Long?) { + /** + * Select a teacher by the [teacherId] **if it's not selected yet**. + */ + fun selectDefault(teacherId: Long?): Item? { if (teacherId == null || selected != null) - return - selectTeacher(teacherId) + return null + return selectTeacher(teacherId) } /** * Get the currently selected teacher. * ### Returns: * - null if no valid teacher is selected - * - [Long] - the selected teacher's ID + * - [Teacher] - the selected teacher */ - fun getSelected(): Long? { + fun getSelected(): Teacher? { return when (selected?.tag) { -1L -> null - is Long -> selected?.tag as Long + is Teacher -> selected?.tag as Teacher else -> null } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/TeamDropdown.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/TeamDropdown.kt index b31d2ca8..b5eee0ac 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/TeamDropdown.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/TeamDropdown.kt @@ -5,9 +5,7 @@ package pl.szczodrzynski.edziennik.ui.modules.views import android.content.Context -import android.content.ContextWrapper import android.util.AttributeSet -import androidx.appcompat.app.AppCompatActivity import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import pl.szczodrzynski.edziennik.R @@ -20,22 +18,10 @@ class TeamDropdown : TextInputDropDown { constructor(context: Context, attrs: AttributeSet) : super(context, attrs) constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) - private val activity: AppCompatActivity? - get() { - var context: Context? = context ?: return null - if (context is AppCompatActivity) return context - while (context is ContextWrapper) { - if (context is AppCompatActivity) - return context - context = context.baseContext - } - return null - } - lateinit var db: AppDb var profileId: Int = 0 var showNoTeam = true - var onTeamSelected: ((teamId: Long?) -> Unit)? = null + var onTeamSelected: ((team: Team?) -> Unit)? = null override fun create(context: Context) { super.create(context) @@ -59,7 +45,7 @@ class TeamDropdown : TextInputDropDown { list += teams.map { Item( it.id, it.name, - tag = it.id + tag = it ) } list @@ -72,10 +58,11 @@ class TeamDropdown : TextInputDropDown { when (it.tag) { -1L -> { // no team + deselect() onTeamSelected?.invoke(null) - true + false } - is Long -> { + is Team -> { // selected a team onTeamSelected?.invoke(it.tag) true @@ -85,21 +72,29 @@ class TeamDropdown : TextInputDropDown { } } - fun selectTeam(teamId: Long) { - if (select(teamId) == null) - select(Item( - teamId, - "nieznana grupa ($teamId)", - tag = teamId - )) + /** + * Select a teacher by the [teamId]. + */ + fun selectTeam(teamId: Long): Item? { + if (teamId == -1L) { + deselect() + return null + } + return select(teamId) } - fun selectDefault(teamId: Long?) { + /** + * Select a team by the [teamId] **if it's not selected yet**. + */ + fun selectDefault(teamId: Long?): Item? { if (teamId == null || selected != null) - return - selectTeam(teamId) + return null + return selectTeam(teamId) } + /** + * Select a team of the [Team.TYPE_CLASS] type. + */ fun selectTeamClass() { select(items.singleOrNull { it.tag is Team && it.tag.type == Team.TYPE_CLASS @@ -110,12 +105,12 @@ class TeamDropdown : TextInputDropDown { * Get the currently selected team. * ### Returns: * - null if no valid team is selected - * - [Long] - the team's ID + * - [Team] - the selected team */ - fun getSelected(): Any? { + fun getSelected(): Team? { return when (selected?.tag) { -1L -> null - is Long -> selected?.tag as Long + is Team -> selected?.tag as Team else -> null } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/TimeDropdown.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/TimeDropdown.kt index 989184af..afecaf7e 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/TimeDropdown.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/TimeDropdown.kt @@ -175,7 +175,7 @@ class TimeDropdown : TextInputDropDown { return !noTimetable } - fun pickerDialog() { + private fun pickerDialog() { val time = (getSelected() as? Pair<*, *>)?.first as? Time ?: Time.getNow() MaterialTimePicker.Builder() diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/notifications/WidgetNotificationsFactory.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/notifications/WidgetNotificationsFactory.kt index b5207300..1b6d58e0 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/notifications/WidgetNotificationsFactory.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/notifications/WidgetNotificationsFactory.kt @@ -45,6 +45,7 @@ class WidgetNotificationsFactory(val app: App, val config: WidgetConfig) : Remot getLong("id") ?: 0, getString("title") ?: "", getString("text") ?: "", + getString("textLong"), getInt("type") ?: 0, getInt("profileId"), getString("profileName"), @@ -74,4 +75,4 @@ class WidgetNotificationsFactory(val app: App, val config: WidgetConfig) : Remot override fun hasStableIds() = true override fun getViewTypeCount() = 1 override fun onDestroy() = cursor?.close() ?: Unit -} \ No newline at end of file +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/BetterLink.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/BetterLink.kt index 53aa0429..6a5306bf 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/BetterLink.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/BetterLink.kt @@ -7,6 +7,8 @@ package pl.szczodrzynski.edziennik.utils import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent import android.graphics.Color import android.text.Spannable import android.text.SpannableString @@ -19,141 +21,268 @@ import android.view.ViewGroup import android.widget.TextView import androidx.appcompat.view.menu.MenuBuilder import androidx.appcompat.view.menu.MenuPopupHelper -import pl.szczodrzynski.edziennik.Intent -import pl.szczodrzynski.edziennik.copyToClipboard +import androidx.core.widget.addTextChangedListener +import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.data.api.Regexes -import pl.szczodrzynski.edziennik.get -import pl.szczodrzynski.edziennik.getTextPosition import pl.szczodrzynski.edziennik.utils.models.Date +@SuppressLint("RestrictedApi") object BetterLink { - @SuppressLint("RestrictedApi") - fun attach(textView: TextView, onActionSelected: (() -> Unit)? = null) { - textView.autoLinkMask = Linkify.WEB_URLS or Linkify.EMAIL_ADDRESSES - BetterLinkMovementMethod.linkify(textView.autoLinkMask, textView).setOnLinkClickListener { v, span: BetterLinkMovementMethod.ClickableSpanWithText -> - val url = span.text() - val c = v.context + /** + * Used in conjunction with the item's ID to execute the + * [attach]'s onActionSelected listener when the item is + * clicked. + */ + private const val FLAG_ACTION = 0x8000 - val s = v.text as Spanned - val start = s.getSpanStart(span.span()) - val end = s.getSpanEnd(span.span()) + private fun MenuBuilder.setTitle(title: CharSequence): MenuBuilder { + this::class.java.getDeclaredMethod("setHeaderTitleInt", CharSequence::class.java).let { + it.isAccessible = true + it.invoke(this, title) + } + return this + } - val parent = v.rootView.findViewById(android.R.id.content) - val parentLocation = intArrayOf(0, 0) - parent.getLocationOnScreen(parentLocation) - - val rect = textView.getTextPosition(start..end) - - val view = View(c) - view.layoutParams = ViewGroup.LayoutParams(rect.width(), rect.height()) - view.setBackgroundColor(Color.TRANSPARENT) - - parent.addView(view) - - view.x = rect.left.toFloat() - parentLocation[0] - view.y = rect.top.toFloat() - parentLocation[1] - - val menu = MenuBuilder(c) - val helper = MenuPopupHelper(c, menu, view) - val popup = helper.popup - - var menuTitle = url.substringAfter(":") - var date: Date? = null - - var urlItem: MenuItem? = null - var createEventItem: MenuItem? = null - //var goToTimetableItem: MenuItem? = null // TODO 2020-03-19: implement this - var mailItem: MenuItem? = null - var copyItem: MenuItem? = null - - when { - url.startsWith("mailto:") -> { - mailItem = menu.add(1, 20, 2, "Napisz e-mail") + private fun MenuItem.addListener(listener: (item: MenuItem) -> Boolean): MenuItem { + this::class.java.getDeclaredField("mClickListener").let { + it.isAccessible = true + val oldListener = it.get(this) as? MenuItem.OnMenuItemClickListener + it.set(this, object : MenuItem.OnMenuItemClickListener { + override fun onMenuItemClick(item: MenuItem): Boolean { + oldListener?.onMenuItemClick(item) + return listener(item) } - url.startsWith("dateYmd:") -> { - createEventItem = menu.add(1, 10, 2, "Utwórz wydarzenie") - //goToTimetableItem = menu.add(1, 11, 3, "Idź do planu lekcji") - date = parseDateYmd(menuTitle) - } - url.startsWith("dateDmy:") -> { - createEventItem = menu.add(1, 10, 2, "Utwórz wydarzenie") - //goToTimetableItem = menu.add(1, 11, 3, "Idź do planu lekcji") - date = parseDateDmy(menuTitle) - } - url.startsWith("dateAbs:") -> { - createEventItem = menu.add(1, 10, 2, "Utwórz wydarzenie") - //goToTimetableItem = menu.add(1, 11, 3, "Idź do planu lekcji") - date = parseDateAbs(menuTitle) - } - url.startsWith("dateRel:") -> { - createEventItem = menu.add(1, 10, 2, "Utwórz wydarzenie") - //goToTimetableItem = menu.add(1, 11, 3, "Idź do planu lekcji") - date = parseDateRel(menuTitle) - } - else -> { - urlItem = menu.add(1, 1, 2, "Otwórz w przeglądarce") - menuTitle = url - } - } - copyItem = menu.add(1, 1000, 1000, "Kopiuj tekst") + }) + } + return this + } - helper.setOnDismissListener { parent.removeView(view) } + private fun createUrlItems(menu: MenuBuilder, context: Context, url: String) { + menu.setTitle(url) + menu.add( + 1, + 2, + 2, + "Otwórz w przeglądarce" + ).setOnMenuItemClickListener { + Utils.openUrl(context, url) + true + } + } - urlItem?.setOnMenuItemClickListener { Utils.openUrl(c, url); true } - mailItem?.setOnMenuItemClickListener { Utils.openUrl(c, url); true } - copyItem?.setOnMenuItemClickListener { menuTitle.copyToClipboard(c); true } - createEventItem?.setOnMenuItemClickListener { - onActionSelected?.invoke() - val intent = Intent( - android.content.Intent.ACTION_MAIN, - "action" to "createManualEvent", - "eventDate" to date?.stringY_m_d - ) - c.sendBroadcast(intent) - true - } + private fun createMailtoItems(menu: MenuBuilder, context: Context, url: String) { + menu.add( + 1, + 3, + 3, + "Napisz e-mail" + ).setOnMenuItemClickListener { + Utils.openUrl(context, url) + true + } + } - menu::class.java.getDeclaredMethod("setHeaderTitleInt", CharSequence::class.java).let { - it.isAccessible = true - it.invoke(menu, menuTitle) - } - popup::class.java.getDeclaredField("mShowTitle").let { - it.isAccessible = true - it.set(popup, true) - } - helper::class.java.getDeclaredMethod("showPopup", Int::class.java, Int::class.java, Boolean::class.java, Boolean::class.java).let { - it.isAccessible = true - it.invoke(helper, 0, 0, false, true) - } + private fun createDateItems(menu: MenuBuilder, context: Context, date: Date?) { + date ?: return + menu.setTitle(date.formattedString) + menu.add( + 1, + 4 or FLAG_ACTION, + 4, + "Utwórz wydarzenie" + ).setOnMenuItemClickListener { + val intent = Intent( + Intent.ACTION_MAIN, + "action" to "createManualEvent", + "eventDate" to date.stringY_m_d + ) + context.sendBroadcast(intent) + true + } + } + + private fun createTeacherItems(menu: MenuBuilder, context: Context, teacherId: Long, fullName: String) { + menu.setTitle(fullName) + menu.add( + 1, + 5 or FLAG_ACTION, + 5, + "Napisz wiadomość" + ).setOnMenuItemClickListener { + val intent = Intent( + Intent.ACTION_MAIN, + "fragmentId" to MainActivity.TARGET_MESSAGES_COMPOSE, + "messageRecipientId" to teacherId + ) + context.sendBroadcast(intent) + true + } + } + + private fun onClickListener( + view: TextView, + span: BetterLinkMovementMethod.ClickableSpanWithText, + onActionSelected: (() -> Unit)? + ): Boolean { + val context = view.context + + val spanned = view.text as Spanned + val start = spanned.getSpanStart(span.span()) + val end = spanned.getSpanEnd(span.span()) + + val parent = view.rootView.findViewById(android.R.id.content) + val parentLocation = intArrayOf(0, 0) + parent.getLocationOnScreen(parentLocation) + + val rect = view.getTextPosition(start..end) + + val popupView = View(context) + popupView.layoutParams = ViewGroup.LayoutParams(rect.width(), rect.height()) + popupView.setBackgroundColor(Color.TRANSPARENT) + + parent.addView(popupView) + + popupView.x = rect.left.toFloat() - parentLocation[0] + popupView.y = rect.top.toFloat() - parentLocation[1] + + val menu = MenuBuilder(context) + val helper = MenuPopupHelper(context, menu, popupView) + val popup = helper.popup + + val spanUrl = span.text() + val spanParameter = spanUrl.substringAfter(":") + val spanText = spanned.substring(start, end) + + //goToTimetableItem = menu.add(1, 11, 3, "Idź do planu lekcji") + + // create appropriate items for spans + when { + spanUrl.startsWith("mailto:") -> createMailtoItems(menu, context, spanUrl) + spanUrl.startsWith("dateYmd:") -> createDateItems(menu, context, parseDateYmd(spanParameter)) + spanUrl.startsWith("dateDmy:") -> createDateItems(menu, context, parseDateDmy(spanParameter)) + spanUrl.startsWith("dateAbs:") -> createDateItems(menu, context, parseDateAbs(spanParameter)) + spanUrl.startsWith("dateRel:") -> createDateItems(menu, context, parseDateRel(spanParameter)) + spanUrl.startsWith("teacher:") -> createTeacherItems( + menu, + context, + teacherId = spanParameter.toLongOrNull() ?: -1, + fullName = spanText + ) + else -> createUrlItems(menu, context, spanUrl) + } + menu.add(1, 1000, 1000, "Kopiuj tekst").setOnMenuItemClickListener { + spanParameter.copyToClipboard(context) true } - val spanned = textView.text as? Spannable ?: { - SpannableString(textView.text) - }() + helper.setOnDismissListener { parent.removeView(popupView) } + + menu.visibleItems.forEach { item -> + if ((item.itemId and FLAG_ACTION) != FLAG_ACTION) + return@forEach + item.addListener { + onActionSelected?.invoke() + true + } + } + + popup::class.java.getDeclaredField("mShowTitle").let { + it.isAccessible = true + it.set(popup, true) + } + helper::class.java.getDeclaredMethod( + "showPopup", + Int::class.java, + Int::class.java, + Boolean::class.java, + Boolean::class.java + ).let { + it.isAccessible = true + it.invoke(helper, 0, 0, false, true) + } + return true + } + + fun attach( + textView: TextView, + teachers: Map? = null, + onActionSelected: (() -> Unit)? = null + ) { + textView.autoLinkMask = Linkify.WEB_URLS or Linkify.EMAIL_ADDRESSES + + BetterLinkMovementMethod + .linkify(textView.autoLinkMask, textView) + .setOnLinkClickListener { view, span -> + onClickListener(view, span, onActionSelected) + } + + textView.addTextChangedListener { + attachSpan(textView, teachers) + } + + attachSpan(textView, teachers) + } + + private fun attachSpan( + textView: TextView, + teachers: Map? = null + ) { + val spanned = textView.text as? Spannable ?: SpannableString(textView.text) + + teachers?.forEach { (id, fullName) -> + val index = textView.text.indexOf(fullName) + if (index == -1) + return@forEach + val span = URLSpan("teacher:$id") + spanned.setSpan( + span, + index, + index + fullName.length, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) + } Regexes.LINKIFY_DATE_YMD.findAll(textView.text).forEach { match -> val span = URLSpan("dateYmd:" + match.value) - spanned.setSpan(span, match.range.first, match.range.last + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + spanned.setSpan( + span, + match.range.first, + match.range.last + 1, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) } Regexes.LINKIFY_DATE_DMY.findAll(textView.text).forEach { match -> val span = URLSpan("dateDmy:" + match.value) - spanned.setSpan(span, match.range.first, match.range.last + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + spanned.setSpan( + span, + match.range.first, + match.range.last + 1, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) } Regexes.LINKIFY_DATE_ABSOLUTE.findAll(textView.text).forEach { match -> val span = URLSpan("dateAbs:" + match.value) - spanned.setSpan(span, match.range.first, match.range.last + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + spanned.setSpan( + span, + match.range.first, + match.range.last + 1, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) } Regexes.LINKIFY_DATE_RELATIVE.findAll(textView.text).forEach { match -> val span = URLSpan("dateRel:" + match.value) - spanned.setSpan(span, match.range.first, match.range.last + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + spanned.setSpan( + span, + match.range.first, + match.range.last + 1, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) } - //Linkify.addLinks(textView, LINKIFY_DATE_ABSOLUTE.toPattern(), "dateAbs:") - //Linkify.addLinks(textView, LINKIFY_DATE_RELATIVE.toPattern(), "dateRel:") } - private val monthNames = listOf("sty", "lut", "mar", "kwi", "maj", "cze", "lip", "sie", "wrz", "paź", "lis", "gru") + private val monthNames = + listOf("sty", "lut", "mar", "kwi", "maj", "cze", "lip", "sie", "wrz", "paź", "lis", "gru") private fun parseDateYmd(text: String): Date? { return Regexes.LINKIFY_DATE_YMD.find(text)?.let { @@ -163,6 +292,7 @@ object BetterLink { Date(year, month, day) } } + private fun parseDateDmy(text: String): Date? { return Regexes.LINKIFY_DATE_DMY.find(text)?.let { val day = it[1].toIntOrNull() ?: 1 @@ -194,7 +324,7 @@ object BetterLink { else -> 1 } - date.stepForward(0, 0, amount*unitInDays) + date.stepForward(0, 0, amount * unitInDays) } } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/BigNightUtil.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/BigNightUtil.kt new file mode 100644 index 00000000..c7f57ccc --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/BigNightUtil.kt @@ -0,0 +1,69 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-4-8. + */ + +package pl.szczodrzynski.edziennik.utils + +import pl.szczodrzynski.edziennik.utils.models.Date +import kotlin.math.absoluteValue + +// Obliczanie daty wielkanocy - algorytm Gaussa +// www.algorytm.org +// (c) 2008 by Tomasz Lubinski +// http://www.algorytm.org/przetwarzanie-dat/wyznaczanie-daty-wielkanocy-algortym-gaussa/dwg-j.html + +class BigNightUtil { + + /* Pobierz wartosc A z tabeli lat */ + private fun getA(rok: Int) = when { + rok <= 1582 -> 15 + rok <= 1699 -> 22 + rok <= 1899 -> 23 + rok <= 2199 -> 24 + rok <= 2299 -> 25 + rok <= 2399 -> 26 + rok <= 2499 -> 25 + else -> 0 + } + + /* Pobierz wartosc B z tabeli lat */ + private fun getB(rok: Int) = when { + rok <= 1582 -> 6 + rok <= 1699 -> 2 + rok <= 1799 -> 3 + rok <= 1899 -> 4 + rok <= 2099 -> 5 + rok <= 2199 -> 6 + rok <= 2299 -> 0 + rok <= 2499 -> 1 + else -> 0 + } + + /* oblicz ile dni po 22 marca przypada wielkanoc */ + private fun Oblicz_Date_wielkanocy(rok: Int): Int { + val a = rok % 19 + val b = rok % 4 + val c = rok % 7 + var d = (a * 19 + getA(rok)) % 30 + val e = (2 * b + 4 * c + 6 * d + getB(rok)) % 7 + if (d == 29 && e == 6 || d == 28 && e == 6) { + d -= 7 + } + return d + e + } + + private fun get_dataOf_bigNight(): Date { + val date = Date.getToday() + date.month = 4 + date.day = 22 + Oblicz_Date_wielkanocy(date.year) + if (date.day > 31) + date.day = date.day % 31 + else + date.month = 3 + + return date + } + + fun isDataWielkanocyNearDzisiaj() = + Date.diffDays(Date.getToday(), get_dataOf_bigNight()).absoluteValue < 7 +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/TextInputDropDown.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/TextInputDropDown.kt index f8836374..44db5d9a 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/TextInputDropDown.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/TextInputDropDown.kt @@ -1,7 +1,11 @@ package pl.szczodrzynski.edziennik.utils +import android.annotation.SuppressLint import android.content.Context +import android.graphics.drawable.Drawable import android.util.AttributeSet +import androidx.appcompat.view.menu.MenuBuilder +import androidx.appcompat.view.menu.MenuPopupHelper import androidx.appcompat.widget.PopupMenu import com.google.android.material.textfield.TextInputEditText import com.mikepenz.iconics.IconicsDrawable @@ -29,10 +33,11 @@ open class TextInputDropDown : TextInputEditText { val selectedId get() = selected?.id - fun updateText() { + private fun updateText() { setText(selected?.displayText ?: selected?.text) } + @SuppressLint("RestrictedApi") open fun create(context: Context) { val drawable = IconicsDrawable(context, CommunityMaterial.Icon.cmd_chevron_down).apply { colorInt = Themes.getPrimaryTextColor(context) @@ -58,7 +63,9 @@ open class TextInputDropDown : TextInputEditText { val popup = PopupMenu(context, this) items.forEachIndexed { index, item -> - popup.menu.add(0, item.id.toInt(), index, item.text) + popup.menu.add(0, item.id.toInt(), index, item.text).also { + it.icon = item.icon + } } popup.setOnMenuItemClickListener { menuItem -> @@ -70,29 +77,46 @@ open class TextInputDropDown : TextInputEditText { true } - popup.setOnDismissListener { + val helper = MenuPopupHelper(context, popup.menu as MenuBuilder, this) + helper.setForceShowIcon(true) + helper.setOnDismissListener { clearFocus() } - - popup.show() + helper.show() } } - fun select(item: Item): Item? { + /** + * Select an arbitrary [item]. Allows to select an item not present + * in the original list. + */ + fun select(item: Item): Item { selected = item updateText() error = null return item } + /** + * Select an item by its ID. Returns the selected item + * if found. + */ fun select(id: Long?): Item? { return items.singleOrNull { it.id == id }?.let { select(it) } } + /** + * Select an item by its tag. Returns the selected item + * if found. + */ fun select(tag: Any?): Item? { return items.singleOrNull { it.tag == tag }?.let { select(it) } } + /** + * Select an item by its index. Returns the selected item + * if the index exists. + */ fun select(index: Int): Item? { return items.getOrNull(index)?.let { select(it) } } @@ -143,5 +167,11 @@ open class TextInputDropDown : TextInputEditText { } } - class Item(val id: Long, val text: CharSequence, val displayText: CharSequence? = null, val tag: Any? = null) + class Item( + val id: Long, + val text: CharSequence, + val displayText: CharSequence? = null, + val tag: Any? = null, + val icon: Drawable? = null + ) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/EventManager.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/EventManager.kt index ca444ca5..f5b7faa9 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/EventManager.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/EventManager.kt @@ -4,12 +4,17 @@ package pl.szczodrzynski.edziennik.utils.managers +import androidx.core.view.isVisible +import com.mikepenz.iconics.IconicsDrawable +import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial +import com.mikepenz.iconics.utils.colorInt +import com.mikepenz.iconics.utils.sizeDp +import com.mikepenz.iconics.view.IconicsTextView import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job -import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.data.db.full.EventFull -import pl.szczodrzynski.edziennik.startCoroutineTimer import kotlin.coroutines.CoroutineContext class EventManager(val app: App) : CoroutineScope { @@ -32,4 +37,41 @@ class EventManager(val app: App) : CoroutineScope { app.db.metadataDao().setSeen(event.profileId, event, true) } } + + fun setEventTopic( + title: IconicsTextView, + event: EventFull, + showType: Boolean = true, + doneIconColor: Int? = null + ) { + var eventTopic = if (showType) + "${event.typeName ?: "wydarzenie"} - ${event.topic}" + else + event.topic + + if (event.addedManually) { + eventTopic = "{cmd-clipboard-edit-outline} $eventTopic" + } + + title.text = eventTopic + + title.setCompoundDrawables( + null, + null, + if (event.isDone) IconicsDrawable(title.context).apply { + icon = CommunityMaterial.Icon.cmd_check + colorInt = doneIconColor ?: R.color.md_green_500.resolveColor(title.context) + sizeDp = 24 + } else null, + null + ) + } + + fun setLegendText(legend: IconicsTextView, event: EventFull) { + legend.text = listOfNotNull( + if (event.addedManually) R.string.legend_event_added_manually else null, + if (event.isDone) R.string.legend_event_is_done else null + ).map { legend.context.getString(it) }.join("\n") + legend.isVisible = legend.text.isNotBlank() + } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/models/Date.java b/app/src/main/java/pl/szczodrzynski/edziennik/utils/models/Date.java index b39d0298..ebc4570c 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/models/Date.java +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/models/Date.java @@ -94,17 +94,29 @@ public class Date implements Comparable { } } - public long getInMillis() { + public Calendar getAsCalendar() { Calendar c = Calendar.getInstance(); c.set(year, month - 1, day, 0, 0, 0); c.set(Calendar.MILLISECOND, 0); + return c; + } + + public Calendar getAsCalendar(Time time) { + if (time == null) + return getAsCalendar(); + Calendar c = Calendar.getInstance(); + c.set(year, month - 1, day, time.hour, time.minute, time.second); + c.set(Calendar.MILLISECOND, 0); + return c; + } + + public long getInMillis() { + Calendar c = getAsCalendar(); return c.getTimeInMillis(); } public long getInMillisUtc() { - Calendar c = Calendar.getInstance(); - c.set(year, month - 1, day, 0, 0, 0); - c.set(Calendar.MILLISECOND, 0); + Calendar c = getAsCalendar(); c.setTimeZone(TimeZone.getTimeZone("UTC")); return c.getTimeInMillis(); } @@ -179,13 +191,7 @@ public class Date implements Comparable { } public long combineWith(Time time) { - if (time == null) { - return getInMillis(); - } - Calendar c = Calendar.getInstance(); - c.set(this.year, this.month - 1, this.day, time.hour, time.minute, time.second); - c.set(Calendar.MILLISECOND, 0); - return c.getTimeInMillis(); + return getAsCalendar(time).getTimeInMillis(); } public int getWeekDay() { diff --git a/app/src/main/res/drawable/egg1.webp b/app/src/main/res/drawable/egg1.webp new file mode 100644 index 00000000..94440758 Binary files /dev/null and b/app/src/main/res/drawable/egg1.webp differ diff --git a/app/src/main/res/drawable/egg2.webp b/app/src/main/res/drawable/egg2.webp new file mode 100644 index 00000000..bfc8644a Binary files /dev/null and b/app/src/main/res/drawable/egg2.webp differ diff --git a/app/src/main/res/drawable/egg3.webp b/app/src/main/res/drawable/egg3.webp new file mode 100644 index 00000000..886b5ece Binary files /dev/null and b/app/src/main/res/drawable/egg3.webp differ diff --git a/app/src/main/res/drawable/egg4.webp b/app/src/main/res/drawable/egg4.webp new file mode 100644 index 00000000..1b958948 Binary files /dev/null and b/app/src/main/res/drawable/egg4.webp differ diff --git a/app/src/main/res/drawable/egg5.webp b/app/src/main/res/drawable/egg5.webp new file mode 100644 index 00000000..2cc48842 Binary files /dev/null and b/app/src/main/res/drawable/egg5.webp differ diff --git a/app/src/main/res/drawable/egg6.webp b/app/src/main/res/drawable/egg6.webp new file mode 100644 index 00000000..9dd98d89 Binary files /dev/null and b/app/src/main/res/drawable/egg6.webp differ diff --git a/app/src/main/res/drawable/timetable_marker_triangle.xml b/app/src/main/res/drawable/timetable_marker_triangle.xml new file mode 100644 index 00000000..97d2e71b --- /dev/null +++ b/app/src/main/res/drawable/timetable_marker_triangle.xml @@ -0,0 +1,15 @@ + + + + + + + diff --git a/app/src/main/res/layout/agenda_counter_item.xml b/app/src/main/res/layout/agenda_counter_item.xml new file mode 100644 index 00000000..334c419c --- /dev/null +++ b/app/src/main/res/layout/agenda_counter_item.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/agenda_event_compact_item.xml b/app/src/main/res/layout/agenda_event_compact_item.xml new file mode 100644 index 00000000..b97ddfa3 --- /dev/null +++ b/app/src/main/res/layout/agenda_event_compact_item.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/agenda_event_item.xml b/app/src/main/res/layout/agenda_event_item.xml new file mode 100644 index 00000000..acaffa27 --- /dev/null +++ b/app/src/main/res/layout/agenda_event_item.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/agenda_group_item.xml b/app/src/main/res/layout/agenda_group_item.xml new file mode 100644 index 00000000..4787a8cf --- /dev/null +++ b/app/src/main/res/layout/agenda_group_item.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/agenda_event_lesson_change.xml b/app/src/main/res/layout/agenda_wrapped_counter.xml similarity index 77% rename from app/src/main/res/layout/agenda_event_lesson_change.xml rename to app/src/main/res/layout/agenda_wrapped_counter.xml index c51f9b35..31864636 100644 --- a/app/src/main/res/layout/agenda_event_lesson_change.xml +++ b/app/src/main/res/layout/agenda_wrapped_counter.xml @@ -1,15 +1,17 @@ + + - diff --git a/app/src/main/res/layout/agenda_event_teacher_absence.xml b/app/src/main/res/layout/agenda_wrapped_event.xml similarity index 79% rename from app/src/main/res/layout/agenda_event_teacher_absence.xml rename to app/src/main/res/layout/agenda_wrapped_event.xml index 785256d0..2ca38528 100644 --- a/app/src/main/res/layout/agenda_event_teacher_absence.xml +++ b/app/src/main/res/layout/agenda_wrapped_event.xml @@ -1,13 +1,17 @@ + + - diff --git a/app/src/main/res/layout/agenda_wrapped_event_compact.xml b/app/src/main/res/layout/agenda_wrapped_event_compact.xml new file mode 100644 index 00000000..997c24db --- /dev/null +++ b/app/src/main/res/layout/agenda_wrapped_event_compact.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/app/src/main/res/layout/agenda_wrapped_group.xml b/app/src/main/res/layout/agenda_wrapped_group.xml new file mode 100644 index 00000000..6b081d8e --- /dev/null +++ b/app/src/main/res/layout/agenda_wrapped_group.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/app/src/main/res/layout/attendance_details_dialog.xml b/app/src/main/res/layout/attendance_details_dialog.xml index 34a2dc0d..b18e1c89 100644 --- a/app/src/main/res/layout/attendance_details_dialog.xml +++ b/app/src/main/res/layout/attendance_details_dialog.xml @@ -94,6 +94,7 @@ android:textAppearance="@style/NavView.TextView.Helper" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_day.xml b/app/src/main/res/layout/dialog_day.xml index 59fd7f8c..8bbc7b64 100644 --- a/app/src/main/res/layout/dialog_day.xml +++ b/app/src/main/res/layout/dialog_day.xml @@ -3,118 +3,108 @@ ~ Copyright (c) Kuba Szczodrzyński 2019-12-16. --> - - + android:layout_width="match_parent" + android:layout_height="match_parent"> - - - + + + + + + + + + + + + + + + + + android:paddingVertical="16dp" + android:visibility="gone" + tools:visibility="visible"> - + android:layout_gravity="center" + android:drawablePadding="16dp" + android:fontFamily="sans-serif-light" + android:gravity="center" + android:text="@string/dialog_day_no_events" + android:textSize="24sp" + app:drawableTopCompat="@drawable/ic_no_events" /> - - - - - - - - - - - - - - - - - - - - - + android:layout_gravity="center" + android:gravity="center" + android:text="@string/dialog_no_events_hint" + android:textStyle="italic" /> - - + + + + diff --git a/app/src/main/res/layout/dialog_event_details.xml b/app/src/main/res/layout/dialog_event_details.xml index 7a9d8004..a2fe00f5 100644 --- a/app/src/main/res/layout/dialog_event_details.xml +++ b/app/src/main/res/layout/dialog_event_details.xml @@ -105,13 +105,28 @@ + + + + - + + + diff --git a/app/src/main/res/layout/event_list_item.xml b/app/src/main/res/layout/event_list_item.xml index 24445327..a69b74c0 100644 --- a/app/src/main/res/layout/event_list_item.xml +++ b/app/src/main/res/layout/event_list_item.xml @@ -4,8 +4,7 @@ --> + xmlns:android="http://schemas.android.com/apk/res/android"> @@ -61,12 +60,10 @@ android:gravity="center_vertical" android:orientation="horizontal"> - - - - + tools:visibility="visible" /> - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/messages_config_dialog.xml b/app/src/main/res/layout/messages_config_dialog.xml new file mode 100644 index 00000000..d0205b39 --- /dev/null +++ b/app/src/main/res/layout/messages_config_dialog.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/notifications_list_item.xml b/app/src/main/res/layout/notifications_list_item.xml index 498f7e21..13df88d1 100644 --- a/app/src/main/res/layout/notifications_list_item.xml +++ b/app/src/main/res/layout/notifications_list_item.xml @@ -9,12 +9,28 @@ android:orientation="vertical" android:padding="8dp"> - + android:gravity="center_vertical" + android:orientation="horizontal"> + + + + + - - - - - - - - - - diff --git a/app/src/main/res/layout/row_teacher_absence_item.xml b/app/src/main/res/layout/row_teacher_absence_item.xml deleted file mode 100644 index da78c625..00000000 --- a/app/src/main/res/layout/row_teacher_absence_item.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - diff --git a/app/src/main/res/layout/timetable_day_fragment.xml b/app/src/main/res/layout/timetable_day_fragment.xml new file mode 100644 index 00000000..5464482d --- /dev/null +++ b/app/src/main/res/layout/timetable_day_fragment.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 9949f2e2..431fd960 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -856,7 +856,7 @@ Open-Source-Lizenzen Datenschutzrichtlinie E-Klassenbuch - © Kuba Szczodrzyński && Kacper Ziubryniewicz\nSeptember 2018 - Februar 2021 + © Kuba Szczodrzyński && Kacper Ziubryniewicz\nSeptember 2018 - Mai 2021 Klicken Sie hier, um nach Aktualisierungen zu suchen Aktualisierung Version @@ -967,6 +967,8 @@ Öffnen Sie das Menü mit der Zurück-Taste Schlittenfahrt im Schnee Jingle Bells, Jingle Bells + Brrrr + Bajo jajo, bajo jajo Rosa System Thema diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml index 64b56cdb..e519d272 100644 --- a/app/src/main/res/values-en/strings.xml +++ b/app/src/main/res/values-en/strings.xml @@ -356,7 +356,7 @@ Sharing event… Removing event from the rest of the class… Removing shared event… - Are you sure you want to mark this task as completed?\n\nIt won\'t show up on the main or homework pages, but will still be available in the timetable + Are you sure you want to mark this task as completed?\n\nIt won\'t show up on the main or homework pages, but will still be available in the agenda. Mark as done other project @@ -858,7 +858,7 @@ Open-source licenses Privacy policy E-register - © Kuba Szczodrzyński && Kacper Ziubryniewicz\nSeptember 2018 - February 2021 + © Kuba Szczodrzyński && Kacper Ziubryniewicz\nSeptember 2018 - May 2021 Click to check for updates Update Version @@ -969,6 +969,8 @@ Back button opens drawer Jingle all the way Jingle bells, Jingle bells + Brrrr + Bajo jajo, bajo jajo Pink System Theme diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e5f21e05..5aa206af 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -921,7 +921,7 @@ Licencje open-source Polityka prywatności E-dziennik - © Kuba Szczodrzyński && Kacper Ziubryniewicz\nwrzesień 2018 - luty 2021 + © Kuba Szczodrzyński && Kacper Ziubryniewicz\nwrzesień 2018 - maj 2021 Kliknij, aby sprawdzić aktualizacje Aktualizacja Wersja @@ -1034,6 +1034,8 @@ Otwieraj menu przyciskiem wstecz Dzwonią dzwonki sań Pada śnieg, pada śnieg + Brrrr + Bajo jajo, bajo jajo Różowy Systemowy Motyw @@ -1425,4 +1427,34 @@ Aby móc zapisać wygenerowany plan lekcji musisz przyznać uprawnienia dostępu do pamięci urządzenia.\n\nKliknij OK, aby przyznać uprawnienia. przeczytanie Polityki prywatności i akceptujesz jej postanowienia.

Autorzy aplikacji nie biorą odpowiedzialności za korzystanie z aplikacji Szkolny.eu.]]>
Szkolny.eu v%s\n%s + Ustawienia terminarza + Wygląd + Pokazuj zmiany planu lekcji + Pokazuj nieobecności nauczycieli + Tryb kompaktowy + Mniejszy rozmiar wydarzeń na liście + Grupuj wydarzenia tego samego typu + Niedostępne w trybie kalendarza + Udostępnianie wydarzeń + Włącz Udostępnianie wydarzeń + Nauczanie zdalne + Ustaw wydarzenia jako lekcje on-line + Wybierz rodzaj wydarzeń + Grupuj lekcje on-line na liście + {cmd-clipboard-edit-outline} wydarzenie dodane ręcznie + {cmd-check} oznaczono jako wykonane + Funkcja jeszcze nie jest dostępna. + Tworzenie wiadomości + Dodaj podpis przy tworzeniu wiadomości + Dodaj podpis przy odpowiadaniu na wiadomość + Dodaj podpis przy przekazywaniu wiadomości + Treść podpisu + Ustawienia wiadomości + lekcja online + Rodzaj: %s\nData: %s (%s)\nLekcja: %s\nNauczyciel: %s + Rodzaj: %s\nPrzedmiot: %s\nTermin: %s (%s), %s\nTreść: %s + Dodane przez: %s\nRodzaj: %s\nPrzedmiot: %s\nTermin: %s (%s), %s\nTreść: %s + Ocena: %s (waga %s)\nPrzedmiot: %s\nKategoria: %s\nOpis: %s\nNauczyciel: %s + Rodzaj: %s\nNauczyciel: %s\nTreść: %s + Rodzaj: %s\nTermin: %s, %s\nNr lekcji: %s\nPrzedmiot: %s\nNauczyciel: %s\nTemat lekcji: %s diff --git a/build.gradle b/build.gradle index cc8c8471..a2eb932b 100644 --- a/build.gradle +++ b/build.gradle @@ -5,8 +5,8 @@ buildscript { kotlin_version = '1.4.31' release = [ - versionName: "4.7", - versionCode: 4070099 + versionName: "4.8", + versionCode: 4080099 ] setup = [