[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.
This commit is contained in:
Kuba Szczodrzyński 2021-10-16 19:37:41 +02:00 committed by GitHub
parent 7e0f69d95d
commit 74b766f18a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 733 additions and 414 deletions

View File

@ -1007,8 +1007,29 @@ inline fun <T> LongSparseArray<T>.filter(predicate: (T) -> Boolean): List<T> {
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(

View File

@ -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

View File

@ -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()

View File

@ -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<String>) {
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])

View File

@ -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
}
}
}
}

View File

@ -69,80 +69,90 @@ class Notifications(val app: App, val notifications: MutableList<Notification>,
}
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())
}
}

View File

@ -84,6 +84,10 @@ abstract class EventDao : BaseDao<Event, EventFull> {
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")

View File

@ -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")

View File

@ -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),

View File

@ -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

View File

@ -122,7 +122,7 @@ class SzkolnyAppFirebase(val app: App, val profiles: List<Profile>, 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<Profile>, 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<Profile>, 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),

View File

@ -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)

View File

@ -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
}
}
}

View File

@ -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))

View File

@ -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(), "<a href=\"$commitsUrlPrefix$2\">$1</a>")
text = text.replace("""\s@([A-z0-9-]+)""".toRegex(), " <a href=\"$commitsUrlPrefix$1\">@$1</a>")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
textView.text = Html.fromHtml(text)
}
else {
textView.text = Html.fromHtml(text.replace("<li>", "<br><li> - "))
}
val html = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
text
else
text.replace("<li>", "<br><li> - ")
textView.text = BetterHtml.fromHtml(activity, html)
textView.movementMethod = BetterLinkMovementMethod.getInstance()

View File

@ -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()
}

View File

@ -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()

View File

@ -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)

View File

@ -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,

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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("</ul>")
b.recipients.text = Html.fromHtml(messageRecipients.toString())
b.recipients.text = BetterHtml.fromHtml(activity, messageRecipients)
showAttachments()

View File

@ -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("<i>"+lesson.subjectName+"</i>"));
views.setTextViewText(R.id.widgetTimetableSubjectName, ExtensionsKt.asItalicSpannable(lesson.subjectName));
if (lesson.lessonChangeNoClassroom) {
views.setTextViewText(R.id.widgetTimetableClassroomName, Html.fromHtml("<del>"+lesson.classroomName+"</del>"));
views.setTextViewText(R.id.widgetTimetableClassroomName, ExtensionsKt.asStrikethroughSpannable(lesson.classroomName));
}
else {
views.setTextViewText(R.id.widgetTimetableClassroomName, Html.fromHtml("<i>" + lesson.classroomName + "</i>"));
views.setTextViewText(R.id.widgetTimetableClassroomName, ExtensionsKt.asItalicSpannable(lesson.classroomName));
}
}
else if (lesson.lessonCancelled) {
views.setTextViewText(R.id.widgetTimetableSubjectName, Html.fromHtml("<del>"+lesson.subjectName+"</del>"));
views.setTextViewText(R.id.widgetTimetableClassroomName, Html.fromHtml("<del>"+lesson.classroomName+"</del>"));
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);

View File

@ -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,
),
)
}

View File

@ -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", "<br>")
}
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 = """<ol>(.+?)</\s*?ol>"""

View File

@ -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,

View File

@ -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) {

View File

@ -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) {
"((?:<br>)+)</p>".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 <br>.
* 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<Style>,
val textHtml: TextView? = null,
val htmlCompatibleMode: Boolean = false,
) {
htmlMode: HtmlMode = ORIGINAL,
) : StylingConfigBase(editText, htmlMode) {
data class Style(
val button: MaterialButton,
val spanClass: Class<*>,
@ -45,9 +83,6 @@ class TextStylingManager(private val app: App) {
) {
fun newInstance(): Any = spanClass.newInstance()
}
var watchStyleChecked = true
var watchSelectionChanged = true
}
fun attach(config: StylingConfig) {
@ -76,6 +111,14 @@ class TextStylingManager(private val app: App) {
onSelectionChanged(config, selectionStart, selectionEnd)
}
if (config.textHtml != null) {
config.editText.addTextChangedListener {
config.textHtml.text = getHtmlText(config)
}
config.textHtml.isVisible = true
config.textHtml.text = getHtmlText(config)
}
/*b.fontStyleBold.shapeAppearanceModel = b.fontStyleBold.shapeAppearanceModel
.toBuilder()
.setBottomLeftCornerSize(0f)
@ -91,15 +134,14 @@ class TextStylingManager(private val app: App) {
.build()*/
}
fun getHtmlText(config: StylingConfig, enableHtmlCompatible: Boolean = true): String {
fun getHtmlText(config: StylingConfigBase, htmlMode: HtmlMode = config.htmlMode): String {
val text = config.editText.text?.trimEnd() ?: return ""
val spanned = SpannableStringBuilder(text)
val htmlCompatibleMode = config.htmlCompatibleMode && enableHtmlCompatible
val toHtmlFlag = if (htmlCompatibleMode)
HtmlCompat.TO_HTML_PARAGRAPH_LINES_INDIVIDUAL
else
HtmlCompat.TO_HTML_PARAGRAPH_LINES_CONSECUTIVE
val toHtmlFlag = when (htmlMode) {
COMPATIBLE -> HtmlCompat.TO_HTML_PARAGRAPH_LINES_INDIVIDUAL
else -> HtmlCompat.TO_HTML_PARAGRAPH_LINES_CONSECUTIVE
}
// apparently setting the spans to a different Spannable calls the original EditText's
// onSelectionChanged with selectionStart=-1, which in effect unchecks the format toggles
@ -113,24 +155,41 @@ class TextStylingManager(private val app: App) {
if (spanStart == spanEnd && it::class.java in BetterHtml.customSpanClasses)
spanned.removeSpan(it)
}
var textHtml = HtmlCompat.toHtml(spanned, toHtmlFlag)
.replace("\n", "")
.replace(" dir=\"ltr\"", "")
.replace("</b><b>", "")
.replace("</i><i>", "")
.replace("</u><u>", "")
.replace("</sub><sub>", "")
.replace("</sup><sup>", "")
.replace("p style=\"margin-top:0; margin-bottom:0;\"", "p")
.replace("<br></p>", "</p><br>")
// replace multiple newlines so they convert fromHtml correctly
// this should not be breaking with htmlCompatibleMode == true,
// as line breaks cannot occur inside paragraphs with these flags
.replace(paragraphBrRegex, "</p>$1")
var textHtml = when (htmlMode) {
SIMPLE -> spanned
.replaceSpan(BoldSpan::class.java, "<b>", "</b>")
.replaceSpan(ItalicSpan::class.java, "<i>", "</i>")
.replaceSpan(UnderlineSpan::class.java, "<u>", "</u>")
.replaceSpan(StrikethroughSpan::class.java, "<s>", "</s>")
.replaceSpan(SubscriptSpan::class.java, "<sub>", "</sub>")
.replaceSpan(SuperscriptSpan::class.java, "<sup>", "</sup>")
.toString()
MARKDOWN -> spanned
.replaceSpan(BoldSpan::class.java, "**", "**")
.replaceSpan(ItalicSpan::class.java, "_", "_")
.replaceSpan(UnderlineSpan::class.java, "__", "__")
.replaceSpan(StrikethroughSpan::class.java, "~~", "~~")
.toString()
else -> HtmlCompat.toHtml(spanned, toHtmlFlag)
.replace("\n", "")
.replace(" dir=\"ltr\"", "")
.replace("</b><b>", "")
.replace("</i><i>", "")
.replace("</u><u>", "")
.replace("</sub><sub>", "")
.replace("</sup><sup>", "")
.replace("p style=\"margin-top:0; margin-bottom:0;\"", "p")
.replace("<br></p>", "</p><br>")
// replace multiple newlines so they convert fromHtml correctly
// this should not be breaking with htmlCompatibleMode == true,
// as line breaks cannot occur inside paragraphs with these flags
.replace(paragraphBrRegex, "</p>$1")
}
config.watchSelectionChanged = true
if (htmlCompatibleMode) {
if (htmlMode == COMPATIBLE) {
textHtml = textHtml
.replace("<br>", "<p>&nbsp;</p>")
.replace("<b>", "<strong>")
@ -147,7 +206,7 @@ class TextStylingManager(private val app: App) {
private fun onStyleChecked(
config: StylingConfig,
style: StylingConfig.Style,
isChecked: Boolean
isChecked: Boolean,
) {
if (!config.watchStyleChecked)
return

View File

@ -164,7 +164,6 @@
android:id="@+id/topic"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{event.topic}"
android:textAppearance="@style/NavView.TextView.Medium"
android:textIsSelectable="true"
tools:text="Rozdział II: Panowanie Piastów i Jagiellonów.Przeniesiony z 11 grudnia." />
@ -190,7 +189,6 @@
android:id="@+id/body"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{event.homeworkBody}"
android:textAppearance="@style/NavView.TextView.Medium"
android:textIsSelectable="true"
tools:text="Rozdział II: Panowanie Piastów i Jagiellonów.Przeniesiony z 11 grudnia." />

View File

@ -118,11 +118,14 @@
</LinearLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/topicLayout"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="@string/dialog_event_manual_topic">
android:hint="@string/dialog_event_manual_topic"
app:endIconMode="custom"
tools:endIconDrawable="@android:drawable/ic_menu_crop">
<pl.szczodrzynski.edziennik.utils.TextInputKeyboardEdit
android:id="@+id/topic"

View File

@ -8,7 +8,7 @@
<data>
<import type="android.view.View" />
<import type="androidx.core.text.HtmlCompat" />
<import type="pl.szczodrzynski.edziennik.utils.html.BetterHtml" />
<variable
name="message"
type="pl.szczodrzynski.edziennik.data.api.szkolny.response.RegisterAvailabilityStatus.Message" />
@ -43,7 +43,7 @@
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{HtmlCompat.fromHtml(message.title, HtmlCompat.FROM_HTML_MODE_LEGACY)}"
android:text="@{BetterHtml.fromHtml(context, message.title, false)}"
android:textAppearance="@style/NavView.TextView.Title"
tools:text="Dziennik nie działa" />
@ -52,7 +52,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@{HtmlCompat.fromHtml(message.contentLong, HtmlCompat.FROM_HTML_MODE_LEGACY)}"
android:text="@{BetterHtml.fromHtml(context, message.contentLong, false)}"
tools:text="Dziennik się zepsuł i nie działa, szkoda\n\n\nwiele linijek ma ten tekst" />
<com.google.android.material.button.MaterialButton

View File

@ -95,7 +95,7 @@
tools:visibility="visible" />
</LinearLayout>
<TextView
<com.mikepenz.iconics.view.IconicsTextView
android:id="@+id/addedBy"
android:layout_width="match_parent"
android:layout_height="wrap_content"

View File

@ -2,202 +2,106 @@
<!--
~ Copyright (c) Kuba Szczodrzyński 2019-12-22.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
<ScrollView 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">
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<data>
<import type="com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial" />
</data>
<ScrollView
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingBottom="40dp"><!-- half of the FAB's size -->
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/recipientsLayout"
style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox.Dense"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingBottom="40dp"><!-- half of the FAB's size -->
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/recipientsLayout"
style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox.Dense"
app:boxBackgroundColor="@android:color/transparent"
app:boxBackgroundMode="filled"
app:endIconDrawable="@drawable/dropdown_arrow"
app:endIconMode="custom">
<com.hootsuite.nachos.NachoTextView
android:id="@+id/recipients"
style="@style/Widget.MaterialComponents.TextInputEditText.FilledBox.Dense"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:boxBackgroundColor="@android:color/transparent"
app:boxBackgroundMode="filled"
app:endIconDrawable="@drawable/dropdown_arrow"
app:endIconMode="custom">
android:focusable="true"
android:focusableInTouchMode="true"
android:focusedByDefault="true"
android:hint="@string/messages_compose_to_hint" />
</com.google.android.material.textfield.TextInputLayout>
<com.hootsuite.nachos.NachoTextView
android:id="@+id/recipients"
style="@style/Widget.MaterialComponents.TextInputEditText.FilledBox.Dense"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:focusable="true"
android:focusableInTouchMode="true"
android:focusedByDefault="true"
android:hint="@string/messages_compose_to_hint" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/subjectLayout"
style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:boxBackgroundColor="@android:color/transparent"
app:boxBackgroundMode="filled"
app:counterEnabled="true"
tools:counterMaxLength="180">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/subjectLayout"
style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox"
<pl.szczodrzynski.edziennik.utils.TextInputKeyboardEdit
android:id="@+id/subject"
style="@style/Widget.MaterialComponents.TextInputEditText.FilledBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:boxBackgroundColor="@android:color/transparent"
app:boxBackgroundMode="filled"
app:counterEnabled="true"
tools:counterMaxLength="180">
android:focusable="true"
android:focusableInTouchMode="true"
android:hint="@string/messages_compose_subject_hint"
android:inputType="textCapSentences|textAutoCorrect|textShortMessage|textAutoComplete|textEmailSubject"
android:singleLine="true"
tools:text="kachoomba" />
</com.google.android.material.textfield.TextInputLayout>
<pl.szczodrzynski.edziennik.utils.TextInputKeyboardEdit
android:id="@+id/subject"
style="@style/Widget.MaterialComponents.TextInputEditText.FilledBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:focusable="true"
android:focusableInTouchMode="true"
android:hint="@string/messages_compose_subject_hint"
android:inputType="textCapSentences|textAutoCorrect|textShortMessage|textAutoComplete|textEmailSubject"
android:singleLine="true"
tools:text="kachoomba" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/textLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:boxBackgroundColor="@android:color/transparent"
app:boxBackgroundMode="filled"
app:counterEnabled="true"
tools:counterMaxLength="1983">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/textLayout"
<pl.szczodrzynski.edziennik.utils.TextInputKeyboardEdit
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:boxBackgroundColor="@android:color/transparent"
app:boxBackgroundMode="filled"
app:counterEnabled="true"
tools:counterMaxLength="1983">
android:ems="10"
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="start|top"
android:hint="@string/messages_compose_text_hint"
android:inputType="textMultiLine|textAutoCorrect|textLongMessage|textAutoComplete|textCapSentences"
android:minLines="3"
tools:text="Witam,\n\nchciałem przekazać bardzo ważną wiadomość.\nJest to cytat znanej osoby.\n\n&quot;To jest tak, ale nie. Pamiętaj żeby oczywiście&quot;\n\nCytat ma bardzo duże przesłanie i ogromny wpływ na dzisiejszą kulturę i rozwój współczesnej cywilizacji.\n\nJako zadanie domowe, należy wypisać 5 pierwszych liczb pierwszych. Uzasadnij decyzję, odwołując się do cytatu i 3 innych przykładów z literatury lub filmu.\n\nPozdrawiam,\nJa." />
</com.google.android.material.textfield.TextInputLayout>
<pl.szczodrzynski.edziennik.utils.TextInputKeyboardEdit
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="start|top"
android:hint="@string/messages_compose_text_hint"
android:inputType="textMultiLine|textAutoCorrect|textLongMessage|textAutoComplete|textCapSentences"
android:minLines="3"
tools:text="Witam,\n\nchciałem przekazać bardzo ważną wiadomość.\nJest to cytat znanej osoby.\n\n&quot;To jest tak, ale nie. Pamiętaj żeby oczywiście&quot;\n\nCytat ma bardzo duże przesłanie i ogromny wpływ na dzisiejszą kulturę i rozwój współczesnej cywilizacji.\n\nJako zadanie domowe, należy wypisać 5 pierwszych liczb pierwszych. Uzasadnij decyzję, odwołując się do cytatu i 3 innych przykładów z literatury lub filmu.\n\nPozdrawiam,\nJa." />
</com.google.android.material.textfield.TextInputLayout>
<include
android:id="@+id/fontStyle"
layout="@layout/styled_text_buttons" />
<LinearLayout
android:id="@+id/fontStyleLayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:orientation="vertical">
<Button
android:id="@+id/breakpoints"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="8dp"
android:text="Breakpoints!"
android:visibility="gone"
tools:visibility="visible" />
<com.google.android.material.button.MaterialButtonToggleGroup
android:id="@+id/fontStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="8dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/fontStyleBold"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/community_material_font_v5_8_55"
android:minWidth="48dp"
android:textSize="20sp"
app:iconPadding="0dp"
tools:text="\uf674" />
<com.google.android.material.button.MaterialButton
android:id="@+id/fontStyleItalic"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/community_material_font_v5_8_55"
android:minWidth="48dp"
android:textSize="20sp"
app:iconPadding="0dp"
tools:text="\uf691" />
<com.google.android.material.button.MaterialButton
android:id="@+id/fontStyleUnderline"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/community_material_font_v5_8_55"
android:minWidth="48dp"
android:textSize="20sp"
app:iconPadding="0dp"
tools:text="\uf6c5" />
<com.google.android.material.button.MaterialButton
android:id="@+id/fontStyleStrike"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/community_material_font_v5_8_55"
android:minWidth="48dp"
android:textSize="20sp"
app:iconPadding="0dp"
tools:text="\uf6b1" />
<com.google.android.material.button.MaterialButton
android:id="@+id/fontStyleSubscript"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/community_material_font_v5_8_55"
android:minWidth="48dp"
android:textSize="20sp"
app:iconPadding="0dp"
tools:text="\uf6b2" />
<com.google.android.material.button.MaterialButton
android:id="@+id/fontStyleSuperscript"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/community_material_font_v5_8_55"
android:minWidth="48dp"
android:textSize="20sp"
app:iconPadding="0dp"
tools:text="\uf6b3" />
</com.google.android.material.button.MaterialButtonToggleGroup>
<!-- TODO 2021-10-05: stack the group and button on top -->
<!-- android:layout_marginTop="-13dp" -->
<com.google.android.material.button.MaterialButton
android:id="@+id/fontStyleClear"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="8dp"
android:text="@string/messages_compose_style_clear" />
</LinearLayout>
<Button
android:id="@+id/breakpoints"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="8dp"
android:text="Breakpoints!"
android:visibility="gone"
tools:visibility="visible" />
<TextView
android:id="@+id/textHtml"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:fontFamily="monospace"
android:visibility="gone"
tools:text="&lt;p&gt;Witam,&lt;/p&gt;To jest bardzo długi tekst żeby sprawdzić ok działa"
tools:visibility="visible" />
</LinearLayout>
</ScrollView>
</layout>
<TextView
android:id="@+id/textHtml"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:fontFamily="monospace"
android:visibility="gone"
tools:text="&lt;p&gt;Witam,&lt;/p&gt;To jest bardzo długi tekst żeby sprawdzić ok działa"
tools:visibility="visible" />
</LinearLayout>
</ScrollView>

View File

@ -0,0 +1,99 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kuba Szczodrzyński 2021-10-11.
-->
<LinearLayout 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"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:orientation="vertical">
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.button.MaterialButtonToggleGroup
android:id="@+id/styles"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingHorizontal="8dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/bold"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/community_material_font_v5_8_55"
android:minWidth="48dp"
android:textSize="20sp"
app:iconPadding="0dp"
tools:text="\uf674" />
<com.google.android.material.button.MaterialButton
android:id="@+id/italic"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/community_material_font_v5_8_55"
android:minWidth="48dp"
android:textSize="20sp"
app:iconPadding="0dp"
tools:text="\uf691" />
<com.google.android.material.button.MaterialButton
android:id="@+id/underline"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/community_material_font_v5_8_55"
android:minWidth="48dp"
android:textSize="20sp"
app:iconPadding="0dp"
tools:text="\uf6c5" />
<com.google.android.material.button.MaterialButton
android:id="@+id/strike"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/community_material_font_v5_8_55"
android:minWidth="48dp"
android:textSize="20sp"
app:iconPadding="0dp"
tools:text="\uf6b1" />
<com.google.android.material.button.MaterialButton
android:id="@+id/subscript"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/community_material_font_v5_8_55"
android:minWidth="48dp"
android:textSize="20sp"
app:iconPadding="0dp"
tools:text="\uf6b2" />
<com.google.android.material.button.MaterialButton
android:id="@+id/superscript"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/community_material_font_v5_8_55"
android:minWidth="48dp"
android:textSize="20sp"
app:iconPadding="0dp"
tools:text="\uf6b3" />
</com.google.android.material.button.MaterialButtonToggleGroup>
</HorizontalScrollView>
<!-- TODO 2021-10-05: stack the group and button on top -->
<!-- android:layout_marginTop="-13dp" -->
<com.google.android.material.button.MaterialButton
android:id="@+id/clear"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="8dp"
android:text="@string/styled_text_clear_button" />
</LinearLayout>

View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kuba Szczodrzyński 2021-10-11.
-->
<ScrollView 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"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp">
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="250dp"
app:counterEnabled="true">
<pl.szczodrzynski.edziennik.utils.TextInputKeyboardEdit
android:id="@+id/editText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="top"
android:inputType="textMultiLine|textAutoCorrect|textLongMessage|textAutoComplete|textCapSentences"
android:minLines="10"
tools:text="Witam,\n\nchciałem przekazać bardzo ważną wiadomość.\nJest to cytat znanej osoby.\n\n&quot;To jest tak, ale nie. Pamiętaj żeby oczywiście&quot;\n\nCytat ma bardzo duże przesłanie i ogromny wpływ na dzisiejszą kulturę i rozwój współczesnej cywilizacji.\n\nJako zadanie domowe, należy wypisać 5 pierwszych liczb pierwszych. Uzasadnij decyzję, odwołując się do cytatu i 3 innych przykładów z literatury lub filmu.\n\nPozdrawiam,\nJa." />
</com.google.android.material.textfield.TextInputLayout>
<include
android:id="@+id/fontStyle"
layout="@layout/styled_text_buttons" />
<TextView
android:id="@+id/htmlText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:fontFamily="monospace"
android:visibility="gone" />
</LinearLayout>
</ScrollView>

View File

@ -1470,7 +1470,7 @@
<string name="hint_style_strike">Przekreślenie</string>
<string name="hint_style_subscript">Indeks dolny</string>
<string name="hint_style_superscript">Indeks górny</string>
<string name="messages_compose_style_clear">Wyczyść format</string>
<string name="styled_text_clear_button">Wyczyść format</string>
<string name="hint_message_star">Oznacz gwiazdką</string>
<string name="message_forward">Przekaż dalej</string>
<string name="message_delete">Usuń</string>
@ -1491,4 +1491,5 @@
<string name="messages_compose_discard_draft_text">Czy chcesz odrzucić zapisaną wersję wiadomości? Spowoduje to również anulowanie wprowadzonych zmian i usunięcie wiadomości.</string>
<string name="login_mobidziennik_server_prefix">https://</string>
<string name="login_mobidziennik_server_suffix">.mobidziennik.pl/</string>
<string name="styled_text_dialog_title">Edytuj tekst</string>
</resources>