From 74b766f18ab828090f8f20c785626196dcd0340a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Sat, 16 Oct 2021 19:37:41 +0200 Subject: [PATCH] [UI] Add text styling to manual events. (#96) * [UI] Move text styling outside of messages module. * [UI] Add text styling to event manual dialog. * [UI/Events] Implement showing HTML-formatted content. * [UI] Fix searching in styled event topic. Create HtmlMode enum. * [UI] Add HTML Simple mode to text styling manager. Fix CharSequence replacing. * [Events] Disable self-shared event notifications. * [UI] Fix simple HTML mode format. Fix HTML in notifications. * [HTML] Replace usages of Html and HtmlCompat with BetterHtml. * [Events] Fix editing self-added events from other devices. * [Events] Implement receiving and fix showing HTML-formatted events. * [UI/Events] Add observing changes in event details dialog. * [Firebase] Disable self-shared event notifications. --- .../pl/szczodrzynski/edziennik/Extensions.kt | 25 +- .../data/web/EdudziennikWebGetHomework.kt | 5 +- .../synergia/LibrusSynergiaGetHomework.kt | 7 +- .../data/api/MobidziennikApiHomework.kt | 4 +- .../edziennik/data/api/szkolny/SzkolnyApi.kt | 5 +- .../edziennik/data/api/task/Notifications.kt | 120 ++++---- .../edziennik/data/db/dao/EventDao.kt | 4 + .../edziennik/data/db/entity/Event.kt | 1 + .../edziennik/data/db/full/EventFull.kt | 17 +- .../edziennik/data/db/full/MessageFull.kt | 5 +- .../data/firebase/SzkolnyAppFirebase.kt | 9 +- .../edziennik/sync/UpdateWorker.kt | 4 +- .../edziennik/ui/dialogs/StyledTextDialog.kt | 89 ++++++ .../ui/dialogs/UpdateAvailableDialog.kt | 4 +- .../ui/dialogs/changelog/ChangelogDialog.kt | 13 +- .../dialogs/sync/RegistrationConfigDialog.kt | 6 +- .../ui/modules/base/CrashActivity.kt | 4 +- .../ui/modules/event/EventDetailsDialog.kt | 17 +- .../ui/modules/event/EventManualDialog.kt | 33 ++- .../ui/modules/event/EventViewHolder.kt | 7 +- .../home/cards/HomeAvailabilityCard.kt | 6 +- .../ui/modules/login/LoginChooserFragment.kt | 4 +- .../compose/MessagesComposeFragment.kt | 65 +---- .../messages/single/MessageFragment.kt | 4 +- .../timetable/WidgetTimetableFactory.java | 16 +- .../edziennik/utils/DefaultTextStyles.kt | 55 ++++ .../edziennik/utils/html/BetterHtml.kt | 50 ++-- .../edziennik/utils/managers/EventManager.kt | 15 +- .../utils/managers/MessageManager.kt | 3 +- .../utils/managers/TextStylingManager.kt | 121 ++++++-- .../main/res/layout/dialog_event_details.xml | 2 - .../res/layout/dialog_event_manual_v2.xml | 5 +- .../layout/dialog_register_unavailable.xml | 6 +- app/src/main/res/layout/event_list_item.xml | 2 +- .../res/layout/messages_compose_fragment.xml | 266 ++++++------------ .../main/res/layout/styled_text_buttons.xml | 99 +++++++ .../main/res/layout/styled_text_dialog.xml | 46 +++ app/src/main/res/values/strings.xml | 3 +- 38 files changed, 733 insertions(+), 414 deletions(-) create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/StyledTextDialog.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/utils/DefaultTextStyles.kt create mode 100644 app/src/main/res/layout/styled_text_buttons.xml create mode 100644 app/src/main/res/layout/styled_text_dialog.xml diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt b/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt index e9865b05..b2a62c8c 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt @@ -1007,8 +1007,29 @@ inline fun LongSparseArray.filter(predicate: (T) -> Boolean): List { return destination } -fun CharSequence.replace(oldValue: String, newValue: CharSequence, ignoreCase: Boolean = false): CharSequence = - splitToSequence(oldValue, ignoreCase = ignoreCase).toList().concat(newValue) +fun CharSequence.replaceSpanned(oldValue: String, newValue: CharSequence, ignoreCase: Boolean = false): CharSequence { + var seq = this + var index = seq.indexOf(oldValue, ignoreCase = ignoreCase) + while (index != -1) { + val sb = SpannableStringBuilder() + sb.appendRange(seq, 0, index) + sb.append(newValue) + sb.appendRange(seq, index + oldValue.length, seq.length) + seq = sb + index = seq.indexOf(oldValue, startIndex = index + 1, ignoreCase = ignoreCase) + } + return seq +} + +fun SpannableStringBuilder.replaceSpan(spanClass: Class<*>, prefix: CharSequence, suffix: CharSequence): SpannableStringBuilder { + getSpans(0, length, spanClass).forEach { + val spanStart = getSpanStart(it) + insert(spanStart, prefix) + val spanEnd = getSpanEnd(it) + insert(spanEnd, suffix) + } + return this +} fun Int.toColorStateList(): ColorStateList { val states = arrayOf( diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebGetHomework.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebGetHomework.kt index 5213b7eb..12c13d68 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebGetHomework.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebGetHomework.kt @@ -1,6 +1,5 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.web -import android.text.Html import org.greenrobot.eventbus.EventBus import pl.szczodrzynski.edziennik.data.api.Regexes import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.DataEdudziennik @@ -9,6 +8,7 @@ import pl.szczodrzynski.edziennik.data.api.events.EventGetEvent import pl.szczodrzynski.edziennik.data.db.full.EventFull import pl.szczodrzynski.edziennik.get import pl.szczodrzynski.edziennik.isNotNullNorEmpty +import pl.szczodrzynski.edziennik.utils.html.BetterHtml class EdudziennikWebGetHomework( override val data: DataEdudziennik, @@ -26,7 +26,8 @@ class EdudziennikWebGetHomework( webGet(TAG, "Homework/$id") { text -> val description = Regexes.EDUDZIENNIK_HOMEWORK_DESCRIPTION.find(text)?.get(1)?.trim() - if (description != null) event.topic = Html.fromHtml(description).toString() + if (description != null) + event.topic = BetterHtml.fromHtml(context = null, description).toString() event.homeworkBody = "" event.isDownloaded = true diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/synergia/LibrusSynergiaGetHomework.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/synergia/LibrusSynergiaGetHomework.kt index 75903907..4ba502f0 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/synergia/LibrusSynergiaGetHomework.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/synergia/LibrusSynergiaGetHomework.kt @@ -1,12 +1,12 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.synergia -import android.text.Html import org.greenrobot.eventbus.EventBus import org.jsoup.Jsoup import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusSynergia import pl.szczodrzynski.edziennik.data.api.events.EventGetEvent import pl.szczodrzynski.edziennik.data.db.full.EventFull +import pl.szczodrzynski.edziennik.utils.html.BetterHtml class LibrusSynergiaGetHomework(override val data: DataLibrus, val event: EventFull, @@ -23,7 +23,10 @@ class LibrusSynergiaGetHomework(override val data: DataLibrus, val table = doc.select("table.decorated tbody > tr") event.topic = table[1].select("td")[1].text() - event.homeworkBody = Html.fromHtml(table[5].select("td")[1].html()).toString() + event.homeworkBody = BetterHtml.fromHtml( + context = null, + html = table[5].select("td")[1].html(), + ).toString() event.isDownloaded = true event.attachmentIds = mutableListOf() diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiHomework.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiHomework.kt index 683a8820..a79eaded 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiHomework.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiHomework.kt @@ -4,12 +4,12 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.api -import android.text.Html import androidx.core.util.contains import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel import pl.szczodrzynski.edziennik.data.db.entity.Event import pl.szczodrzynski.edziennik.data.db.entity.Metadata +import pl.szczodrzynski.edziennik.utils.html.BetterHtml import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Time @@ -26,7 +26,7 @@ class MobidziennikApiHomework(val data: DataMobidziennik, rows: List) { val id = cols[0].toLong() val teacherId = cols[7].toLong() val subjectId = cols[6].toLong() - val topic = Html.fromHtml(cols[1])?.toString()?.trim() ?: "" + val topic = BetterHtml.fromHtml(context = null, cols[1]).toString().trim() val eventDate = Date.fromYmd(cols[2]) val startTime = Time.fromYmdHm(cols[3]) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyApi.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyApi.kt index da64cbc3..3b574b96 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyApi.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyApi.kt @@ -245,7 +245,10 @@ class SzkolnyApi(val app: App) : CoroutineScope { seen = profile.empty notified = profile.empty - if (profile.userCode == event.sharedBy) sharedBy = "self" + if (profile.userCode == event.sharedBy) { + sharedBy = "self" + addedManually = true + } } } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/task/Notifications.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/task/Notifications.kt index 43cc1455..69f44523 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/task/Notifications.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/task/Notifications.kt @@ -69,80 +69,90 @@ class Notifications(val app: App, val notifications: MutableList, } private fun eventNotifications() { - for (event in app.db.eventDao().getNotNotifiedNow().filter { it.date >= today }) { + app.db.eventDao().getNotNotifiedNow().filter { + it.date >= today + }.forEach { event -> val text = if (event.isHomework) app.getString( - if (event.subjectLongName.isNullOrEmpty()) - R.string.notification_homework_no_subject_format - else - R.string.notification_homework_format, - event.subjectLongName, - event.date.formattedString + if (event.subjectLongName.isNullOrEmpty()) + R.string.notification_homework_no_subject_format + else + R.string.notification_homework_format, + event.subjectLongName, + event.date.formattedString ) else app.getString( - if (event.subjectLongName.isNullOrEmpty()) - R.string.notification_event_no_subject_format - else - R.string.notification_event_format, - event.typeName ?: "wydarzenie", - event.date.formattedString, - event.subjectLongName + if (event.subjectLongName.isNullOrEmpty()) + R.string.notification_event_no_subject_format + else + R.string.notification_event_format, + event.typeName ?: "wydarzenie", + event.date.formattedString, + event.subjectLongName ) val textLong = app.getString( - R.string.notification_event_long_format, - event.typeName ?: "-", - event.subjectLongName ?: "-", - event.date.formattedString, - Week.getFullDayName(event.date.weekDay), - event.time?.stringHM ?: app.getString(R.string.event_all_day), - event.topic.take(200) + R.string.notification_event_long_format, + event.typeName ?: "-", + event.subjectLongName ?: "-", + event.date.formattedString, + Week.getFullDayName(event.date.weekDay), + event.time?.stringHM ?: app.getString(R.string.event_all_day), + event.topic.take(200) ) - val type = if (event.isHomework) Notification.TYPE_NEW_HOMEWORK else Notification.TYPE_NEW_EVENT + val type = if (event.isHomework) + Notification.TYPE_NEW_HOMEWORK + else + Notification.TYPE_NEW_EVENT notifications += Notification( - id = Notification.buildId(event.profileId, type, event.id), - title = app.getNotificationTitle(type), - text = text, - textLong = textLong, - type = type, - profileId = event.profileId, - profileName = profiles.singleOrNull { it.id == event.profileId }?.name, - viewId = if (event.isHomework) MainActivity.DRAWER_ITEM_HOMEWORK else MainActivity.DRAWER_ITEM_AGENDA, - addedDate = event.addedDate + id = Notification.buildId(event.profileId, type, event.id), + title = app.getNotificationTitle(type), + text = text, + textLong = textLong, + type = type, + profileId = event.profileId, + profileName = profiles.singleOrNull { it.id == event.profileId }?.name, + viewId = if (event.isHomework) MainActivity.DRAWER_ITEM_HOMEWORK else MainActivity.DRAWER_ITEM_AGENDA, + addedDate = event.addedDate ).addExtra("eventId", event.id).addExtra("eventDate", event.date.value.toLong()) } } fun sharedEventNotifications() { - for (event in app.db.eventDao().getNotNotifiedNow().filter { it.date >= today && it.sharedBy != null }) { + app.db.eventDao().getNotNotifiedNow().filter { + it.date >= today && it.sharedBy != null && it.sharedBy != "self" + }.forEach { event -> val text = app.getString( - R.string.notification_shared_event_format, - event.sharedByName, - event.typeName ?: "wydarzenie", - event.date.formattedString, - event.topic + R.string.notification_shared_event_format, + event.sharedByName, + event.typeName ?: "wydarzenie", + event.date.formattedString, + event.topicHtml ) val textLong = app.getString( - R.string.notification_shared_event_long_format, - event.sharedByName, - event.typeName ?: "-", - event.subjectLongName ?: "-", - event.date.formattedString, - Week.getFullDayName(event.date.weekDay), - event.time?.stringHM ?: app.getString(R.string.event_all_day), - event.topic.take(200) + R.string.notification_shared_event_long_format, + event.sharedByName, + event.typeName ?: "-", + event.subjectLongName ?: "-", + event.date.formattedString, + Week.getFullDayName(event.date.weekDay), + event.time?.stringHM ?: app.getString(R.string.event_all_day), + event.topicHtml.take(200) ) - val type = if (event.isHomework) Notification.TYPE_NEW_HOMEWORK else Notification.TYPE_NEW_EVENT + val type = if (event.isHomework) + Notification.TYPE_NEW_HOMEWORK + else + Notification.TYPE_NEW_EVENT notifications += Notification( - id = Notification.buildId(event.profileId, type, event.id), - title = app.getNotificationTitle(type), - text = text, - textLong = textLong, - type = type, - profileId = event.profileId, - profileName = profiles.singleOrNull { it.id == event.profileId }?.name, - viewId = if (event.isHomework) MainActivity.DRAWER_ITEM_HOMEWORK else MainActivity.DRAWER_ITEM_AGENDA, - addedDate = event.addedDate + id = Notification.buildId(event.profileId, type, event.id), + title = app.getNotificationTitle(type), + text = text, + textLong = textLong, + type = type, + profileId = event.profileId, + profileName = profiles.singleOrNull { it.id == event.profileId }?.name, + viewId = if (event.isHomework) MainActivity.DRAWER_ITEM_HOMEWORK else MainActivity.DRAWER_ITEM_AGENDA, + addedDate = event.addedDate ).addExtra("eventId", event.id).addExtra("eventDate", event.date.value.toLong()) } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/EventDao.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/EventDao.kt index 897d53ff..5c64613e 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/EventDao.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/EventDao.kt @@ -84,6 +84,10 @@ abstract class EventDao : BaseDao { fun getAllByDateNow(profileId: Int, date: Date) = getRawNow("$QUERY WHERE $NOT_BLACKLISTED AND events.profileId = $profileId AND eventDate = '${date.stringY_m_d}' $ORDER_BY") + // GET ONE - LIVE DATA + fun getById(profileId: Int, id: Long) = + getOne("$QUERY WHERE events.profileId = $profileId AND eventId = $id") + // GET ONE - NOW fun getByIdNow(profileId: Int, id: Long) = getOneNow("$QUERY WHERE events.profileId = $profileId AND eventId = $id") 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 928f0931..af313865 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 @@ -75,6 +75,7 @@ open class Event( @ColumnInfo(name = "eventAddedManually") var addedManually: Boolean = false + get() = field || sharedBy == "self" @ColumnInfo(name = "eventSharedBy") var sharedBy: String? = null @ColumnInfo(name = "eventSharedByName") 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 c68ecedd..5e3e62de 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 @@ -7,6 +7,7 @@ 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.html.BetterHtml import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Time @@ -48,6 +49,20 @@ class EventFull( var teamName: String? = null var teamCode: String? = null + @delegate:Ignore + @delegate:Transient + val topicHtml by lazy { + BetterHtml.fromHtml(context = null, topic, nl2br = true) + } + + @delegate:Ignore + @delegate:Transient + val bodyHtml by lazy { + homeworkBody?.let { + BetterHtml.fromHtml(context = null, it, nl2br = true) + } + } + @Ignore @Transient override var searchPriority = 0 @@ -60,7 +75,7 @@ class EventFull( @delegate:Transient override val searchKeywords by lazy { listOf( - listOf(topic, homeworkBody), + listOf(topicHtml.toString(), bodyHtml?.toString()), attachmentNames, listOf(subjectLongName), listOf(teacherName), 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 534ad11c..8ff6fd73 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,12 +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 +import pl.szczodrzynski.edziennik.utils.html.BetterHtml class MessageFull( profileId: Int, id: Long, type: Int, @@ -33,11 +33,10 @@ class MessageFull( @delegate:Transient val bodyHtml by lazy { body?.let { - HtmlCompat.fromHtml(it, HtmlCompat.FROM_HTML_MODE_LEGACY) + BetterHtml.fromHtml(context = null, it) } } - @Ignore @Transient override var searchPriority = 0 diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/firebase/SzkolnyAppFirebase.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/firebase/SzkolnyAppFirebase.kt index a0e70326..13eb0980 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/firebase/SzkolnyAppFirebase.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/firebase/SzkolnyAppFirebase.kt @@ -122,7 +122,7 @@ class SzkolnyAppFirebase(val app: App, val profiles: List, val message: id = json.getLong("id") ?: return, date = json.getInt("eventDate")?.let { Date.fromValue(it) } ?: return, time = json.getInt("startTime")?.let { Time.fromValue(it) }, - topic = json.getString("topic") ?: "", + topic = json.getString("topicHtml") ?: json.getString("topic") ?: "", color = json.getInt("color"), type = json.getLong("type") ?: 0, teacherId = json.getLong("teacherId") ?: -1, @@ -135,7 +135,10 @@ class SzkolnyAppFirebase(val app: App, val profiles: List, val message: event.sharedBy = json.getString("sharedBy") event.sharedByName = json.getString("sharedByName") - if (profile.userCode == event.sharedBy) event.sharedBy = "self" + if (profile.userCode == event.sharedBy) { + event.sharedBy = "self" + event.addedManually = true + } val metadata = Metadata( event.profileId, @@ -148,7 +151,7 @@ class SzkolnyAppFirebase(val app: App, val profiles: List, val message: val type = if (event.isHomework) Notification.TYPE_NEW_SHARED_HOMEWORK else Notification.TYPE_NEW_SHARED_EVENT val notificationFilter = app.config.getFor(event.profileId).sync.notificationFilter - if (!notificationFilter.contains(type)) { + if (!notificationFilter.contains(type) && event.sharedBy != "self") { val notification = Notification( id = Notification.buildId(event.profileId, type, event.id), title = app.getNotificationTitle(type), diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/sync/UpdateWorker.kt b/app/src/main/java/pl/szczodrzynski/edziennik/sync/UpdateWorker.kt index 80e21e6b..c574e9c8 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/sync/UpdateWorker.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/sync/UpdateWorker.kt @@ -9,7 +9,6 @@ import android.app.NotificationManager import android.app.PendingIntent import android.content.Context import android.content.Intent -import android.text.Html import android.widget.Toast import androidx.core.app.NotificationCompat import androidx.work.* @@ -20,6 +19,7 @@ import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update import pl.szczodrzynski.edziennik.utils.Utils +import pl.szczodrzynski.edziennik.utils.html.BetterHtml import java.util.concurrent.TimeUnit import kotlin.coroutines.CoroutineContext @@ -115,7 +115,7 @@ class UpdateWorker(val context: Context, val params: WorkerParameters) : Worker( .setStyle(NotificationCompat.BigTextStyle() .bigText(listOf( app.getString(R.string.notification_updates_text, update.versionName), - update.releaseNotes?.let { Html.fromHtml(it) } + update.releaseNotes?.let { BetterHtml.fromHtml(context = null, it) } ).concat("\n"))) .setColor(0xff2196f3.toInt()) .setLights(0xFF00FFFF.toInt(), 2000, 2000) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/StyledTextDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/StyledTextDialog.kt new file mode 100644 index 00000000..8072b616 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/StyledTextDialog.kt @@ -0,0 +1,89 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-10-11. + */ + +package pl.szczodrzynski.edziennik.ui.dialogs + +import android.content.res.ColorStateList +import android.text.Editable +import android.text.SpannableStringBuilder +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.databinding.StyledTextDialogBinding +import pl.szczodrzynski.edziennik.utils.DefaultTextStyles +import pl.szczodrzynski.edziennik.utils.Themes +import pl.szczodrzynski.edziennik.utils.managers.TextStylingManager.HtmlMode.SIMPLE +import pl.szczodrzynski.edziennik.utils.managers.TextStylingManager.StylingConfig + +class StyledTextDialog( + val activity: AppCompatActivity, + val initialText: Editable?, + val onSuccess: (text: Editable) -> Unit, + val onShowListener: ((tag: String) -> Unit)? = null, + val onDismissListener: ((tag: String) -> Unit)? = null +) { + companion object { + private const val TAG = "StyledTextDialog" + } + + private lateinit var app: App + private lateinit var b: StyledTextDialogBinding + private lateinit var dialog: AlertDialog + private lateinit var config: StylingConfig + + private val manager + get() = app.textStylingManager + + init { + show() + } + + fun show() { + if (activity.isFinishing) + return + onShowListener?.invoke(TAG) + app = activity.applicationContext as App + b = StyledTextDialogBinding.inflate(activity.layoutInflater) + + dialog = MaterialAlertDialogBuilder(activity) + .setTitle(R.string.styled_text_dialog_title) + .setView(b.root) + .setPositiveButton(R.string.save) { _, _ -> + onSuccess(b.editText.text ?: SpannableStringBuilder("")) + } + .setNeutralButton(R.string.cancel, null) + .setOnDismissListener { + onDismissListener?.invoke(TAG) + } + .show() + + config = StylingConfig( + editText = b.editText, + fontStyleGroup = b.fontStyle.styles, + fontStyleClear = b.fontStyle.clear, + styles = DefaultTextStyles.getAsList(b.fontStyle), + textHtml = if (App.devMode) b.htmlText else null, + htmlMode = SIMPLE, + ) + + manager.attach(config) + + b.editText.text = initialText + + // this is awful + if (Themes.isDark) { + val colorStateList = ColorStateList.valueOf(0x40ffffff) + b.fontStyle.bold.strokeColor = colorStateList + b.fontStyle.italic.strokeColor = colorStateList + b.fontStyle.underline.strokeColor = colorStateList + b.fontStyle.strike.strokeColor = colorStateList + b.fontStyle.subscript.strokeColor = colorStateList + b.fontStyle.superscript.strokeColor = colorStateList + b.fontStyle.clear.strokeColor = colorStateList + } + } +} + diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/UpdateAvailableDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/UpdateAvailableDialog.kt index 6446170b..3c54dc92 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/UpdateAvailableDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/UpdateAvailableDialog.kt @@ -4,7 +4,6 @@ package pl.szczodrzynski.edziennik.ui.dialogs -import android.text.Html import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import com.google.android.material.dialog.MaterialAlertDialogBuilder @@ -14,6 +13,7 @@ import kotlinx.coroutines.Job import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update import pl.szczodrzynski.edziennik.sync.UpdateDownloaderService +import pl.szczodrzynski.edziennik.utils.html.BetterHtml import kotlin.coroutines.CoroutineContext class UpdateAvailableDialog( @@ -48,7 +48,7 @@ class UpdateAvailableDialog( R.string.update_available_format, BuildConfig.VERSION_NAME, update.versionName, - update.releaseNotes?.let { Html.fromHtml(it) } ?: "---" + update.releaseNotes?.let { BetterHtml.fromHtml(activity, it) } ?: "---" ) .setPositiveButton(R.string.update_available_button) { dialog, _ -> activity.startService(Intent(app, UpdateDownloaderService::class.java)) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/changelog/ChangelogDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/changelog/ChangelogDialog.kt index 40ea90b2..aab73a69 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/changelog/ChangelogDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/changelog/ChangelogDialog.kt @@ -5,7 +5,6 @@ package pl.szczodrzynski.edziennik.ui.dialogs.changelog import android.os.Build -import android.text.Html import android.widget.ScrollView import android.widget.TextView import androidx.appcompat.app.AlertDialog @@ -18,6 +17,7 @@ import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.dp import pl.szczodrzynski.edziennik.utils.BetterLinkMovementMethod +import pl.szczodrzynski.edziennik.utils.html.BetterHtml import kotlin.coroutines.CoroutineContext class ChangelogDialog( @@ -52,12 +52,11 @@ class ChangelogDialog( text = text.replace("""\[(.+?)]\(@([A-z0-9-]+)\)""".toRegex(), "$1") text = text.replace("""\s@([A-z0-9-]+)""".toRegex(), " @$1") - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - textView.text = Html.fromHtml(text) - } - else { - textView.text = Html.fromHtml(text.replace("
  • ", "
  • - ")) - } + val html = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + text + else + text.replace("
  • ", "
  • - ") + textView.text = BetterHtml.fromHtml(activity, html) textView.movementMethod = BetterLinkMovementMethod.getInstance() diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/sync/RegistrationConfigDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/sync/RegistrationConfigDialog.kt index fff65b43..4a309536 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/sync/RegistrationConfigDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/sync/RegistrationConfigDialog.kt @@ -4,7 +4,6 @@ package pl.szczodrzynski.edziennik.ui.dialogs.sync -import android.text.Html import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import com.google.android.material.dialog.MaterialAlertDialogBuilder @@ -14,6 +13,7 @@ import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi import pl.szczodrzynski.edziennik.data.api.task.AppSync import pl.szczodrzynski.edziennik.data.db.entity.Profile +import pl.szczodrzynski.edziennik.utils.html.BetterHtml import kotlin.coroutines.CoroutineContext class RegistrationConfigDialog( @@ -61,7 +61,7 @@ class RegistrationConfigDialog( onShowListener?.invoke(TAG + "Enable") dialog = MaterialAlertDialogBuilder(activity) .setTitle(R.string.registration_config_title) - .setMessage(Html.fromHtml(app.getString(R.string.registration_config_enable_text))) + .setMessage(BetterHtml.fromHtml(activity, R.string.registration_config_enable_text)) .setPositiveButton(R.string.i_agree) { _, _ -> enableRegistration() } @@ -76,7 +76,7 @@ class RegistrationConfigDialog( onShowListener?.invoke(TAG + "Disable") dialog = MaterialAlertDialogBuilder(activity) .setTitle(R.string.registration_config_title) - .setMessage(Html.fromHtml(app.getString(R.string.registration_config_disable_text))) + .setMessage(R.string.registration_config_disable_text) .setPositiveButton(R.string.ok) { _, _ -> disableRegistration() } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/base/CrashActivity.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/base/CrashActivity.kt index 0b39b913..bd892fd4 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/base/CrashActivity.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/base/CrashActivity.kt @@ -6,7 +6,6 @@ import android.content.Context import android.content.Intent import android.os.Build import android.os.Bundle -import android.text.Html import android.view.View import android.widget.Button import android.widget.Toast @@ -22,6 +21,7 @@ import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi import pl.szczodrzynski.edziennik.data.api.szkolny.request.ErrorReportRequest import pl.szczodrzynski.edziennik.data.db.entity.Profile import pl.szczodrzynski.edziennik.utils.Themes.appTheme +import pl.szczodrzynski.edziennik.utils.html.BetterHtml import kotlin.coroutines.CoroutineContext /* @@ -90,7 +90,7 @@ class CrashActivity : AppCompatActivity(), CoroutineScope { moreInfoButton.setOnClickListener { MaterialAlertDialogBuilder(this, R.style.AppTheme_MaterialAlertDialogMonospace) .setTitle(R.string.crash_details) - .setMessage(Html.fromHtml(getErrorString(intent, false))) + .setMessage(BetterHtml.fromHtml(context = null, getErrorString(intent, false))) .setPositiveButton(R.string.close, null) .setNeutralButton(R.string.copy_to_clipboard) { _, _ -> copyErrorToClipboard() } .show() diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/event/EventDetailsDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/event/EventDetailsDialog.kt index ae848edb..1b23d45e 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/event/EventDetailsDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/event/EventDetailsDialog.kt @@ -31,6 +31,7 @@ import kotlin.coroutines.CoroutineContext class EventDetailsDialog( val activity: AppCompatActivity, + // this event is observed for changes var event: EventFull, val onShowListener: ((tag: String) -> Unit)? = null, val onDismissListener: ((tag: String) -> Unit)? = null @@ -85,7 +86,11 @@ class EventDetailsDialog( showRemoveEventDialog() } - update() + // watch the event for changes + app.db.eventDao().getById(event.profileId, event.id).observe(activity) { + event = it ?: return@observe + update() + } }} private fun update() { @@ -93,6 +98,9 @@ class EventDetailsDialog( b.eventShared = eventShared b.eventOwn = eventOwn + b.topic.text = event.topicHtml + b.body.text = event.bodyHtml + if (!event.seen) { manager.markAsSeen(event) } @@ -170,8 +178,9 @@ class EventDetailsDialog( dialog.dismiss() return@EventManualDialog } - event = it - update() + // this should not be needed as the event is observed by the ID + // event = it + // update() }, onShowListener = onShowListener, onDismissListener = onDismissListener @@ -350,7 +359,7 @@ class EventDetailsDialog( val intent = Intent(Intent.ACTION_EDIT).apply { data = Events.CONTENT_URI putExtra(Events.TITLE, title) - putExtra(Events.DESCRIPTION, event.topic) + putExtra(Events.DESCRIPTION, event.topicHtml.toString()) if (event.time == null) { putExtra(CalendarContract.EXTRA_EVENT_ALL_DAY, true) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/event/EventManualDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/event/EventManualDialog.kt index 5385ee96..51557351 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/event/EventManualDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/event/EventManualDialog.kt @@ -13,6 +13,9 @@ import androidx.appcompat.app.AppCompatActivity import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.jaredrummler.android.colorpicker.ColorPickerDialog import com.jaredrummler.android.colorpicker.ColorPickerDialogListener +import com.mikepenz.iconics.IconicsDrawable +import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial +import com.mikepenz.iconics.utils.sizeDp import kotlinx.coroutines.* import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe @@ -27,9 +30,13 @@ import pl.szczodrzynski.edziennik.data.db.entity.* import pl.szczodrzynski.edziennik.data.db.full.EventFull import pl.szczodrzynski.edziennik.data.db.full.LessonFull import pl.szczodrzynski.edziennik.databinding.DialogEventManualV2Binding +import pl.szczodrzynski.edziennik.ui.dialogs.StyledTextDialog import pl.szczodrzynski.edziennik.ui.dialogs.sync.RegistrationConfigDialog import pl.szczodrzynski.edziennik.ui.modules.views.TimeDropdown.Companion.DISPLAY_LESSONS import pl.szczodrzynski.edziennik.utils.Anim +import pl.szczodrzynski.edziennik.utils.html.BetterHtml +import pl.szczodrzynski.edziennik.utils.managers.TextStylingManager.HtmlMode.SIMPLE +import pl.szczodrzynski.edziennik.utils.managers.TextStylingManager.StylingConfigBase import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Time import kotlin.coroutines.CoroutineContext @@ -59,6 +66,10 @@ class EventManualDialog( private lateinit var b: DialogEventManualV2Binding private lateinit var dialog: AlertDialog private lateinit var profile: Profile + private lateinit var stylingConfig: StylingConfigBase + + private val textStylingManager + get() = app.textStylingManager private var customColor: Int? = null private val editingShared = editingEvent?.sharedBy != null @@ -128,6 +139,23 @@ class EventManualDialog( } } + b.topicLayout.endIconDrawable = IconicsDrawable(activity, CommunityMaterial.Icon3.cmd_open_in_new).apply { + sizeDp = 24 + } + b.topicLayout.setEndIconOnClickListener { + StyledTextDialog( + activity, + initialText = b.topic.text, + onSuccess = { + b.topic.text = it + }, + onShowListener, + onDismissListener + ) + } + + stylingConfig = StylingConfigBase(editText = b.topic, htmlMode = SIMPLE) + updateShareText() b.shareSwitch.onChange { _, isChecked -> updateShareText(isChecked) @@ -332,7 +360,7 @@ class EventManualDialog( // copy data from event being edited editingEvent?.let { - b.topic.setText(it.topic) + b.topic.setText(BetterHtml.fromHtml(activity, it.topic, nl2br = true)) if (it.color != -1) customColor = it.color } @@ -458,12 +486,13 @@ class EventManualDialog( val id = System.currentTimeMillis() + val topicHtml = textStylingManager.getHtmlText(stylingConfig) val eventObject = Event( profileId = profileId, id = editingEvent?.id ?: id, date = date, time = startTime, - topic = topic, + topic = topicHtml, color = customColor, type = type?.id ?: Event.TYPE_DEFAULT, teacherId = teacher?.id ?: -1, 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 index 399a597d..8aff06dc 100644 --- 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 @@ -9,7 +9,6 @@ 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 @@ -99,15 +98,13 @@ class EventViewHolder( /* 3$ */ item.teamName?.let { bullet + it } ?: "", ) + // workaround for the span data lost during setText above val addedBySpanned = adapter.highlightSearchText( item = item, text = addedBy, color = colorHighlight ) - b.addedBy.text = b.addedBy.text.replace(addedBy, addedBySpanned) - // for now, as CharSequence.replace() converts the original sequence to string, - // so the Iconics span data is lost and the share icon set above does not display - b.addedBy.buildIconics() + b.addedBy.text = b.addedBy.text.replaceSpanned(addedBy, addedBySpanned) b.attachmentIcon.isVisible = item.hasAttachments diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/cards/HomeAvailabilityCard.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/cards/HomeAvailabilityCard.kt index 613658f4..411d3aef 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/cards/HomeAvailabilityCard.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/cards/HomeAvailabilityCard.kt @@ -8,7 +8,6 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.FrameLayout -import androidx.core.text.HtmlCompat import androidx.core.view.isVisible import androidx.core.view.plusAssign import androidx.core.view.setMargins @@ -25,6 +24,7 @@ import pl.szczodrzynski.edziennik.ui.dialogs.UpdateAvailableDialog import pl.szczodrzynski.edziennik.ui.modules.home.HomeCard import pl.szczodrzynski.edziennik.ui.modules.home.HomeCardAdapter import pl.szczodrzynski.edziennik.ui.modules.home.HomeFragment +import pl.szczodrzynski.edziennik.utils.html.BetterHtml import kotlin.coroutines.CoroutineContext class HomeAvailabilityCard( @@ -61,8 +61,8 @@ class HomeAvailabilityCard( // show "register unavailable" only when disabled if (status?.userMessage != null) { - b.homeAvailabilityTitle.text = HtmlCompat.fromHtml(status.userMessage.title, HtmlCompat.FROM_HTML_MODE_LEGACY) - b.homeAvailabilityText.text = HtmlCompat.fromHtml(status.userMessage.contentShort, HtmlCompat.FROM_HTML_MODE_LEGACY) + b.homeAvailabilityTitle.text = BetterHtml.fromHtml(activity, status.userMessage.title) + b.homeAvailabilityText.text = BetterHtml.fromHtml(activity, status.userMessage.contentShort) b.homeAvailabilityUpdate.isVisible = false b.homeAvailabilityIcon.setImageResource(R.drawable.ic_sync) if (status.userMessage.icon != null) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginChooserFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginChooserFragment.kt index 71a5b804..c69d3ec2 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginChooserFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginChooserFragment.kt @@ -10,7 +10,6 @@ import android.app.Activity import android.content.Intent import android.graphics.Color import android.os.Bundle -import android.text.Html import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -31,6 +30,7 @@ import pl.szczodrzynski.edziennik.ui.dialogs.RegisterUnavailableDialog import pl.szczodrzynski.edziennik.ui.modules.feedback.FeedbackActivity import pl.szczodrzynski.edziennik.utils.BetterLinkMovementMethod import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration +import pl.szczodrzynski.edziennik.utils.html.BetterHtml import pl.szczodrzynski.edziennik.utils.managers.AvailabilityManager.Error.Type import pl.szczodrzynski.edziennik.utils.models.Date import kotlin.coroutines.CoroutineContext @@ -218,7 +218,7 @@ class LoginChooserFragment : Fragment(), CoroutineScope { if (!app.config.privacyPolicyAccepted) { MaterialAlertDialogBuilder(activity) .setTitle(R.string.privacy_policy) - .setMessage(Html.fromHtml(activity.getString(R.string.privacy_policy_dialog_html))) + .setMessage(BetterHtml.fromHtml(activity, R.string.privacy_policy_dialog_html)) .setPositiveButton(R.string.i_agree) { _, _ -> app.config.privacyPolicyAccepted = true onLoginModeClicked(loginType, loginMode) 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 bd468ea7..f8aefd1f 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 @@ -38,15 +38,17 @@ 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.list.MessagesFragment +import pl.szczodrzynski.edziennik.utils.DefaultTextStyles import pl.szczodrzynski.edziennik.utils.Themes import pl.szczodrzynski.edziennik.utils.managers.MessageManager.UIConfig +import pl.szczodrzynski.edziennik.utils.managers.TextStylingManager.HtmlMode.COMPATIBLE +import pl.szczodrzynski.edziennik.utils.managers.TextStylingManager.HtmlMode.ORIGINAL import pl.szczodrzynski.edziennik.utils.managers.TextStylingManager.StylingConfig import pl.szczodrzynski.edziennik.utils.span.* import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetSeparatorItem import kotlin.coroutines.CoroutineContext - class MessagesComposeFragment : Fragment(), CoroutineScope { companion object { private const val TAG = "MessagesComposeFragment" @@ -103,6 +105,7 @@ class MessagesComposeFragment : Fragment(), CoroutineScope { b.breakpoints.visibility = if (App.devMode) View.VISIBLE else View.GONE b.breakpoints.setOnClickListener { b.breakpoints.isEnabled = true + @SuppressLint("SetTextI18n") b.breakpoints.text = "Breakpoints!" // do your job } @@ -232,45 +235,7 @@ class MessagesComposeFragment : Fragment(), CoroutineScope { b.subjectLayout.isEnabled = false b.textLayout.isEnabled = false - val styles = listOf( - StylingConfig.Style( - button = b.fontStyleBold, - spanClass = BoldSpan::class.java, - icon = CommunityMaterial.Icon2.cmd_format_bold, - hint = R.string.hint_style_bold, - ), - StylingConfig.Style( - button = b.fontStyleItalic, - spanClass = ItalicSpan::class.java, - icon = CommunityMaterial.Icon2.cmd_format_italic, - hint = R.string.hint_style_italic, - ), - StylingConfig.Style( - button = b.fontStyleUnderline, - // a custom span is used to prevent issues with keyboards which underline words - spanClass = UnderlineCustomSpan::class.java, - icon = CommunityMaterial.Icon2.cmd_format_underline, - hint = R.string.hint_style_underline, - ), - StylingConfig.Style( - button = b.fontStyleStrike, - spanClass = StrikethroughSpan::class.java, - icon = CommunityMaterial.Icon2.cmd_format_strikethrough, - hint = R.string.hint_style_strike, - ), - StylingConfig.Style( - button = b.fontStyleSubscript, - spanClass = SubscriptSizeSpan::class.java, - icon = CommunityMaterial.Icon2.cmd_format_subscript, - hint = R.string.hint_style_subscript, - ), - StylingConfig.Style( - button = b.fontStyleSuperscript, - spanClass = SuperscriptSizeSpan::class.java, - icon = CommunityMaterial.Icon2.cmd_format_superscript, - hint = R.string.hint_style_superscript, - ), - ) + val styles = DefaultTextStyles.getAsList(b.fontStyle) uiConfig = UIConfig( context = activity, @@ -285,28 +250,24 @@ class MessagesComposeFragment : Fragment(), CoroutineScope { ) stylingConfig = StylingConfig( editText = b.text, - fontStyleGroup = b.fontStyle, - fontStyleClear = b.fontStyleClear, + fontStyleGroup = b.fontStyle.styles, + fontStyleClear = b.fontStyle.clear, styles = styles, textHtml = if (App.devMode) b.textHtml else null, - htmlCompatibleMode = app.profile.loginStoreType == LOGIN_TYPE_MOBIDZIENNIK, + htmlMode = when (app.profile.loginStoreType) { + LOGIN_TYPE_MOBIDZIENNIK -> COMPATIBLE + else -> ORIGINAL + }, ) - b.fontStyleLayout.isVisible = enableTextStyling + b.fontStyle.root.isVisible = enableTextStyling if (enableTextStyling) { textStylingManager.attach(stylingConfig) - b.fontStyle.addOnButtonCheckedListener { _, _, _ -> + b.fontStyle.styles.addOnButtonCheckedListener { _, _, _ -> changedBody = true } } - if (App.devMode) { - b.textHtml.isVisible = true - b.text.addTextChangedListener { - b.textHtml.text = getMessageBody() - } - } - activity.navView.bottomBar.apply { fabEnable = true fabExtendedText = getString(R.string.messages_compose_send) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/single/MessageFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/single/MessageFragment.kt index ad86f287..f17c207e 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/single/MessageFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/single/MessageFragment.kt @@ -5,7 +5,6 @@ package pl.szczodrzynski.edziennik.ui.modules.messages.single import android.os.Bundle -import android.text.Html import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -32,6 +31,7 @@ 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.html.BetterHtml import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Time import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem @@ -255,7 +255,7 @@ class MessageFragment : Fragment(), CoroutineScope { } } messageRecipients.append("") - b.recipients.text = Html.fromHtml(messageRecipients.toString()) + b.recipients.text = BetterHtml.fromHtml(activity, messageRecipients) showAttachments() diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/timetable/WidgetTimetableFactory.java b/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/timetable/WidgetTimetableFactory.java index 369f6783..01fda6d9 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/timetable/WidgetTimetableFactory.java +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/timetable/WidgetTimetableFactory.java @@ -4,6 +4,8 @@ package pl.szczodrzynski.edziennik.ui.widgets.timetable; +import static android.util.TypedValue.COMPLEX_UNIT_SP; + import android.appwidget.AppWidgetManager; import android.content.ComponentName; import android.content.Context; @@ -16,7 +18,6 @@ import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; import android.graphics.drawable.Drawable; -import android.text.Html; import android.util.Log; import android.view.View; import android.widget.RemoteViews; @@ -32,14 +33,13 @@ import com.mikepenz.iconics.utils.IconicsDrawableExtensionsKt; import java.util.List; import kotlin.Unit; +import pl.szczodrzynski.edziennik.ExtensionsKt; import pl.szczodrzynski.edziennik.R; import pl.szczodrzynski.edziennik.utils.models.Date; import pl.szczodrzynski.edziennik.utils.models.ItemWidgetTimetableModel; import pl.szczodrzynski.edziennik.utils.models.Time; import pl.szczodrzynski.edziennik.utils.models.Week; -import static android.util.TypedValue.COMPLEX_UNIT_SP; - public class WidgetTimetableFactory implements RemoteViewsService.RemoteViewsFactory { private static final String TAG = "WidgetTimetableProvider"; @@ -309,17 +309,17 @@ public class WidgetTimetableFactory implements RemoteViewsService.RemoteViewsFac views.setViewVisibility(R.id.widgetTimetableOldSubjectName, View.GONE); if (lesson.lessonChange) { - views.setTextViewText(R.id.widgetTimetableSubjectName, Html.fromHtml(""+lesson.subjectName+"")); + views.setTextViewText(R.id.widgetTimetableSubjectName, ExtensionsKt.asItalicSpannable(lesson.subjectName)); if (lesson.lessonChangeNoClassroom) { - views.setTextViewText(R.id.widgetTimetableClassroomName, Html.fromHtml(""+lesson.classroomName+"")); + views.setTextViewText(R.id.widgetTimetableClassroomName, ExtensionsKt.asStrikethroughSpannable(lesson.classroomName)); } else { - views.setTextViewText(R.id.widgetTimetableClassroomName, Html.fromHtml("" + lesson.classroomName + "")); + views.setTextViewText(R.id.widgetTimetableClassroomName, ExtensionsKt.asItalicSpannable(lesson.classroomName)); } } else if (lesson.lessonCancelled) { - views.setTextViewText(R.id.widgetTimetableSubjectName, Html.fromHtml(""+lesson.subjectName+"")); - views.setTextViewText(R.id.widgetTimetableClassroomName, Html.fromHtml(""+lesson.classroomName+"")); + views.setTextViewText(R.id.widgetTimetableSubjectName, ExtensionsKt.asStrikethroughSpannable(lesson.subjectName)); + views.setTextViewText(R.id.widgetTimetableClassroomName, ExtensionsKt.asStrikethroughSpannable(lesson.classroomName)); } else { views.setTextViewText(R.id.widgetTimetableSubjectName, lesson.subjectName); diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/DefaultTextStyles.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/DefaultTextStyles.kt new file mode 100644 index 00000000..7f1fa02a --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/DefaultTextStyles.kt @@ -0,0 +1,55 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-10-11. + */ + +package pl.szczodrzynski.edziennik.utils + +import android.text.style.StrikethroughSpan +import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.databinding.StyledTextButtonsBinding +import pl.szczodrzynski.edziennik.utils.managers.TextStylingManager.StylingConfig +import pl.szczodrzynski.edziennik.utils.span.* + +object DefaultTextStyles { + + fun getAsList(b: StyledTextButtonsBinding) = listOf( + StylingConfig.Style( + button = b.bold, + spanClass = BoldSpan::class.java, + icon = CommunityMaterial.Icon2.cmd_format_bold, + hint = R.string.hint_style_bold, + ), + StylingConfig.Style( + button = b.italic, + spanClass = ItalicSpan::class.java, + icon = CommunityMaterial.Icon2.cmd_format_italic, + hint = R.string.hint_style_italic, + ), + StylingConfig.Style( + button = b.underline, + // a custom span is used to prevent issues with keyboards which underline words + spanClass = UnderlineCustomSpan::class.java, + icon = CommunityMaterial.Icon2.cmd_format_underline, + hint = R.string.hint_style_underline, + ), + StylingConfig.Style( + button = b.strike, + spanClass = StrikethroughSpan::class.java, + icon = CommunityMaterial.Icon2.cmd_format_strikethrough, + hint = R.string.hint_style_strike, + ), + StylingConfig.Style( + button = b.subscript, + spanClass = SubscriptSizeSpan::class.java, + icon = CommunityMaterial.Icon2.cmd_format_subscript, + hint = R.string.hint_style_subscript, + ), + StylingConfig.Style( + button = b.superscript, + spanClass = SuperscriptSizeSpan::class.java, + icon = CommunityMaterial.Icon2.cmd_format_superscript, + hint = R.string.hint_style_superscript, + ), + ) +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/html/BetterHtml.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/html/BetterHtml.kt index 70886ec9..206dacf7 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/html/BetterHtml.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/html/BetterHtml.kt @@ -32,8 +32,14 @@ object BetterHtml { SuperscriptSizeSpan::class.java, ) + fun fromHtml(context: Context, stringRes: Int) = fromHtml( + context, + context.getString(stringRes), + nl2br = true, + ) + @JvmStatic - fun fromHtml(context: Context, html: String): Spanned { + fun fromHtml(context: Context?, html: CharSequence, nl2br: Boolean = false): Spanned { val hexPattern = "(#[a-fA-F0-9]{6})" val colorRegex = "(?:color=\"$hexPattern\")|(?:style=\"color: ?${hexPattern})" .toRegex(RegexOption.IGNORE_CASE) @@ -42,29 +48,35 @@ object BetterHtml { .replace("\\[META:[A-z0-9]+;[0-9-]+]".toRegex(), "") .replace("background-color: ?$hexPattern;".toRegex(), "") - val colorBackground = android.R.attr.colorBackground.resolveAttr(context) - val textColorPrimary = android.R.attr.textColorPrimary.resolveAttr(context) and 0xffffff + if (nl2br) { + text = text.replace("\n", "
    ") + } - colorRegex.findAll(text).forEach { result -> - val group = result.groups.drop(1).firstOrNull { it != null } ?: return@forEach + if (context != null) { + val colorBackground = android.R.attr.colorBackground.resolveAttr(context) + val textColorPrimary = android.R.attr.textColorPrimary.resolveAttr(context) and 0xffffff - val color = Color.parseColor(group.value) - var newColor = 0xff000000.toInt() or color + colorRegex.findAll(text).forEach { result -> + val group = result.groups.drop(1).firstOrNull { it != null } ?: return@forEach - var blendAmount = 1 - var numIterations = 0 + val color = Color.parseColor(group.value) + var newColor = 0xff000000.toInt() or color - while (numIterations < 100 && ColorUtils.calculateContrast( - colorBackground, - newColor - ) < 4.5f - ) { - blendAmount += 2 - newColor = blendColors(color, blendAmount shl 24 or textColorPrimary) - numIterations++ + var blendAmount = 1 + var numIterations = 0 + + while (numIterations < 100 && ColorUtils.calculateContrast( + colorBackground, + newColor + ) < 4.5f + ) { + blendAmount += 2 + newColor = blendColors(color, blendAmount shl 24 or textColorPrimary) + numIterations++ + } + + text = text.replaceRange(group.range, "#" + (newColor and 0xffffff).toString(16)) } - - text = text.replaceRange(group.range, "#" + (newColor and 0xffffff).toString(16)) } /*val olRegex = """
      (.+?)""" 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 6594aa01..a16175f4 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 @@ -45,16 +45,13 @@ class EventManager(val app: App) : CoroutineScope { showType: Boolean = true, doneIconColor: Int? = null ) { - var eventTopic = if (showType) - "${event.typeName ?: "wydarzenie"} - ${event.topic}" - else - event.topic + val topicSpan = event.topicHtml - if (event.addedManually) { - eventTopic = "{cmd-clipboard-edit-outline} $eventTopic" - } - - title.text = eventTopic + title.text = listOfNotNull( + if (event.addedManually) "{cmd-clipboard-edit-outline} " else null, + if (showType) "${event.typeName ?: "wydarzenie"} - " else null, + topicSpan, + ).concat() title.setCompoundDrawables( 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 c72a770f..f421cc74 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 @@ -25,6 +25,7 @@ import pl.szczodrzynski.edziennik.data.db.full.MessageFull import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesUtils import pl.szczodrzynski.edziennik.utils.TextInputKeyboardEdit import pl.szczodrzynski.edziennik.utils.html.BetterHtml +import pl.szczodrzynski.edziennik.utils.managers.TextStylingManager.HtmlMode.ORIGINAL import pl.szczodrzynski.edziennik.utils.managers.TextStylingManager.StylingConfig import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Time @@ -148,7 +149,7 @@ class MessageManager(private val app: App) { suspend fun saveAsDraft(config: UIConfig, stylingConfig: StylingConfig, profileId: Int, messageId: Long?) { val teachers = config.recipients.allChips.mapNotNull { it.data as? Teacher } val subject = config.subject.text?.toString() ?: "" - val body = textStylingManager.getHtmlText(stylingConfig, enableHtmlCompatible = false) + val body = textStylingManager.getHtmlText(stylingConfig, htmlMode = ORIGINAL) withContext(Dispatchers.Default) { if (messageId != null) { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/TextStylingManager.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/TextStylingManager.kt index b60df935..516792db 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/TextStylingManager.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/TextStylingManager.kt @@ -6,18 +6,25 @@ package pl.szczodrzynski.edziennik.utils.managers import android.text.SpannableStringBuilder import android.text.Spanned +import android.text.style.StrikethroughSpan +import android.text.style.SubscriptSpan +import android.text.style.SuperscriptSpan +import android.text.style.UnderlineSpan import android.widget.Button import android.widget.TextView import androidx.annotation.StringRes import androidx.core.text.HtmlCompat +import androidx.core.view.isVisible +import androidx.core.widget.addTextChangedListener import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButtonToggleGroup import com.mikepenz.iconics.typeface.IIcon -import pl.szczodrzynski.edziennik.App -import pl.szczodrzynski.edziennik.attachToastHint -import pl.szczodrzynski.edziennik.hasSet +import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.utils.TextInputKeyboardEdit import pl.szczodrzynski.edziennik.utils.html.BetterHtml +import pl.szczodrzynski.edziennik.utils.managers.TextStylingManager.HtmlMode.* +import pl.szczodrzynski.edziennik.utils.span.BoldSpan +import pl.szczodrzynski.edziennik.utils.span.ItalicSpan class TextStylingManager(private val app: App) { companion object { @@ -28,14 +35,45 @@ class TextStylingManager(private val app: App) { "((?:
      )+)

      ".toRegex() } - data class StylingConfig( + enum class HtmlMode { + /** + * The default mode, suitable for fromHtml conversion. + */ + ORIGINAL, + + /** + * A more browser-compatible mode. + */ + COMPATIBLE, + + /** + * A simple, paragraph-stripped mode with \n instead of
      . + * The converted text has no HTML tags when no spans in source. + */ + SIMPLE, + + /** + * Markdown-compatible text mode. + */ + MARKDOWN, + } + + open class StylingConfigBase( val editText: TextInputKeyboardEdit, + val htmlMode: HtmlMode = ORIGINAL, + ) { + var watchStyleChecked = true + var watchSelectionChanged = true + } + + class StylingConfig( + editText: TextInputKeyboardEdit, val fontStyleGroup: MaterialButtonToggleGroup, val fontStyleClear: Button, val styles: List