mirror of
https://github.com/szkolny-eu/szkolny-android.git
synced 2025-01-18 12:56:45 -06:00
[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.
This commit is contained in:
parent
50ae767fcd
commit
1a543814f4
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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="
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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<Long>? = null
|
||||
var attachmentNames: MutableList<String>? = null
|
||||
|
||||
|
@ -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<Long>? = null
|
||||
|
@ -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<EventFull> {
|
||||
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
|
||||
|
@ -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<MessageFull> {
|
||||
var senderName: String? = null
|
||||
@Relation(parentColumn = "messageId", entityColumn = "messageId", entity = MessageRecipient::class)
|
||||
var recipients: MutableList<MessageRecipientFull>? = 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
|
||||
|
@ -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)
|
||||
adapter.setAllItems(
|
||||
if (eventTypeId != null)
|
||||
events.filter { it.type == eventTypeId }
|
||||
else
|
||||
events
|
||||
events,
|
||||
)
|
||||
|
||||
if (b.eventsView.adapter == null) {
|
||||
b.eventsView.adapter = adapter
|
||||
|
@ -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<EventListAdapter.ViewHolder>(), 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<EventFull>()
|
||||
|
||||
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<CharSequence?>(
|
||||
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)
|
||||
}
|
@ -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(
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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.*
|
||||
|
||||
|
@ -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
|
@ -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<EventFull>(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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
@ -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<EventFull, EventListAdapter> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
@ -61,6 +61,7 @@ class HomeworkListFragment : LazyFragment(), CoroutineScope {
|
||||
showTime = true,
|
||||
showSubject = true,
|
||||
markAsSeen = true,
|
||||
isReversed = homeworkDate == HomeworkDate.PAST,
|
||||
onItemClick = {
|
||||
EventDetailsDialog(
|
||||
activity,
|
||||
@ -76,35 +77,34 @@ class HomeworkListFragment : LazyFragment(), CoroutineScope {
|
||||
}
|
||||
)
|
||||
|
||||
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 }
|
||||
}
|
||||
|
@ -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<Teacher>,
|
||||
val onItemClick: ((item: MessageFull) -> Unit)? = null,
|
||||
val onStarClick: ((item: MessageFull) -> Unit)? = null,
|
||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>(), 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<Any>()
|
||||
// mutable list managed by the fragment
|
||||
val allItems = mutableListOf<Any>()
|
||||
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
|
||||
}
|
@ -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)
|
||||
|
@ -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!!)
|
||||
|
@ -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<MessageFull, MessagesAdapter> {
|
||||
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.messageSubject.text = adapter.highlightSearchText(
|
||||
item = item,
|
||||
text = item.subject,
|
||||
color = colorHighlight
|
||||
)
|
||||
b.messageSender.text = b.messageSender.text.asSpannable(
|
||||
StyleSpan(Typeface.BOLD), BackgroundColorSpan(colorHighlight),
|
||||
substring = highlight, ignoreCase = true, ignoreDiacritics = true
|
||||
b.messageSender.text = adapter.highlightSearchText(
|
||||
item = item,
|
||||
text = messageInfo.profileName ?: "",
|
||||
color = colorHighlight
|
||||
)
|
||||
}
|
||||
|
||||
adapter.onItemClick?.let { listener ->
|
||||
b.root.onClick { listener(item) }
|
@ -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<Teacher>,
|
||||
val onItemClick: ((item: MessageFull) -> Unit)? = null,
|
||||
val onStarClick: ((item: MessageFull) -> Unit)? = null,
|
||||
) : SearchableAdapter<MessageFull>() {
|
||||
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)
|
||||
}
|
@ -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
|
@ -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()
|
||||
))
|
||||
}
|
||||
}
|
@ -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 = ""
|
||||
}
|
@ -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
|
@ -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<Any> {
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
@ -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<SearchField> {
|
||||
|
||||
override val searchKeywords = emptyList<List<String>>()
|
||||
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)
|
||||
}
|
||||
}
|
@ -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<T : Searchable<T>>(
|
||||
private val adapter: SearchableAdapter<T>,
|
||||
) : Filter() {
|
||||
companion object {
|
||||
private const val NO_MATCH = 1000
|
||||
}
|
||||
|
||||
private val comparator = MessagesComparator()
|
||||
private var prevCount = -1
|
||||
|
||||
private val allItems
|
||||
@ -54,7 +50,7 @@ class MessagesFilter(
|
||||
|
||||
if (prefix.isNullOrBlank()) {
|
||||
allItems.forEach {
|
||||
if (it is MessageFull)
|
||||
it.searchPriority = NO_MATCH
|
||||
it.searchHighlightText = null
|
||||
}
|
||||
results.values = allItems.toList()
|
||||
@ -62,67 +58,50 @@ class MessagesFilter(
|
||||
return results
|
||||
}
|
||||
|
||||
val items = mutableListOf<Any>()
|
||||
val newItems = allItems.mapNotNull { item ->
|
||||
if (item is SearchField) {
|
||||
return@mapNotNull item
|
||||
}
|
||||
item.searchPriority = NO_MATCH
|
||||
item.searchHighlightText = null
|
||||
|
||||
allItems.forEach {
|
||||
if (it !is MessageFull) {
|
||||
items.add(it)
|
||||
return@forEach
|
||||
}
|
||||
it.filterWeight = NO_MATCH
|
||||
it.searchHighlightText = null
|
||||
// get all keyword sets from the entity
|
||||
val searchKeywords = item.searchKeywords
|
||||
// a temporary variable for the loops below
|
||||
var matchWeight: Int
|
||||
|
||||
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)
|
||||
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)
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
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<Any>
|
||||
@Suppress("UNCHECKED_CAST") // yes I know it's checked.
|
||||
adapter.setFilteredItems(it as List<T>)
|
||||
}
|
||||
// do not re-bind the search box
|
||||
val count = results.count - 1
|
@ -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
|
@ -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<MessagesSearch, MessagesAdapter> {
|
||||
),
|
||||
) : 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)
|
||||
|
@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright (c) Kuba Szczodrzyński 2021-10-10.
|
||||
*/
|
||||
|
||||
package pl.szczodrzynski.edziennik.ui.modules.search
|
||||
|
||||
interface Searchable<in T> : Comparable<Searchable<*>> {
|
||||
|
||||
/**
|
||||
* 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<List<String?>?>
|
||||
|
||||
/**
|
||||
* A priority assigned by [SearchFilter]. Lower numbers mean a higher priority.
|
||||
*/
|
||||
var searchPriority: Int
|
||||
|
||||
/**
|
||||
* The text to be highlighted when filtering.
|
||||
*/
|
||||
var searchHighlightText: String?
|
||||
}
|
@ -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<T : Searchable<T>>(
|
||||
val isReversed: Boolean = false,
|
||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>(), 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<T>()
|
||||
|
||||
/**
|
||||
* 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<T>()
|
||||
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<T>) {
|
||||
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<T>, 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<SearchField>().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
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 -> {
|
||||
|
@ -2,12 +2,14 @@
|
||||
<!--
|
||||
~ Copyright (c) Kuba Szczodrzyński 2019-12-15.
|
||||
-->
|
||||
|
||||
<layout xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<data>
|
||||
<import type="android.view.View"/>
|
||||
|
||||
<import type="android.view.View" />
|
||||
|
||||
<variable
|
||||
name="simpleMode"
|
||||
type="Boolean" />
|
||||
@ -16,9 +18,9 @@
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="8dp"
|
||||
android:background="?selectableItemBackground"
|
||||
android:orientation="vertical"
|
||||
android:background="?selectableItemBackground">
|
||||
android:padding="8dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
@ -33,14 +35,24 @@
|
||||
android:layout_marginRight="8dp"
|
||||
android:background="@drawable/unread_red_circle" />
|
||||
|
||||
<com.mikepenz.iconics.view.IconicsImageView
|
||||
android:id="@+id/attachmentIcon"
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginRight="8dp"
|
||||
app:iiv_color="?android:textColorSecondary"
|
||||
app:iiv_icon="cmd-attachment"
|
||||
tools:background="@tools:sample/avatars[4]" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/details"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:maxLines="2"
|
||||
android:textAppearance="@style/NavView.TextView.Helper"
|
||||
android:textSize="16sp"
|
||||
android:maxLines="2"
|
||||
tools:text="sprawdzian • 9:05 • historia i społeczeństwo" />
|
||||
|
||||
<View
|
||||
@ -48,10 +60,9 @@
|
||||
android:layout_width="12dp"
|
||||
android:layout_height="12dp"
|
||||
android:layout_marginHorizontal="4dp"
|
||||
android:visibility="gone"
|
||||
android:background="@drawable/unread_red_circle"
|
||||
tools:visibility="visible"/>
|
||||
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
@ -60,7 +71,7 @@
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.mikepenz.iconics.view.IconicsTextView
|
||||
<TextView
|
||||
android:id="@+id/topic"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
@ -69,7 +80,6 @@
|
||||
android:maxLines="3"
|
||||
android:textAppearance="@style/NavView.TextView.Medium"
|
||||
tools:text="Rozdział II: Panowanie Piastów i Jagiellonów.Przeniesiony z 11 grudnia. Nie wiem co się dzieje w tym roku nie będzie już religii w szkołach podstawowych w Polsce i Europie zachodniej Afryki" />
|
||||
|
||||
<!-- cmd_pencil_outline -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/editButton"
|
||||
@ -85,13 +95,13 @@
|
||||
tools:visibility="visible" />
|
||||
</LinearLayout>
|
||||
|
||||
<com.mikepenz.iconics.view.IconicsTextView
|
||||
<TextView
|
||||
android:id="@+id/addedBy"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@style/NavView.TextView.Helper"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="middle"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="@style/NavView.TextView.Helper"
|
||||
android:visibility="@{simpleMode ? View.GONE : View.VISIBLE}"
|
||||
tools:text="Udostępniono 10 grudnia przez Ktoś Z Twojej Klasy • 2B3T" />
|
||||
</LinearLayout>
|
||||
|
@ -114,13 +114,10 @@
|
||||
|
||||
<com.mikepenz.iconics.view.IconicsImageView
|
||||
android:id="@+id/messageAttachmentImage"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="0dp"
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:layout_marginRight="4dp"
|
||||
android:adjustViewBounds="true"
|
||||
android:paddingVertical="2dp"
|
||||
android:scaleType="fitCenter"
|
||||
app:iiv_color="?android:textColorSecondary"
|
||||
app:iiv_icon="cmd-attachment"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/messageDate"
|
||||
|
Loading…
x
Reference in New Issue
Block a user