From 1a543814f4cd967fa7b0f1a0832ee370ce8b6523 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Sun, 10 Oct 2021 19:21:50 +0200 Subject: [PATCH] [UI] Implement homework searching. (#93) * [Messages] Create message type checking methods. * [UI] Refactor messages searching to a separate module. * [Refactor] Move dialogs.event to modules package. * [Refactor] Move classes from modules.messages to separate packages. * [Homework] Implement searching homework lists. * [Homework] Fix highlighting search query in addedBy text. * [Homework] Workaround IconicsTextView discarding span data. * [Messages] Make attachments searchable. * [Events] Show icons for events with attachments. * [Homework] Workaround IconicsTextView discarding span data, again. * [Search] Fix serialization crashes with searchable models. * [Messages] Fix searching in HTML body. --- .../szczodrzynski/edziennik/MainActivity.kt | 6 +- .../messages/LibrusMessagesSendMessage.kt | 2 +- .../data/web/MobidziennikWebGetAttachment.kt | 2 +- .../data/web/MobidziennikWebGetMessage.kt | 6 +- .../data/web/MobidziennikWebSendMessage.kt | 3 +- .../hebe/VulcanHebeMessagesChangeStatus.kt | 3 +- .../vulcan/data/hebe/VulcanHebeSendMessage.kt | 2 +- .../edziennik/data/db/entity/Event.kt | 3 + .../edziennik/data/db/entity/Message.kt | 9 ++ .../edziennik/data/db/full/EventFull.kt | 44 +++++- .../edziennik/data/db/full/MessageFull.kt | 53 ++++++- .../edziennik/ui/dialogs/day/DayDialog.kt | 18 +-- .../ui/dialogs/event/EventListAdapter.kt | 126 ----------------- .../ui/dialogs/sync/SyncViewListDialog.kt | 2 +- .../dialogs/timetable/LessonDetailsDialog.kt | 8 +- .../ui/modules/agenda/AgendaFragment.kt | 2 +- .../modules/agenda/AgendaFragmentDefault.kt | 2 +- .../event/EventDetailsDialog.kt | 2 +- .../ui/modules/event/EventListAdapter.kt | 71 ++++++++++ .../event/EventManualDialog.kt | 2 +- .../ui/modules/event/EventViewHolder.kt | 133 ++++++++++++++++++ .../ui/modules/home/cards/HomeEventsCard.kt | 8 +- .../ui/modules/homework/HomeworkFragment.kt | 2 +- .../modules/homework/HomeworkListFragment.kt | 86 +++++------ .../ui/modules/messages/MessagesAdapter.kt | 86 ----------- .../ui/modules/messages/MessagesUtils.kt | 5 +- .../compose/MessagesComposeFragment.kt | 4 +- .../{viewholder => list}/MessageViewHolder.kt | 41 +++--- .../modules/messages/list/MessagesAdapter.kt | 50 +++++++ .../messages/{ => list}/MessagesFragment.kt | 2 +- .../{ => list}/MessagesListFragment.kt | 26 +--- .../modules/messages/models/MessagesSearch.kt | 9 -- .../messages/{ => single}/MessageFragment.kt | 14 +- .../messages/utils/MessagesComparator.kt | 28 ---- .../ui/modules/search/SearchField.kt | 21 +++ .../SearchFilter.kt} | 91 +++++------- .../utils => search}/SearchTextWatcher.kt | 11 +- .../viewholder => search}/SearchViewHolder.kt | 24 +--- .../edziennik/ui/modules/search/Searchable.kt | 24 ++++ .../ui/modules/search/SearchableAdapter.kt | 133 ++++++++++++++++++ .../ui/modules/timetable/TimetableFragment.kt | 2 +- .../edziennik/utils/managers/EventManager.kt | 3 +- .../utils/managers/MessageManager.kt | 6 +- app/src/main/res/layout/event_list_item.xml | 40 ++++-- .../main/res/layout/messages_list_item.xml | 7 +- ...s_list_item_search.xml => search_item.xml} | 0 46 files changed, 728 insertions(+), 494 deletions(-) delete mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventListAdapter.kt rename app/src/main/java/pl/szczodrzynski/edziennik/ui/{dialogs => modules}/event/EventDetailsDialog.kt (99%) create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/event/EventListAdapter.kt rename app/src/main/java/pl/szczodrzynski/edziennik/ui/{dialogs => modules}/event/EventManualDialog.kt (99%) create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/event/EventViewHolder.kt delete mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesAdapter.kt rename app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/{viewholder => list}/MessageViewHolder.kt (64%) create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/list/MessagesAdapter.kt rename app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/{ => list}/MessagesFragment.kt (98%) rename app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/{ => list}/MessagesListFragment.kt (83%) delete mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/models/MessagesSearch.kt rename app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/{ => single}/MessageFragment.kt (95%) delete mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/utils/MessagesComparator.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/search/SearchField.kt rename app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/{messages/utils/MessagesFilter.kt => search/SearchFilter.kt} (52%) rename app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/{messages/utils => search}/SearchTextWatcher.kt (75%) rename app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/{messages/viewholder => search}/SearchViewHolder.kt (50%) create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/search/Searchable.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/search/SearchableAdapter.kt rename app/src/main/res/layout/{messages_list_item_search.xml => search_item.xml} (100%) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt b/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt index 48b872e4..b27af5bb 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt @@ -52,7 +52,6 @@ import pl.szczodrzynski.edziennik.ui.dialogs.RegisterUnavailableDialog import pl.szczodrzynski.edziennik.ui.dialogs.ServerMessageDialog import pl.szczodrzynski.edziennik.ui.dialogs.UpdateAvailableDialog import pl.szczodrzynski.edziennik.ui.dialogs.changelog.ChangelogDialog -import pl.szczodrzynski.edziennik.ui.dialogs.event.EventManualDialog import pl.szczodrzynski.edziennik.ui.dialogs.profile.ProfileConfigDialog import pl.szczodrzynski.edziennik.ui.dialogs.sync.SyncViewListDialog import pl.szczodrzynski.edziennik.ui.modules.agenda.AgendaFragment @@ -64,15 +63,16 @@ import pl.szczodrzynski.edziennik.ui.modules.debug.DebugFragment import pl.szczodrzynski.edziennik.ui.modules.debug.LabFragment import pl.szczodrzynski.edziennik.ui.modules.error.ErrorDetailsDialog import pl.szczodrzynski.edziennik.ui.modules.error.ErrorSnackbar +import pl.szczodrzynski.edziennik.ui.modules.event.EventManualDialog import pl.szczodrzynski.edziennik.ui.modules.feedback.FeedbackFragment import pl.szczodrzynski.edziennik.ui.modules.grades.GradesListFragment import pl.szczodrzynski.edziennik.ui.modules.grades.editor.GradesEditorFragment import pl.szczodrzynski.edziennik.ui.modules.home.HomeFragment import pl.szczodrzynski.edziennik.ui.modules.homework.HomeworkFragment import pl.szczodrzynski.edziennik.ui.modules.login.LoginActivity -import pl.szczodrzynski.edziennik.ui.modules.messages.MessageFragment -import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesFragment import pl.szczodrzynski.edziennik.ui.modules.messages.compose.MessagesComposeFragment +import pl.szczodrzynski.edziennik.ui.modules.messages.list.MessagesFragment +import pl.szczodrzynski.edziennik.ui.modules.messages.single.MessageFragment import pl.szczodrzynski.edziennik.ui.modules.notifications.NotificationsListFragment import pl.szczodrzynski.edziennik.ui.modules.settings.ProfileManagerFragment import pl.szczodrzynski.edziennik.ui.modules.settings.SettingsFragment diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusMessagesSendMessage.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusMessagesSendMessage.kt index d568135e..e388f6fd 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusMessagesSendMessage.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusMessagesSendMessage.kt @@ -48,7 +48,7 @@ class LibrusMessagesSendMessage(override val data: DataLibrus, } LibrusMessagesGetList(data, type = Message.TYPE_SENT, lastSync = null) { - val message = data.messageList.firstOrNull { it.type == Message.TYPE_SENT && it.id == id } + val message = data.messageList.firstOrNull { it.isSent && it.id == id } val metadata = data.metadataList.firstOrNull { it.thingType == Metadata.TYPE_MESSAGE && it.thingId == message?.id } val event = MessageSentEvent(data.profileId, message, message?.addedDate) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGetAttachment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGetAttachment.kt index 52561a81..b8c6644f 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGetAttachment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGetAttachment.kt @@ -28,7 +28,7 @@ class MobidziennikWebGetAttachment(override val data: DataMobidziennik, val targetFile = File(Utils.getStorageDir(), attachmentName) val typeUrl = when (owner) { - is Message -> if (owner.type == Message.TYPE_SENT) + is Message -> if (owner.isSent) "dziennik/wiadwyslana/?id=" else "dziennik/wiadodebrana/?id=" diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGetMessage.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGetMessage.kt index 542e4488..b23182ba 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGetMessage.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGetMessage.kt @@ -10,8 +10,6 @@ import pl.szczodrzynski.edziennik.data.api.Regexes import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.MobidziennikWeb import pl.szczodrzynski.edziennik.data.api.events.MessageGetEvent -import pl.szczodrzynski.edziennik.data.db.entity.Message -import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_RECEIVED import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.data.db.full.MessageFull import pl.szczodrzynski.edziennik.data.db.full.MessageRecipientFull @@ -31,7 +29,7 @@ class MobidziennikWebGetMessage(override val data: DataMobidziennik, } init { - val typeUrl = if (message.type == Message.TYPE_SENT) + val typeUrl = if (message.isSent) "wiadwyslana" else "wiadodebrana" @@ -46,7 +44,7 @@ class MobidziennikWebGetMessage(override val data: DataMobidziennik, val body = content.select(".wiadomosc_tresc").first() - if (message.type == TYPE_RECEIVED) { + if (message.isReceived) { var readDate = System.currentTimeMillis() Regexes.MOBIDZIENNIK_MESSAGE_READ_DATE.find(body.html())?.let { val date = Date( diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebSendMessage.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebSendMessage.kt index 89178b8b..b5a6c5ee 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebSendMessage.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebSendMessage.kt @@ -9,7 +9,6 @@ import pl.szczodrzynski.edziennik.data.api.POST import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.MobidziennikWeb import pl.szczodrzynski.edziennik.data.api.events.MessageSentEvent -import pl.szczodrzynski.edziennik.data.db.entity.Message import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.data.db.entity.Teacher @@ -43,7 +42,7 @@ class MobidziennikWebSendMessage(override val data: DataMobidziennik, // TODO create MobidziennikWebMessagesSent and replace this MobidziennikWebMessagesAll(data, null) { - val message = data.messageList.firstOrNull { it.type == Message.TYPE_SENT && it.subject == subject } + val message = data.messageList.firstOrNull { it.isSent && it.subject == subject } val metadata = data.metadataList.firstOrNull { it.thingType == Metadata.TYPE_MESSAGE && it.thingId == message?.id } val event = MessageSentEvent(data.profileId, message, message?.addedDate) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeMessagesChangeStatus.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeMessagesChangeStatus.kt index ff5dbe4c..a5be03b9 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeMessagesChangeStatus.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeMessagesChangeStatus.kt @@ -10,7 +10,6 @@ import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_MESSAGES_STATUS import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe import pl.szczodrzynski.edziennik.data.api.events.MessageGetEvent -import pl.szczodrzynski.edziennik.data.db.entity.Message import pl.szczodrzynski.edziennik.data.db.entity.MessageRecipient import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.data.db.full.MessageFull @@ -48,7 +47,7 @@ class VulcanHebeMessagesChangeStatus( messageObject.seen = true } - if (messageObject.type != Message.TYPE_SENT) { + if (!messageObject.isSent) { val messageRecipientObject = MessageRecipient( profileId, -1, diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeSendMessage.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeSendMessage.kt index c6a02ae7..2e7b1491 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeSendMessage.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeSendMessage.kt @@ -87,7 +87,7 @@ class VulcanHebeSendMessage( } VulcanHebeMessages(data, null) { - val message = data.messageList.firstOrNull { it.type == Message.TYPE_SENT && it.subject == subject } + val message = data.messageList.firstOrNull { it.isSent && it.subject == subject } val metadata = data.metadataList.firstOrNull { it.thingType == Metadata.TYPE_MESSAGE && it.thingId == messageId } val event = MessageSentEvent(data.profileId, message, message?.addedDate) 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 4d7d4450..928f0931 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 @@ -10,6 +10,7 @@ import androidx.room.Index import com.google.gson.annotations.SerializedName import pl.szczodrzynski.edziennik.MINUTE import pl.szczodrzynski.edziennik.data.db.full.EventFull +import pl.szczodrzynski.edziennik.isNotNullNorEmpty import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Time import java.util.* @@ -97,6 +98,8 @@ open class Event( * or the topic contains the body already. */ var homeworkBody: String? = null + val hasAttachments + get() = attachmentIds.isNotNullNorEmpty() var attachmentIds: MutableList? = null var attachmentNames: MutableList? = null diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Message.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Message.kt index c115c3d3..927e6d1d 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Message.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Message.kt @@ -43,6 +43,15 @@ open class Message( @ColumnInfo(name = "messageIsPinned") var isStarred: Boolean = false + val isReceived + get() = type == TYPE_RECEIVED + val isSent + get() = type == TYPE_SENT + val isDeleted + get() = type == TYPE_DELETED + val isDraft + get() = type == TYPE_DRAFT + var hasAttachments = false // if the attachments are not yet downloaded but we already know there are some get() = field || attachmentIds.isNotNullNorEmpty() var attachmentIds: MutableList? = null diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/EventFull.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/EventFull.kt index 86a5ab92..c68ecedd 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/EventFull.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/EventFull.kt @@ -3,8 +3,10 @@ */ package pl.szczodrzynski.edziennik.data.db.full +import androidx.room.Ignore import pl.szczodrzynski.edziennik.data.db.entity.Event import pl.szczodrzynski.edziennik.data.db.entity.Metadata +import pl.szczodrzynski.edziennik.ui.modules.search.Searchable import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Time @@ -16,7 +18,7 @@ class EventFull( profileId, id, date, time, topic, color, type, teacherId, subjectId, teamId, addedDate -) { +), Searchable { constructor(event: Event, metadata: Metadata? = null) : this( event.profileId, event.id, event.date, event.time, event.topic, event.color, event.type, @@ -46,6 +48,46 @@ class EventFull( var teamName: String? = null var teamCode: String? = null + @Ignore + @Transient + override var searchPriority = 0 + + @Ignore + @Transient + override var searchHighlightText: String? = null + + @delegate:Ignore + @delegate:Transient + override val searchKeywords by lazy { + listOf( + listOf(topic, homeworkBody), + attachmentNames, + listOf(subjectLongName), + listOf(teacherName), + listOf(sharedByName), + ) + } + + override fun compareTo(other: Searchable<*>): Int { + if (other !is EventFull) + return 0 + return when { + // ascending sorting + searchPriority > other.searchPriority -> 1 + searchPriority < other.searchPriority -> -1 + // ascending sorting + date > other.date -> 1 + date < other.date -> -1 + // ascending sorting + (time?.value ?: 0) > (other.time?.value ?: 0) -> 1 + (time?.value ?: 0) < (other.time?.value ?: 0) -> -1 + // ascending sorting + addedDate > other.addedDate -> 1 + addedDate < other.addedDate -> -1 + else -> 0 + } + } + // metadata var seen = false var notified = false 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 3f912711..534ad11c 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 @@ -3,10 +3,12 @@ */ package pl.szczodrzynski.edziennik.data.db.full +import androidx.core.text.HtmlCompat import androidx.room.Ignore import androidx.room.Relation import pl.szczodrzynski.edziennik.data.db.entity.Message import pl.szczodrzynski.edziennik.data.db.entity.MessageRecipient +import pl.szczodrzynski.edziennik.ui.modules.search.Searchable class MessageFull( profileId: Int, id: Long, type: Int, @@ -16,7 +18,7 @@ class MessageFull( profileId, id, type, subject, body, senderId, addedDate -) { +), Searchable { var senderName: String? = null @Relation(parentColumn = "messageId", entityColumn = "messageId", entity = MessageRecipient::class) var recipients: MutableList? = null @@ -27,11 +29,56 @@ class MessageFull( return this } + @delegate:Ignore + @delegate:Transient + val bodyHtml by lazy { + body?.let { + HtmlCompat.fromHtml(it, HtmlCompat.FROM_HTML_MODE_LEGACY) + } + } + + @Ignore - var filterWeight = 0 + @Transient + override var searchPriority = 0 + @Ignore - var searchHighlightText: CharSequence? = null + @Transient + override var searchHighlightText: String? = null + + @delegate:Ignore + @delegate:Transient + override val searchKeywords by lazy { + listOf( + when { + isSent -> recipients?.map { it.fullName } + else -> listOf(senderName) + }, + listOf(subject), + listOf(bodyHtml?.toString()), + attachmentNames, + ) + } + + override fun compareTo(other: Searchable<*>): Int { + if (other !is MessageFull) + return 0 + return when { + // ascending sorting + searchPriority > other.searchPriority -> 1 + searchPriority < other.searchPriority -> -1 + // descending sorting (1. true, 2. false) + isStarred && !other.isStarred -> -1 + !isStarred && other.isStarred -> 1 + // descending sorting + addedDate > other.addedDate -> -1 + addedDate < other.addedDate -> 1 + else -> 0 + } + } + @Ignore + @Transient var readByEveryone = true // metadata 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 74ba0030..55850a0e 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 @@ -14,15 +14,15 @@ import kotlinx.coroutines.* import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.data.db.entity.Lesson import pl.szczodrzynski.edziennik.databinding.DialogDayBinding -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.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.ui.modules.event.EventDetailsDialog +import pl.szczodrzynski.edziennik.ui.modules.event.EventListAdapter +import pl.szczodrzynski.edziennik.ui.modules.event.EventManualDialog import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Time @@ -161,7 +161,7 @@ class DayDialog( b.teacherAbsenceFrame.isVisible = teacherAbsences.isNotEmpty() adapter = EventListAdapter( - activity, + activity = activity, showWeekDay = false, showDate = false, showType = true, @@ -188,10 +188,12 @@ class DayDialog( ) app.db.eventDao().getAllByDate(profileId, date).observe(activity) { events -> - adapter.items = if (eventTypeId != null) - events.filter { it.type == eventTypeId } - else - events + adapter.setAllItems( + if (eventTypeId != null) + events.filter { it.type == eventTypeId } + else + events, + ) if (b.eventsView.adapter == null) { b.eventsView.adapter = adapter 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 deleted file mode 100644 index 9bb199ea..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventListAdapter.kt +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright (c) Kacper Ziubryniewicz 2019-11-30 - */ - -package pl.szczodrzynski.edziennik.ui.dialogs.event - -import android.content.Context -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.core.view.isVisible -import androidx.recyclerview.widget.RecyclerView -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import pl.szczodrzynski.edziennik.* -import pl.szczodrzynski.edziennik.data.db.full.EventFull -import pl.szczodrzynski.edziennik.databinding.EventListItemBinding -import pl.szczodrzynski.edziennik.utils.models.Date -import pl.szczodrzynski.edziennik.utils.models.Week -import kotlin.coroutines.CoroutineContext - -class EventListAdapter( - val context: Context, - val simpleMode: Boolean = false, - val showWeekDay: Boolean = false, - val showDate: Boolean = false, - val showType: Boolean = true, - val showTime: Boolean = true, - val showSubject: Boolean = true, - val markAsSeen: Boolean = true, - val onItemClick: ((event: EventFull) -> Unit)? = null, - val onEventEditClick: ((event: EventFull) -> Unit)? = null -) : RecyclerView.Adapter(), CoroutineScope { - - private val app = context.applicationContext as App - private val manager - get() = app.eventManager - - private val job = Job() - override val coroutineContext: CoroutineContext - get() = job + Dispatchers.Main - - var items = listOf() - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val inflater = LayoutInflater.from(parent.context) - val view = EventListItemBinding.inflate(inflater, parent, false) - return ViewHolder(view) - } - - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - val event = items[position] - val b = holder.b - val manager = app.eventManager - - b.root.onClick { - onItemClick?.invoke(event) - if (!event.seen) { - manager.markAsSeen(event) - } - if (event.showAsUnseen == true) { - event.showAsUnseen = false - notifyItemChanged(event) - } - } - - val bullet = " • " - - b.simpleMode = simpleMode - - manager.setEventTopic(b.topic, event, showType = false) - b.topic.maxLines = if (simpleMode) 2 else 3 - - b.details.text = mutableListOf( - if (showWeekDay) Week.getFullDayName(event.date.weekDay) else null, - if (showDate) event.date.getRelativeString(context, 7) ?: event.date.formattedStringShort else null, - if (showType) event.typeName else null, - if (showTime) event.time?.stringHM ?: app.getString(R.string.event_all_day) else null, - if (showSubject) event.subjectLongName else null - ).concat(bullet) - - b.addedBy.setText( - when (event.sharedBy) { - null -> when { - event.addedManually -> R.string.event_list_added_by_self_format - event.teacherName == null -> R.string.event_list_added_by_unknown_format - else -> R.string.event_list_added_by_format - } - "self" -> R.string.event_list_shared_by_self_format - else -> R.string.event_list_shared_by_format - }, - Date.fromMillis(event.addedDate).formattedString, - event.sharedByName ?: event.teacherName ?: "", - event.teamName?.let { bullet+it } ?: "" - ) - - b.typeColor.background?.setTintColor(event.eventColor) - b.typeColor.isVisible = showType - - b.editButton.isVisible = !simpleMode && event.addedManually && !event.isDone - b.editButton.onClick { - onEventEditClick?.invoke(event) - } - b.editButton.attachToastHint(R.string.hint_edit_event) - - if (event.showAsUnseen == null) - event.showAsUnseen = !event.seen - - b.unread.isVisible = event.showAsUnseen == true - if (markAsSeen && !event.seen) { - manager.markAsSeen(event) - } - } - - private fun notifyItemChanged(model: Any) { - startCoroutineTimer(1000L, 0L) { - val index = items.indexOf(model) - if (index != -1) - notifyItemChanged(index) - } - } - - override fun getItemCount() = items.size - - class ViewHolder(val b: EventListItemBinding) : RecyclerView.ViewHolder(b.root) -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/sync/SyncViewListDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/sync/SyncViewListDialog.kt index b9cbd0a0..96a72cb5 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/sync/SyncViewListDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/sync/SyncViewListDialog.kt @@ -14,7 +14,7 @@ import pl.szczodrzynski.edziennik.MainActivity import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask import pl.szczodrzynski.edziennik.databinding.DialogLessonDetailsBinding -import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesFragment +import pl.szczodrzynski.edziennik.ui.modules.messages.list.MessagesFragment import kotlin.coroutines.CoroutineContext class SyncViewListDialog( 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 2df41c36..5662b1a0 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 @@ -26,10 +26,10 @@ import pl.szczodrzynski.edziennik.data.db.full.LessonFull import pl.szczodrzynski.edziennik.databinding.DialogLessonDetailsBinding import pl.szczodrzynski.edziennik.onClick import pl.szczodrzynski.edziennik.setText -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.attendance.AttendanceDetailsDialog +import pl.szczodrzynski.edziennik.ui.modules.event.EventDetailsDialog +import pl.szczodrzynski.edziennik.ui.modules.event.EventListAdapter +import pl.szczodrzynski.edziennik.ui.modules.event.EventManualDialog import pl.szczodrzynski.edziennik.ui.modules.timetable.TimetableFragment import pl.szczodrzynski.edziennik.utils.BetterLink import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration @@ -228,7 +228,7 @@ class LessonDetailsDialog( ) app.db.eventDao().getAllByDateTime(lesson.profileId, lessonDate, lessonTime).observe(activity, Observer { events -> - adapter.items = events + adapter.setAllItems(events) if (b.eventsView.adapter == null) { b.eventsView.adapter = adapter b.eventsView.apply { 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 8607217a..949e36cc 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 @@ -28,7 +28,7 @@ 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.modules.event.EventManualDialog import pl.szczodrzynski.edziennik.utils.Themes import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem 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 index 9167fb9a..68240e0b 100644 --- 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 @@ -22,7 +22,6 @@ 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 @@ -33,6 +32,7 @@ import pl.szczodrzynski.edziennik.ui.modules.agenda.lessonchanges.LessonChangesE 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.ui.modules.event.EventDetailsDialog import pl.szczodrzynski.edziennik.utils.models.Date import java.util.* diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventDetailsDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/event/EventDetailsDialog.kt similarity index 99% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventDetailsDialog.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/event/EventDetailsDialog.kt index 3c4be7bd..ae848edb 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventDetailsDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/event/EventDetailsDialog.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2019-12-18. */ -package pl.szczodrzynski.edziennik.ui.dialogs.event +package pl.szczodrzynski.edziennik.ui.modules.event import android.content.ActivityNotFoundException import android.content.Intent diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/event/EventListAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/event/EventListAdapter.kt new file mode 100644 index 00000000..40de3145 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/event/EventListAdapter.kt @@ -0,0 +1,71 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-11-30 + */ + +package pl.szczodrzynski.edziennik.ui.modules.event + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.recyclerview.widget.RecyclerView +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.data.db.full.EventFull +import pl.szczodrzynski.edziennik.startCoroutineTimer +import pl.szczodrzynski.edziennik.ui.modules.search.SearchableAdapter +import kotlin.coroutines.CoroutineContext + +class EventListAdapter( + val activity: AppCompatActivity, + val simpleMode: Boolean = false, + val showWeekDay: Boolean = false, + val showDate: Boolean = false, + val showType: Boolean = true, + val showTime: Boolean = true, + val showSubject: Boolean = true, + val markAsSeen: Boolean = true, + isReversed: Boolean = false, + val onItemClick: ((event: EventFull) -> Unit)? = null, + val onEventEditClick: ((event: EventFull) -> Unit)? = null, +) : SearchableAdapter(isReversed), CoroutineScope { + companion object { + private const val TAG = "EventListAdapter" + private const val ITEM_TYPE_EVENT = 0 + } + + private val app = activity.applicationContext as App + private val manager + get() = app.eventManager + + private val job = Job() + override val coroutineContext: CoroutineContext + get() = job + Dispatchers.Main + + override fun getItemViewType(item: EventFull) = ITEM_TYPE_EVENT + + override fun onBindViewHolder( + holder: RecyclerView.ViewHolder, + position: Int, + item: EventFull, + ) { + if (holder !is EventViewHolder) + return + holder.onBind(activity, app, item, position, this) + } + + override fun onCreateViewHolder( + inflater: LayoutInflater, + parent: ViewGroup, + viewType: Int, + ) = EventViewHolder(inflater, parent) + + internal fun notifyItemChanged(model: Any) { + startCoroutineTimer(1000L, 0L) { + val index = items.indexOf(model) + if (index != -1) + notifyItemChanged(index) + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventManualDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/event/EventManualDialog.kt similarity index 99% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventManualDialog.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/event/EventManualDialog.kt index da684520..5385ee96 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventManualDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/event/EventManualDialog.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2019-11-12. */ -package pl.szczodrzynski.edziennik.ui.dialogs.event +package pl.szczodrzynski.edziennik.ui.modules.event import android.view.View import android.widget.Toast diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/event/EventViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/event/EventViewHolder.kt new file mode 100644 index 00000000..8eb4f638 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/event/EventViewHolder.kt @@ -0,0 +1,133 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-10-10. + */ + +package pl.szczodrzynski.edziennik.ui.modules.event + +import android.text.SpannableString +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.isVisible +import androidx.recyclerview.widget.RecyclerView +import com.mikepenz.iconics.utils.buildIconics +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.db.full.EventFull +import pl.szczodrzynski.edziennik.databinding.EventListItemBinding +import pl.szczodrzynski.edziennik.ui.modules.grades.viewholder.BindableViewHolder +import pl.szczodrzynski.edziennik.utils.models.Date +import pl.szczodrzynski.edziennik.utils.models.Week + +class EventViewHolder( + inflater: LayoutInflater, + parent: ViewGroup, + val b: EventListItemBinding = EventListItemBinding.inflate(inflater, parent, false), +) : RecyclerView.ViewHolder(b.root), BindableViewHolder { + companion object { + private const val TAG = "EventViewHolder" + } + + override fun onBind( + activity: AppCompatActivity, + app: App, + item: EventFull, + position: Int, + adapter: EventListAdapter, + ) { + val manager = app.eventManager + + b.root.onClick { + adapter.onItemClick?.invoke(item) + if (!item.seen) { + manager.markAsSeen(item) + } + if (item.showAsUnseen == true) { + item.showAsUnseen = false + adapter.notifyItemChanged(item) + } + } + + val bullet = " • " + val colorHighlight = R.attr.colorControlHighlight.resolveAttr(activity) + + b.simpleMode = adapter.simpleMode + + manager.setEventTopic(b.topic, item, showType = false) + b.topic.text = SpannableString( + adapter.highlightSearchText( + item = item, + text = b.topic.text, + color = colorHighlight + ) + ).buildIconics() + b.topic.maxLines = if (adapter.simpleMode) 2 else 3 + + b.details.text = mutableListOf( + if (adapter.showWeekDay) + Week.getFullDayName(item.date.weekDay) + else null, + if (adapter.showDate) + item.date.getRelativeString(activity, 7) ?: item.date.formattedStringShort + else null, + if (adapter.showType) + item.typeName + else null, + if (adapter.showTime) + item.time?.stringHM ?: app.getString(R.string.event_all_day) + else null, + if (adapter.showSubject) + adapter.highlightSearchText( + item = item, + text = item.subjectLongName ?: "", + color = colorHighlight + ) + else null, + ).concat(bullet) + + val addedBy = item.sharedByName ?: item.teacherName ?: "" + b.addedBy.setText( + when (item.sharedBy) { + null -> when { + item.addedManually -> R.string.event_list_added_by_self_format + item.teacherName == null -> R.string.event_list_added_by_unknown_format + else -> R.string.event_list_added_by_format + } + "self" -> R.string.event_list_shared_by_self_format + else -> R.string.event_list_shared_by_format + }, + /* 1$ */ + Date.fromMillis(item.addedDate).formattedString, + /* 2$ */ + addedBy, + /* 3$ */ + item.teamName?.let { bullet + it } ?: "", + ) + val addedBySpanned = adapter.highlightSearchText( + item = item, + text = addedBy, + color = colorHighlight + ) + b.addedBy.text = SpannableString( + b.addedBy.text.replace(addedBy, addedBySpanned) + ).buildIconics() + + b.attachmentIcon.isVisible = item.hasAttachments + + b.typeColor.background?.setTintColor(item.eventColor) + b.typeColor.isVisible = adapter.showType + + b.editButton.isVisible = !adapter.simpleMode && item.addedManually && !item.isDone + b.editButton.onClick { + adapter.onEventEditClick?.invoke(item) + } + b.editButton.attachToastHint(R.string.hint_edit_event) + + if (item.showAsUnseen == null) + item.showAsUnseen = !item.seen + + b.unread.isVisible = item.showAsUnseen == true + if (adapter.markAsSeen && !item.seen) { + manager.markAsSeen(item) + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/cards/HomeEventsCard.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/cards/HomeEventsCard.kt index 9a831c0f..f92e86b8 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/cards/HomeEventsCard.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/cards/HomeEventsCard.kt @@ -22,9 +22,9 @@ import pl.szczodrzynski.edziennik.data.db.entity.Profile import pl.szczodrzynski.edziennik.databinding.CardHomeEventsBinding import pl.szczodrzynski.edziennik.dp import pl.szczodrzynski.edziennik.onClick -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.event.EventDetailsDialog +import pl.szczodrzynski.edziennik.ui.modules.event.EventListAdapter +import pl.szczodrzynski.edziennik.ui.modules.event.EventManualDialog import pl.szczodrzynski.edziennik.ui.modules.home.HomeCard import pl.szczodrzynski.edziennik.ui.modules.home.HomeCardAdapter import pl.szczodrzynski.edziennik.ui.modules.home.HomeFragment @@ -82,7 +82,7 @@ class HomeEventsCard( ) app.db.eventDao().getNearestNotDone(profile.id, Date.getToday(), 4).observe(activity, Observer { events -> - adapter.items = events + adapter.setAllItems(events) if (b.eventsView.adapter == null) { b.eventsView.adapter = adapter b.eventsView.apply { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/homework/HomeworkFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/homework/HomeworkFragment.kt index c8a62def..54763fe7 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/homework/HomeworkFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/homework/HomeworkFragment.kt @@ -20,8 +20,8 @@ import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.data.db.entity.Event import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.databinding.HomeworkFragmentBinding -import pl.szczodrzynski.edziennik.ui.dialogs.event.EventManualDialog import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.FragmentLazyPagerAdapter +import pl.szczodrzynski.edziennik.ui.modules.event.EventManualDialog import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetSeparatorItem import kotlin.coroutines.CoroutineContext diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/homework/HomeworkListFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/homework/HomeworkListFragment.kt index 1852c340..8c1e8b7e 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/homework/HomeworkListFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/homework/HomeworkListFragment.kt @@ -13,10 +13,10 @@ import kotlinx.coroutines.Job import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.data.db.entity.Event import pl.szczodrzynski.edziennik.databinding.HomeworkListFragmentBinding -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.base.lazypager.LazyFragment +import pl.szczodrzynski.edziennik.ui.modules.event.EventDetailsDialog +import pl.szczodrzynski.edziennik.ui.modules.event.EventListAdapter +import pl.szczodrzynski.edziennik.ui.modules.event.EventManualDialog import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration import pl.szczodrzynski.edziennik.utils.models.Date import kotlin.coroutines.CoroutineContext @@ -54,57 +54,57 @@ class HomeworkListFragment : LazyFragment(), CoroutineScope { } val adapter = EventListAdapter( - activity, - showWeekDay = true, - showDate = true, - showType = false, - showTime = true, - showSubject = true, - markAsSeen = true, - onItemClick = { - EventDetailsDialog( - activity, - it - ) - }, - onEventEditClick = { - EventManualDialog( - activity, - it.profileId, - editingEvent = it - ) - } + activity, + showWeekDay = true, + showDate = true, + showType = false, + showTime = true, + showSubject = true, + markAsSeen = true, + isReversed = homeworkDate == HomeworkDate.PAST, + onItemClick = { + EventDetailsDialog( + activity, + it + ) + }, + onEventEditClick = { + EventManualDialog( + activity, + it.profileId, + editingEvent = it + ) + } ) - app.db.eventDao().getAllByType(App.profileId, Event.TYPE_HOMEWORK, filter).observe(this@HomeworkListFragment, Observer { items -> + app.db.eventDao().getAllByType(App.profileId, Event.TYPE_HOMEWORK, filter).observe(this@HomeworkListFragment, Observer { events -> if (!isAdded) return@Observer - // load & configure the adapter - adapter.items = items - if (items.isNotNullNorEmpty() && b.list.adapter == null) { - b.list.adapter = adapter + // show/hide relevant views + setSwipeToRefresh(events.isEmpty()) + b.progressBar.isVisible = false + b.list.isVisible = events.isNotEmpty() + b.noData.isVisible = events.isEmpty() + if (events.isEmpty()) { + return@Observer + } + + // apply the new event list + adapter.setAllItems(events, addSearchField = true) + + // configure the adapter & recycler view + if (b.list.adapter == null) { b.list.apply { setHasFixedSize(true) - layoutManager = LinearLayoutManager(context).apply { - reverseLayout = homeworkDate == HomeworkDate.PAST - stackFromEnd = homeworkDate == HomeworkDate.PAST - } + layoutManager = LinearLayoutManager(context) addItemDecoration(SimpleDividerItemDecoration(context)) addOnScrollListener(onScrollListener) + this.adapter = adapter } } - adapter.notifyDataSetChanged() - setSwipeToRefresh(items.isNullOrEmpty()) - // show/hide relevant views - b.progressBar.isVisible = false - if (items.isNullOrEmpty()) { - b.list.isVisible = false - b.noData.isVisible = true - } else { - b.list.isVisible = true - b.noData.isVisible = false - } + // reapply the filter + adapter.getSearchField()?.applyTo(adapter) }) }; return true } } 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 deleted file mode 100644 index 16faa588..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesAdapter.kt +++ /dev/null @@ -1,86 +0,0 @@ -package pl.szczodrzynski.edziennik.ui.modules.messages - -import android.graphics.Typeface -import android.view.LayoutInflater -import android.view.ViewGroup -import android.widget.Filterable -import androidx.appcompat.app.AppCompatActivity -import androidx.recyclerview.widget.RecyclerView -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import pl.szczodrzynski.edziennik.App -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 kotlin.coroutines.CoroutineContext - -class MessagesAdapter( - val activity: AppCompatActivity, - val teachers: List, - val onItemClick: ((item: MessageFull) -> Unit)? = null, - val onStarClick: ((item: MessageFull) -> Unit)? = null, -) : RecyclerView.Adapter(), CoroutineScope, Filterable { - companion object { - private const val TAG = "MessagesAdapter" - private const val ITEM_TYPE_MESSAGE = 0 - private const val ITEM_TYPE_SEARCH = 1 - } - - private val app = activity.applicationContext as App - // optional: place the manager here - internal val manager - get() = app.messageManager - - private val job = Job() - override val coroutineContext: CoroutineContext - get() = job + Dispatchers.Main - - // mutable var changed by the filter - var items = listOf() - // mutable list managed by the fragment - val allItems = mutableListOf() - val typefaceNormal: Typeface by lazy { Typeface.create(Typeface.DEFAULT, Typeface.NORMAL) } - val typefaceBold: Typeface by lazy { Typeface.create(Typeface.DEFAULT, Typeface.BOLD) } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - val inflater = LayoutInflater.from(parent.context) - return when (viewType) { - ITEM_TYPE_MESSAGE -> MessageViewHolder(inflater, parent) - ITEM_TYPE_SEARCH -> SearchViewHolder(inflater, parent) - else -> throw IllegalArgumentException("Incorrect viewType") - } - } - - override fun getItemViewType(position: Int): Int { - return when (items[position]) { - is MessageFull -> ITEM_TYPE_MESSAGE - is MessagesSearch -> ITEM_TYPE_SEARCH - else -> throw IllegalArgumentException("Incorrect viewType") - } - } - - @Suppress("DEPRECATION") - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - val item = items[position] - if (holder !is BindableViewHolder<*, *>) - 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) - } - } - - private val messagesFilter by lazy { - MessagesFilter(this) - } - override fun getItemCount() = items.size - override fun getFilter() = messagesFilter -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesUtils.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesUtils.kt index 07a0769a..d61604d8 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesUtils.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesUtils.kt @@ -9,7 +9,6 @@ import android.text.Spanned import androidx.core.graphics.ColorUtils import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.R -import pl.szczodrzynski.edziennik.data.db.entity.Message import pl.szczodrzynski.edziennik.data.db.full.MessageFull import pl.szczodrzynski.edziennik.fixName import pl.szczodrzynski.edziennik.getNameInitials @@ -123,10 +122,10 @@ object MessagesUtils { fun getMessageInfo(app: App, message: MessageFull, diameterDp: Int, textSizeBigDp: Int, textSizeMediumDp: Int, textSizeSmallDp: Int): MessageInfo { var profileImage: Bitmap? = null var profileName: String? = null - if (message.type == Message.TYPE_RECEIVED || message.type == Message.TYPE_DELETED) { + if (message.isReceived || message.isDeleted) { profileName = message.senderName?.fixName() profileImage = getProfileImage(diameterDp, textSizeBigDp, textSizeMediumDp, textSizeSmallDp, 1, profileName) - } else if (message.type == Message.TYPE_SENT || message.type == Message.TYPE_DRAFT && message.recipients != null) { + } else if (message.isSent || message.isDraft && message.recipients != null) { when (val count = message.recipients?.size ?: 0) { 0 -> { profileName = app.getString(R.string.messages_draft_title) 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 fcba191b..bd468ea7 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 @@ -37,7 +37,7 @@ import pl.szczodrzynski.edziennik.data.db.entity.Message import pl.szczodrzynski.edziennik.data.db.entity.Teacher import pl.szczodrzynski.edziennik.databinding.MessagesComposeFragmentBinding import pl.szczodrzynski.edziennik.ui.dialogs.MessagesConfigDialog -import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesFragment +import pl.szczodrzynski.edziennik.ui.modules.messages.list.MessagesFragment import pl.szczodrzynski.edziennik.utils.Themes import pl.szczodrzynski.edziennik.utils.managers.MessageManager.UIConfig import pl.szczodrzynski.edziennik.utils.managers.TextStylingManager.StylingConfig @@ -398,7 +398,7 @@ class MessagesComposeFragment : Fragment(), CoroutineScope { b.recipients.setAdapter(adapter) val message = manager.fillWithBundle(uiConfig, arguments) - if (message != null && message.type == Message.TYPE_DRAFT) { + if (message != null && message.isDraft) { draftMessageId = message.id if (discardDraftItem != null) activity.bottomSheet.addItemAt(2, discardDraftItem!!) 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/list/MessageViewHolder.kt similarity index 64% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/viewholder/MessageViewHolder.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/list/MessageViewHolder.kt index f59bee59..230992c2 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/list/MessageViewHolder.kt @@ -2,29 +2,24 @@ * Copyright (c) Kuba Szczodrzyński 2020-4-5. */ -package pl.szczodrzynski.edziennik.ui.modules.messages.viewholder +package pl.szczodrzynski.edziennik.ui.modules.messages.list -import android.graphics.Typeface -import android.text.style.BackgroundColorSpan -import android.text.style.StyleSpan import android.view.LayoutInflater import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView import pl.szczodrzynski.edziennik.* -import pl.szczodrzynski.edziennik.data.db.entity.Message import pl.szczodrzynski.edziennik.data.db.full.MessageFull import pl.szczodrzynski.edziennik.databinding.MessagesListItemBinding import pl.szczodrzynski.edziennik.ui.modules.grades.viewholder.BindableViewHolder -import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesAdapter 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) + val b: MessagesListItemBinding = MessagesListItemBinding.inflate(inflater, parent, false), ) : RecyclerView.ViewHolder(b.root), BindableViewHolder { companion object { private const val TAG = "MessageViewHolder" @@ -35,16 +30,14 @@ class MessageViewHolder( app: App, item: MessageFull, position: Int, - adapter: MessagesAdapter + adapter: MessagesAdapter, ) { - b.messageSubject.text = item.subject b.messageDate.text = Date.fromMillis(item.addedDate).formattedStringShort b.messageAttachmentImage.isVisible = item.hasAttachments - val text = item.body?.take(200) ?: "" - b.messageBody.text = MessagesUtils.htmlToSpannable(activity, text) + b.messageBody.text = item.bodyHtml?.take(200) - val isRead = item.type == Message.TYPE_SENT || item.type == Message.TYPE_DRAFT || item.seen + val isRead = item.isSent || item.isDraft || item.seen val typeface = if (isRead) adapter.typefaceNormal else adapter.typefaceBold val style = if (isRead) R.style.NavView_TextView_Small else R.style.NavView_TextView_Normal // set text styles @@ -62,20 +55,18 @@ class MessageViewHolder( val messageInfo = MessagesUtils.getMessageInfo(app, item, 48, 24, 18, 12) b.messageProfileBackground.setImageBitmap(messageInfo.profileImage) - b.messageSender.text = messageInfo.profileName - 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 - ) - b.messageSender.text = b.messageSender.text.asSpannable( - StyleSpan(Typeface.BOLD), BackgroundColorSpan(colorHighlight), - substring = highlight, ignoreCase = true, ignoreDiacritics = true - ) - } + val colorHighlight = R.attr.colorControlHighlight.resolveAttr(activity) + b.messageSubject.text = adapter.highlightSearchText( + item = item, + text = item.subject, + color = colorHighlight + ) + b.messageSender.text = adapter.highlightSearchText( + item = item, + text = messageInfo.profileName ?: "", + color = colorHighlight + ) adapter.onItemClick?.let { listener -> b.root.onClick { listener(item) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/list/MessagesAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/list/MessagesAdapter.kt new file mode 100644 index 00000000..1061ac86 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/list/MessagesAdapter.kt @@ -0,0 +1,50 @@ +package pl.szczodrzynski.edziennik.ui.modules.messages.list + +import android.graphics.Typeface +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.recyclerview.widget.RecyclerView +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.data.db.entity.Teacher +import pl.szczodrzynski.edziennik.data.db.full.MessageFull +import pl.szczodrzynski.edziennik.ui.modules.search.SearchableAdapter + +class MessagesAdapter( + val activity: AppCompatActivity, + val teachers: List, + val onItemClick: ((item: MessageFull) -> Unit)? = null, + val onStarClick: ((item: MessageFull) -> Unit)? = null, +) : SearchableAdapter() { + companion object { + private const val TAG = "MessagesAdapter" + private const val ITEM_TYPE_MESSAGE = 0 + } + + private val app = activity.applicationContext as App + + // optional: place the manager here + internal val manager + get() = app.messageManager + + val typefaceNormal: Typeface by lazy { Typeface.create(Typeface.DEFAULT, Typeface.NORMAL) } + val typefaceBold: Typeface by lazy { Typeface.create(Typeface.DEFAULT, Typeface.BOLD) } + + override fun getItemViewType(item: MessageFull) = ITEM_TYPE_MESSAGE + + override fun onBindViewHolder( + holder: RecyclerView.ViewHolder, + position: Int, + item: MessageFull, + ) { + if (holder !is MessageViewHolder) + return + holder.onBind(activity, app, item, position, this) + } + + override fun onCreateViewHolder( + inflater: LayoutInflater, + parent: ViewGroup, + viewType: Int, + ) = MessageViewHolder(inflater, parent) +} 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/list/MessagesFragment.kt similarity index 98% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesFragment.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/list/MessagesFragment.kt index d35ec67a..faf0e3ee 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/list/MessagesFragment.kt @@ -1,4 +1,4 @@ -package pl.szczodrzynski.edziennik.ui.modules.messages +package pl.szczodrzynski.edziennik.ui.modules.messages.list import android.os.Bundle import android.view.LayoutInflater 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/list/MessagesListFragment.kt similarity index 83% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesListFragment.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/list/MessagesListFragment.kt index 314aee31..19190e58 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/list/MessagesListFragment.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2020-4-4. */ -package pl.szczodrzynski.edziennik.ui.modules.messages +package pl.szczodrzynski.edziennik.ui.modules.messages.list import android.os.Bundle import android.view.LayoutInflater @@ -17,10 +17,8 @@ import pl.szczodrzynski.edziennik.MainActivity.Companion.TARGET_MESSAGES_COMPOSE import pl.szczodrzynski.edziennik.MainActivity.Companion.TARGET_MESSAGES_DETAILS 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.databinding.MessagesListFragmentBinding import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyFragment -import pl.szczodrzynski.edziennik.ui.modules.messages.models.MessagesSearch import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration import kotlin.coroutines.CoroutineContext @@ -63,7 +61,7 @@ class MessagesListFragment : LazyFragment(), CoroutineScope { adapter = MessagesAdapter(activity, teachers, onItemClick = { val (target, args) = - if (it.type == Message.TYPE_DRAFT) { + if (it.isDraft) { TARGET_MESSAGES_COMPOSE to Bundle("message" to app.gson.toJson(it)) } else { TARGET_MESSAGES_DETAILS to Bundle("messageId" to it.id) @@ -98,17 +96,8 @@ class MessagesListFragment : LazyFragment(), CoroutineScope { return@Observer } - if (adapter.allItems.isEmpty()) { - // items empty - add the search field - adapter.allItems += MessagesSearch().also { - it.searchText = searchText ?: "" - } - } else { - // items not empty - remove all messages - adapter.allItems.removeAll { it is MessageFull } - } - // add all messages - adapter.allItems.addAll(messages) + // apply the new message list + adapter.setAllItems(messages, searchText, addSearchField = true) // configure the adapter & recycler view if (b.list.adapter == null) { @@ -125,8 +114,7 @@ class MessagesListFragment : LazyFragment(), CoroutineScope { val layoutManager = (b.list.layoutManager as? LinearLayoutManager) ?: return@Observer // reapply the filter - val searchItem = adapter.items.firstOrNull { it is MessagesSearch } as? MessagesSearch - adapter.filter.filter(searchText ?: searchItem?.searchText) { + adapter.getSearchField()?.applyTo(adapter) { // restore the previously saved scroll position recyclerViewState?.let { layoutManager.onRestoreInstanceState(it) @@ -141,11 +129,11 @@ class MessagesListFragment : LazyFragment(), CoroutineScope { if (!isAdded || !this::adapter.isInitialized) return val layoutManager = (b.list.layoutManager as? LinearLayoutManager) - val searchItem = adapter.items.firstOrNull { it is MessagesSearch } as? MessagesSearch + val searchField = adapter.getSearchField() onPageDestroy?.invoke(position, Bundle( "recyclerViewState" to layoutManager?.onSaveInstanceState(), - "searchText" to searchItem?.searchText?.toString() + "searchText" to searchField?.searchText?.toString() )) } } 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 deleted file mode 100644 index 71ed0486..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/models/MessagesSearch.kt +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright (c) Kuba Szczodrzyński 2020-4-5. - */ - -package pl.szczodrzynski.edziennik.ui.modules.messages.models - -class MessagesSearch { - var searchText: CharSequence = "" -} 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/single/MessageFragment.kt similarity index 95% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessageFragment.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/single/MessageFragment.kt index 430fe0f0..ad86f287 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/single/MessageFragment.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2019-11-12. */ -package pl.szczodrzynski.edziennik.ui.modules.messages +package pl.szczodrzynski.edziennik.ui.modules.messages.single import android.os.Bundle import android.text.Html @@ -25,11 +25,11 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask import pl.szczodrzynski.edziennik.data.api.events.MessageGetEvent import pl.szczodrzynski.edziennik.data.db.entity.LoginStore import pl.szczodrzynski.edziennik.data.db.entity.LoginStore.Companion.LOGIN_TYPE_IDZIENNIK -import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_DELETED -import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_RECEIVED 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.ui.modules.messages.MessagesUtils +import pl.szczodrzynski.edziennik.ui.modules.messages.list.MessagesFragment import pl.szczodrzynski.edziennik.utils.Anim import pl.szczodrzynski.edziennik.utils.BetterLink import pl.szczodrzynski.edziennik.utils.models.Date @@ -188,7 +188,7 @@ class MessageFragment : Fragment(), CoroutineScope { if (app.profile.loginStoreType == LoginStore.LOGIN_TYPE_VULCAN) { // vulcan: change message status or download attachments - if (message.type == TYPE_RECEIVED && !message.seen || message.attachmentIds == null) { + if ((message.isReceived || message.isDeleted) && !message.seen || message.attachmentIds == null) { EdziennikTask.messageGet(App.profileId, message).enqueue(activity) return } @@ -214,9 +214,9 @@ class MessageFragment : Fragment(), CoroutineScope { manager.setStarIcon(b.messageStar, message) - b.replyButton.isVisible = message.type == TYPE_RECEIVED || message.type == TYPE_DELETED - b.deleteButton.isVisible = message.type == TYPE_RECEIVED - if (message.type == TYPE_RECEIVED || message.type == TYPE_DELETED) { + b.replyButton.isVisible = message.isReceived || message.isDeleted + b.deleteButton.isVisible = message.isReceived + if (message.isReceived || message.isDeleted) { activity.navView.apply { bottomBar.apply { fabEnable = true 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 deleted file mode 100644 index 43497aa1..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/utils/MessagesComparator.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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 { - // descending sorting (1. true, 2. false) - o1.isStarred && !o2.isStarred -> -1 - !o1.isStarred && o2.isStarred -> 1 - // ascending sorting - o1.filterWeight > o2.filterWeight -> 1 - o1.filterWeight < o2.filterWeight -> -1 - // descending 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/search/SearchField.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/search/SearchField.kt new file mode 100644 index 00000000..00f7b49d --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/search/SearchField.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-4-5. + */ + +package pl.szczodrzynski.edziennik.ui.modules.search + +import android.widget.Filter + +class SearchField( + var searchText: CharSequence = "", +) : Searchable { + + override val searchKeywords = emptyList>() + override var searchPriority = 0 + override var searchHighlightText: String? = null + override fun compareTo(other: Searchable<*>) = 0 + + fun applyTo(adapter: SearchableAdapter<*>, listener: Filter.FilterListener? = null) { + adapter.filter.filter(searchText, listener) + } +} 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/search/SearchFilter.kt similarity index 52% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/utils/MessagesFilter.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/search/SearchFilter.kt index 202db386..ffbcbf45 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/utils/MessagesFilter.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/search/SearchFilter.kt @@ -2,24 +2,20 @@ * Copyright (c) Kuba Szczodrzyński 2021-4-14. */ -package pl.szczodrzynski.edziennik.ui.modules.messages.utils +package pl.szczodrzynski.edziennik.ui.modules.search 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 +class SearchFilter>( + private val adapter: SearchableAdapter, ) : Filter() { companion object { private const val NO_MATCH = 1000 } - private val comparator = MessagesComparator() private var prevCount = -1 private val allItems @@ -54,75 +50,58 @@ class MessagesFilter( if (prefix.isNullOrBlank()) { allItems.forEach { - if (it is MessageFull) - it.searchHighlightText = null + it.searchPriority = NO_MATCH + 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 + val newItems = allItems.mapNotNull { item -> + if (item is SearchField) { + return@mapNotNull item } - it.filterWeight = NO_MATCH - it.searchHighlightText = null + item.searchPriority = NO_MATCH + item.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) + // get all keyword sets from the entity + val searchKeywords = item.searchKeywords + // a temporary variable for the loops below + var matchWeight: Int + + searchKeywords.forEachIndexed { priority, keywords -> + keywords ?: return@forEachIndexed + keywords.forEach { keyword -> + matchWeight = getMatchWeight(keyword, prefix) + if (matchWeight != NO_MATCH) { + // a match not at the word start boundary should be least prioritized + if (matchWeight == 3) + matchWeight = 100 + item.searchPriority = min(item.searchPriority, priority * 10 + matchWeight) } } - } 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) + if (item.searchPriority != NO_MATCH) { + // the adapter is reversed, the search priority also should be + if (adapter.isReversed) + item.searchPriority *= -1 + item.searchHighlightText = prefix.toString() + return@mapNotNull item } + return@mapNotNull null } - Collections.sort(items, comparator) - results.values = items - results.count = items.size + results.values = newItems.sorted() + results.count = newItems.size return results } override fun publishResults(constraint: CharSequence?, results: FilterResults) { results.values?.let { - adapter.items = it as MutableList + @Suppress("UNCHECKED_CAST") // yes I know it's checked. + adapter.setFilteredItems(it as List) } // do not re-bind the search box val count = results.count - 1 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/search/SearchTextWatcher.kt similarity index 75% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/utils/SearchTextWatcher.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/search/SearchTextWatcher.kt index 7103ff29..f37b1760 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/utils/SearchTextWatcher.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/search/SearchTextWatcher.kt @@ -2,18 +2,17 @@ * Copyright (c) Kuba Szczodrzyński 2021-4-14. */ -package pl.szczodrzynski.edziennik.ui.modules.messages.utils +package pl.szczodrzynski.edziennik.ui.modules.search 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 +import pl.szczodrzynski.edziennik.databinding.SearchItemBinding class SearchTextWatcher( - private val b: MessagesListItemSearchBinding, - private val filter: MessagesFilter, - private val item: MessagesSearch + private val b: SearchItemBinding, + private val filter: SearchFilter<*>, + private val item: SearchField, ) : TextWatcher { override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit 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/search/SearchViewHolder.kt similarity index 50% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/viewholder/SearchViewHolder.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/search/SearchViewHolder.kt index a0a7d69c..06a38ab7 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/search/SearchViewHolder.kt @@ -2,40 +2,28 @@ * Copyright (c) Kuba Szczodrzyński 2020-4-5. */ -package pl.szczodrzynski.edziennik.ui.modules.messages.viewholder +package pl.szczodrzynski.edziennik.ui.modules.search import android.view.LayoutInflater 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 +import pl.szczodrzynski.edziennik.databinding.SearchItemBinding class SearchViewHolder( inflater: LayoutInflater, parent: ViewGroup, - val b: MessagesListItemSearchBinding = MessagesListItemSearchBinding.inflate( + val b: SearchItemBinding = SearchItemBinding.inflate( inflater, parent, false - ) -) : RecyclerView.ViewHolder(b.root), BindableViewHolder { + ), +) : RecyclerView.ViewHolder(b.root) { companion object { private const val TAG = "SearchViewHolder" } - override fun onBind( - activity: AppCompatActivity, - app: App, - item: MessagesSearch, - position: Int, - adapter: MessagesAdapter - ) { + internal fun bind(item: SearchField, adapter: SearchableAdapter<*>) { val watcher = SearchTextWatcher(b, adapter.filter, item) b.searchEdit.removeTextChangedListener(watcher) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/search/Searchable.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/search/Searchable.kt new file mode 100644 index 00000000..072d6db5 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/search/Searchable.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-10-10. + */ + +package pl.szczodrzynski.edziennik.ui.modules.search + +interface Searchable : Comparable> { + + /** + * A prioritized list of keywords sets. First items are of the highest priority. + * Items within a keyword set have the same priority. + */ + val searchKeywords: List?> + + /** + * A priority assigned by [SearchFilter]. Lower numbers mean a higher priority. + */ + var searchPriority: Int + + /** + * The text to be highlighted when filtering. + */ + var searchHighlightText: String? +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/search/SearchableAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/search/SearchableAdapter.kt new file mode 100644 index 00000000..7f9cc103 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/search/SearchableAdapter.kt @@ -0,0 +1,133 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-10-10. + */ + +package pl.szczodrzynski.edziennik.ui.modules.search + +import android.text.SpannableStringBuilder +import android.text.style.BackgroundColorSpan +import android.view.LayoutInflater +import android.view.ViewGroup +import android.widget.Filterable +import androidx.recyclerview.widget.RecyclerView +import pl.szczodrzynski.edziennik.asSpannable +import pl.szczodrzynski.edziennik.utils.span.BoldSpan + +abstract class SearchableAdapter>( + val isReversed: Boolean = false, +) : RecyclerView.Adapter(), Filterable { + companion object { + const val ITEM_TYPE_SEARCH = 2137 + } + + /** + * A mutable list managed by [setAllItems]. + * Items are never displayed straight from this list. + * Items in this list are always sorted according to their + * natural order, with the [SearchField] preceding any other. + */ + val allItems = mutableListOf() + + /** + * A mutable var changed by the [SearchFilter]. + * This list is the only direct source of displayed items. + * Items in this list may be in reverse order ([isReversed]), with the [SearchField] + * still as the first item. + */ + var items = listOf() + private set + + /** + * Set [items] as the currently displayed item list. The [items] are first + * sorted appropriately to the [isReversed] property. + */ + internal fun setFilteredItems(items: List) { + this.items = if (isReversed) + items.sortedDescending() // the sort is stable - SearchField should stay at the top + else + items.sorted() + } + + /** + * Put [items] to the sorted, unfiltered data source. + * + * @param searchText the text to fill the [SearchField] with, by default + * @param addSearchField whether searching should be enabled and visible + */ + fun setAllItems(items: List, searchText: String? = null, addSearchField: Boolean = false) { + if (allItems.isEmpty()) { + // items empty - add the search field + if (addSearchField) { + @Suppress("UNCHECKED_CAST") // what ??? + allItems += SearchField(searchText ?: "") as T + } + } else { + // items not empty - remove all except the search field + allItems.removeAll { it !is SearchField } + } + // add all new items + allItems.addAll(items.sorted()) + // show all items if searching is disabled + if (!addSearchField) { + setFilteredItems(allItems) + } + } + + /** + * Return the search field in this adapter's list, or null if not found. + */ + fun getSearchField(): SearchField? { + return allItems.filterIsInstance().firstOrNull() + } + + fun highlightSearchText(item: T, text: CharSequence, color: Int): CharSequence { + if (item.searchHighlightText == null) + return SpannableStringBuilder(text) + return text.asSpannable( + BoldSpan(), + BackgroundColorSpan(color), + substring = item.searchHighlightText, + ignoreCase = true, + ignoreDiacritics = true, + ) + } + + final override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int, + ): RecyclerView.ViewHolder { + val inflater = LayoutInflater.from(parent.context) + return when (viewType) { + ITEM_TYPE_SEARCH -> SearchViewHolder(inflater, parent) + else -> onCreateViewHolder(inflater, parent, viewType) + } + } + + final override fun getItemViewType(position: Int): Int { + return when (val item = items[position]) { + is SearchField -> ITEM_TYPE_SEARCH + else -> getItemViewType(item) + } + } + + final override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + val item = items[position] + if (holder is SearchViewHolder && item is SearchField) { + holder.bind(item, this) + } else { + onBindViewHolder(holder, position, item) + } + } + + abstract fun getItemViewType(item: T): Int + abstract fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int, item: T) + abstract fun onCreateViewHolder( + inflater: LayoutInflater, + parent: ViewGroup, + viewType: Int, + ): RecyclerView.ViewHolder + + private val filter = SearchFilter(this) + override fun getItemCount() = items.size + override fun getFilter() = filter +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/TimetableFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/TimetableFragment.kt index 85332fe1..13d3ae2c 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/TimetableFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/TimetableFragment.kt @@ -26,8 +26,8 @@ import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.databinding.FragmentTimetableV2Binding import pl.szczodrzynski.edziennik.getSchoolYearConstrains -import pl.szczodrzynski.edziennik.ui.dialogs.event.EventManualDialog import pl.szczodrzynski.edziennik.ui.dialogs.timetable.GenerateBlockTimetableDialog +import pl.szczodrzynski.edziennik.ui.modules.event.EventManualDialog import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetSeparatorItem 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 f5b7faa9..6594aa01 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,6 +4,7 @@ package pl.szczodrzynski.edziennik.utils.managers +import android.widget.TextView import androidx.core.view.isVisible import com.mikepenz.iconics.IconicsDrawable import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial @@ -39,7 +40,7 @@ class EventManager(val app: App) : CoroutineScope { } fun setEventTopic( - title: IconicsTextView, + title: TextView, event: EventFull, showType: Boolean = true, doneIconColor: Int? = null diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/MessageManager.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/MessageManager.kt index 600353a6..c72a770f 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/MessageManager.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/MessageManager.kt @@ -95,12 +95,12 @@ class MessageManager(private val app: App) { recipient.fullName = teachers.firstOrNull { it.id == recipient.id }?.fullName ?: "" // unset the readByEveryone flag - if (recipient.readDate < 1 && message.type == Message.TYPE_SENT) + if (recipient.readDate < 1 && message.isSent) message.readByEveryone = false } // store the account name as sender for sent messages - if (message.type == Message.TYPE_SENT && message.senderName == null) { + if (message.isSent && message.senderName == null) { message.senderName = app.profile.accountName ?: app.profile.studentNameLong } @@ -193,7 +193,7 @@ class MessageManager(private val app: App) { else null when { - message != null && message.type == Message.TYPE_DRAFT -> { + message != null && message.isDraft -> { fillWithDraftMessage(config, message) } message != null -> { diff --git a/app/src/main/res/layout/event_list_item.xml b/app/src/main/res/layout/event_list_item.xml index a69b74c0..e353c837 100644 --- a/app/src/main/res/layout/event_list_item.xml +++ b/app/src/main/res/layout/event_list_item.xml @@ -2,12 +2,14 @@ - - + - + + + @@ -16,9 +18,9 @@ + android:padding="8dp"> + + - + android:visibility="gone" + tools:visibility="visible" /> - - - diff --git a/app/src/main/res/layout/messages_list_item.xml b/app/src/main/res/layout/messages_list_item.xml index fc38ad0d..ab365ba0 100644 --- a/app/src/main/res/layout/messages_list_item.xml +++ b/app/src/main/res/layout/messages_list_item.xml @@ -114,13 +114,10 @@