forked from github/szkolny
[Messages] Add support for starring messages. (#86)
* [UI/Messages] Add stars to message list layout. * [Messages] Move text styling-related code to a manager. * [Messages] Implement starring messages. Move more code to the manager. Update UI. * [UI] Add padding to the no data text. * [Messages] Fix checking sent message recipient read state. * [Messages] Add star icon padding.
This commit is contained in:
parent
a6aca42c8c
commit
44263ac95f
@ -72,6 +72,8 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
|
||||
val attendanceManager by lazy { AttendanceManager(this) }
|
||||
val buildManager by lazy { BuildManager(this) }
|
||||
val availabilityManager by lazy { AvailabilityManager(this) }
|
||||
val textStylingManager by lazy { TextStylingManager(this) }
|
||||
val messageManager by lazy { MessageManager(this) }
|
||||
|
||||
val db
|
||||
get() = App.db
|
||||
|
@ -814,6 +814,8 @@ fun View.attachToastHint(stringRes: Int) = onLongClick {
|
||||
true
|
||||
}
|
||||
|
||||
fun View.detachToastHint() = setOnLongClickListener(null)
|
||||
|
||||
fun <T> LiveData<T>.observeOnce(lifecycleOwner: LifecycleOwner, observer: Observer<T>) {
|
||||
observe(lifecycleOwner, object : Observer<T> {
|
||||
override fun onChanged(t: T?) {
|
||||
|
@ -29,7 +29,7 @@ abstract class MessageDao : BaseDao<Message, MessageFull> {
|
||||
LEFT JOIN metadata ON messageId = thingId AND thingType = ${Metadata.TYPE_MESSAGE} AND metadata.profileId = messages.profileId
|
||||
"""
|
||||
|
||||
private const val ORDER_BY = """ORDER BY messageIsPinned, addedDate DESC"""
|
||||
private const val ORDER_BY = """ORDER BY messageIsPinned DESC, addedDate DESC"""
|
||||
}
|
||||
|
||||
private val selective by lazy { MessageDaoSelective(App.db) }
|
||||
|
@ -41,7 +41,7 @@ open class Message(
|
||||
}
|
||||
|
||||
@ColumnInfo(name = "messageIsPinned")
|
||||
var isPinned: Boolean = false
|
||||
var isStarred: Boolean = false
|
||||
|
||||
var hasAttachments = false // if the attachments are not yet downloaded but we already know there are some
|
||||
get() = field || attachmentIds.isNotNullNorEmpty()
|
||||
|
@ -31,6 +31,8 @@ class MessageFull(
|
||||
var filterWeight = 0
|
||||
@Ignore
|
||||
var searchHighlightText: CharSequence? = null
|
||||
@Ignore
|
||||
var readByEveryone = true
|
||||
|
||||
// metadata
|
||||
var seen = false
|
||||
|
@ -27,7 +27,6 @@ import pl.szczodrzynski.edziennik.data.db.entity.LoginStore
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.LoginStore.Companion.LOGIN_TYPE_IDZIENNIK
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_DELETED
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_RECEIVED
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_SENT
|
||||
import pl.szczodrzynski.edziennik.data.db.full.MessageFull
|
||||
import pl.szczodrzynski.edziennik.databinding.MessageFragmentBinding
|
||||
import pl.szczodrzynski.edziennik.ui.dialogs.MessagesConfigDialog
|
||||
@ -53,6 +52,8 @@ class MessageFragment : Fragment(), CoroutineScope {
|
||||
override val coroutineContext: CoroutineContext
|
||||
get() = job + Dispatchers.Main
|
||||
|
||||
private val manager
|
||||
get() = app.messageManager
|
||||
private lateinit var message: MessageFull
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
@ -92,6 +93,35 @@ class MessageFragment : Fragment(), CoroutineScope {
|
||||
it.maxLines = if (it.maxLines == 30) 2 else 30
|
||||
}
|
||||
|
||||
val replyDrawable = IconicsDrawable(activity, CommunityMaterial.Icon3.cmd_reply_outline).apply {
|
||||
sizeDp = 24
|
||||
colorAttr(activity, android.R.attr.textColorPrimary)
|
||||
}
|
||||
val forwardDrawable = IconicsDrawable(activity, CommunityMaterial.Icon.cmd_arrow_right).apply {
|
||||
sizeDp = 24
|
||||
colorAttr(activity, android.R.attr.textColorPrimary)
|
||||
}
|
||||
val deleteDrawable = IconicsDrawable(activity, CommunityMaterial.Icon.cmd_delete_outline).apply {
|
||||
sizeDp = 24
|
||||
colorAttr(activity, android.R.attr.textColorPrimary)
|
||||
}
|
||||
val downloadDrawable = IconicsDrawable(activity, CommunityMaterial.Icon.cmd_download_outline).apply {
|
||||
sizeDp = 24
|
||||
colorAttr(activity, android.R.attr.textColorPrimary)
|
||||
}
|
||||
b.replyButton.setCompoundDrawables(null, replyDrawable, null, null)
|
||||
b.forwardButton.setCompoundDrawables(null, forwardDrawable, null, null)
|
||||
b.deleteButton.setCompoundDrawables(null, deleteDrawable, null, null)
|
||||
b.downloadButton.setCompoundDrawables(null, downloadDrawable, null, null)
|
||||
|
||||
b.messageStar.onClick {
|
||||
launch {
|
||||
manager.starMessage(message, !message.isStarred)
|
||||
manager.setStarIcon(b.messageStar, message)
|
||||
}
|
||||
}
|
||||
b.messageStar.attachToastHint(R.string.hint_message_star)
|
||||
|
||||
b.replyButton.onClick {
|
||||
activity.loadTarget(MainActivity.TARGET_MESSAGES_COMPOSE, Bundle(
|
||||
"message" to app.gson.toJson(message),
|
||||
@ -110,10 +140,7 @@ class MessageFragment : Fragment(), CoroutineScope {
|
||||
.setMessage(R.string.messages_delete_confirmation_text)
|
||||
.setPositiveButton(R.string.ok) { _, _ ->
|
||||
launch {
|
||||
message.type = TYPE_DELETED
|
||||
withContext(Dispatchers.Default) {
|
||||
app.db.messageDao().replace(message)
|
||||
}
|
||||
manager.markAsDeleted(message)
|
||||
Toast.makeText(activity, "Wiadomość przeniesiona do usuniętych", Toast.LENGTH_SHORT).show()
|
||||
activity.navigateUp()
|
||||
}
|
||||
@ -127,59 +154,10 @@ class MessageFragment : Fragment(), CoroutineScope {
|
||||
}
|
||||
|
||||
launch {
|
||||
|
||||
val messageString = arguments?.getString("message")
|
||||
val messageId = arguments?.getLong("messageId")
|
||||
if (messageId == null) {
|
||||
message = manager.getMessage(App.profileId, arguments) ?: run {
|
||||
activity.navigateUp()
|
||||
return@launch
|
||||
}
|
||||
|
||||
val msg = withContext(Dispatchers.Default) {
|
||||
|
||||
val msg =
|
||||
if (messageString != null)
|
||||
app.gson.fromJson(messageString, MessageFull::class.java)?.also {
|
||||
if (arguments?.getLong("sentDate") ?: 0L > 0L)
|
||||
it.addedDate = arguments?.getLong("sentDate") ?: 0L
|
||||
}
|
||||
else
|
||||
app.db.messageDao().getByIdNow(App.profileId, messageId)
|
||||
|
||||
if (msg == null)
|
||||
return@withContext null
|
||||
|
||||
// make recipients ID-unique
|
||||
// this helps when multiple profiles receive the same message
|
||||
// (there are multiple -1 recipients for the same message ID)
|
||||
val recipientsDistinct =
|
||||
msg.recipients?.distinctBy { it.id } ?: return@withContext null
|
||||
msg.recipients?.clear()
|
||||
msg.recipients?.addAll(recipientsDistinct)
|
||||
|
||||
// load recipients in sent messages
|
||||
val teachers by lazy { app.db.teacherDao().getAllNow(App.profileId) }
|
||||
msg.recipients?.forEach { recipient ->
|
||||
if (recipient.fullName == null) {
|
||||
recipient.fullName = teachers.firstOrNull { it.id == recipient.id }?.fullName ?: ""
|
||||
}
|
||||
}
|
||||
|
||||
if (msg.type == TYPE_SENT && msg.senderName == null) {
|
||||
msg.senderName = app.profile.accountName ?: app.profile.studentNameLong
|
||||
}
|
||||
|
||||
//msg.recipients = app.db.messageRecipientDao().getAllByMessageId(msg.profileId, msg.id)
|
||||
if (msg.body != null && !msg.seen) {
|
||||
app.db.metadataDao().setSeen(msg.profileId, msg, true)
|
||||
}
|
||||
return@withContext msg
|
||||
} ?: run {
|
||||
activity.navigateUp()
|
||||
return@launch
|
||||
}
|
||||
|
||||
message = msg
|
||||
b.subject.text = message.subject
|
||||
checkMessage()
|
||||
}
|
||||
@ -208,7 +186,6 @@ class MessageFragment : Fragment(), CoroutineScope {
|
||||
}
|
||||
}
|
||||
|
||||
val readByAll = checkRecipients()
|
||||
if (app.profile.loginStoreType == LoginStore.LOGIN_TYPE_VULCAN) {
|
||||
// vulcan: change message status or download attachments
|
||||
if (message.type == TYPE_RECEIVED && !message.seen || message.attachmentIds == null) {
|
||||
@ -216,7 +193,7 @@ class MessageFragment : Fragment(), CoroutineScope {
|
||||
return
|
||||
}
|
||||
}
|
||||
else if (!readByAll) {
|
||||
else if (!message.readByEveryone) {
|
||||
// if a sent msg is not read by everyone, download it again to check the read status
|
||||
EdziennikTask.messageGet(App.profileId, message).enqueue(activity)
|
||||
return
|
||||
@ -225,16 +202,6 @@ class MessageFragment : Fragment(), CoroutineScope {
|
||||
showMessage()
|
||||
}
|
||||
|
||||
private fun checkRecipients(): Boolean {
|
||||
message.recipients?.forEach { recipient ->
|
||||
if (recipient.id == -1L)
|
||||
recipient.fullName = app.profile.accountName ?: app.profile.studentNameLong ?: ""
|
||||
if (message.type == TYPE_SENT && recipient.readDate < 1)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun showMessage() {
|
||||
b.body.text = MessagesUtils.htmlToSpannable(activity, message.body.toString())
|
||||
b.date.text = getString(R.string.messages_date_time_format, Date.fromMillis(message.addedDate).formattedStringShort, Time.fromMillis(message.addedDate).stringHM)
|
||||
@ -245,6 +212,8 @@ class MessageFragment : Fragment(), CoroutineScope {
|
||||
|
||||
b.subject.text = message.subject
|
||||
|
||||
manager.setStarIcon(b.messageStar, message)
|
||||
|
||||
b.replyButton.isVisible = message.type == TYPE_RECEIVED || message.type == TYPE_DELETED
|
||||
b.deleteButton.isVisible = message.type == TYPE_RECEIVED
|
||||
if (message.type == TYPE_RECEIVED || message.type == TYPE_DELETED) {
|
||||
@ -255,9 +224,9 @@ class MessageFragment : Fragment(), CoroutineScope {
|
||||
fabIcon = CommunityMaterial.Icon3.cmd_reply_outline
|
||||
}
|
||||
|
||||
setFabOnClickListener(View.OnClickListener {
|
||||
setFabOnClickListener {
|
||||
b.replyButton.performClick()
|
||||
})
|
||||
}
|
||||
}
|
||||
activity.gainAttentionFAB()
|
||||
}
|
||||
|
@ -22,7 +22,8 @@ import kotlin.coroutines.CoroutineContext
|
||||
class MessagesAdapter(
|
||||
val activity: AppCompatActivity,
|
||||
val teachers: List<Teacher>,
|
||||
val onItemClick: ((item: MessageFull) -> Unit)? = null
|
||||
val onItemClick: ((item: MessageFull) -> Unit)? = null,
|
||||
val onStarClick: ((item: MessageFull) -> Unit)? = null,
|
||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>(), CoroutineScope, Filterable {
|
||||
companion object {
|
||||
private const val TAG = "MessagesAdapter"
|
||||
@ -32,13 +33,17 @@ class MessagesAdapter(
|
||||
|
||||
private val app = activity.applicationContext as App
|
||||
// optional: place the manager here
|
||||
internal val manager
|
||||
get() = app.messageManager
|
||||
|
||||
private val job = Job()
|
||||
override val coroutineContext: CoroutineContext
|
||||
get() = job + Dispatchers.Main
|
||||
|
||||
// mutable var changed by the filter
|
||||
var items = listOf<Any>()
|
||||
var allItems = listOf<Any>()
|
||||
// mutable list managed by the fragment
|
||||
val allItems = mutableListOf<Any>()
|
||||
val typefaceNormal: Typeface by lazy { Typeface.create(Typeface.DEFAULT, Typeface.NORMAL) }
|
||||
val typefaceBold: Typeface by lazy { Typeface.create(Typeface.DEFAULT, Typeface.BOLD) }
|
||||
|
||||
|
@ -12,13 +12,11 @@ import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView.NO_POSITION
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.*
|
||||
import pl.szczodrzynski.edziennik.*
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Message
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Teacher
|
||||
import pl.szczodrzynski.edziennik.data.db.full.MessageFull
|
||||
import pl.szczodrzynski.edziennik.databinding.MessagesListFragmentBinding
|
||||
import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyFragment
|
||||
import pl.szczodrzynski.edziennik.ui.modules.messages.models.MessagesSearch
|
||||
@ -33,13 +31,15 @@ class MessagesListFragment : LazyFragment(), CoroutineScope {
|
||||
private lateinit var app: App
|
||||
private lateinit var activity: MainActivity
|
||||
private lateinit var b: MessagesListFragmentBinding
|
||||
private var adapter: MessagesAdapter? = null
|
||||
private lateinit var adapter: MessagesAdapter
|
||||
|
||||
private val job: Job = Job()
|
||||
override val coroutineContext: CoroutineContext
|
||||
get() = job + Dispatchers.Main
|
||||
|
||||
// local/private variables go here
|
||||
private val manager
|
||||
get() = app.messageManager
|
||||
var teachers = listOf<Teacher>()
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
@ -54,22 +54,28 @@ class MessagesListFragment : LazyFragment(), CoroutineScope {
|
||||
val messageType = arguments.getInt("messageType", Message.TYPE_RECEIVED)
|
||||
var topPosition = arguments.getInt("topPosition", NO_POSITION)
|
||||
var bottomPosition = arguments.getInt("bottomPosition", NO_POSITION)
|
||||
val searchText = arguments.getString("searchText", "")
|
||||
val searchText = arguments?.getString("searchText")
|
||||
|
||||
teachers = withContext(Dispatchers.Default) {
|
||||
app.db.teacherDao().getAllNow(App.profileId)
|
||||
}
|
||||
|
||||
adapter = MessagesAdapter(activity, teachers) {
|
||||
adapter = MessagesAdapter(activity, teachers, onItemClick = {
|
||||
activity.loadTarget(MainActivity.TARGET_MESSAGES_DETAILS, Bundle(
|
||||
"messageId" to it.id
|
||||
))
|
||||
}, onStarClick = {
|
||||
this@MessagesListFragment.launch {
|
||||
manager.starMessage(it, !it.isStarred)
|
||||
}
|
||||
})
|
||||
|
||||
app.db.messageDao().getAllByType(App.profileId, messageType).observe(this@MessagesListFragment, Observer { messages ->
|
||||
if (!isAdded) return@Observer
|
||||
if (!isAdded || !this@MessagesListFragment::adapter.isInitialized)
|
||||
return@Observer
|
||||
|
||||
messages.forEach { message ->
|
||||
// uh oh, so these are the workarounds ??
|
||||
message.recipients?.removeAll { it.profileId != message.profileId }
|
||||
message.recipients?.forEach { recipient ->
|
||||
if (recipient.fullName == null) {
|
||||
@ -78,35 +84,46 @@ class MessagesListFragment : LazyFragment(), CoroutineScope {
|
||||
}
|
||||
}
|
||||
|
||||
// load & configure the adapter
|
||||
val items = messages.toMutableList<Any>()
|
||||
items.add(0, MessagesSearch().also {
|
||||
it.searchText = searchText
|
||||
})
|
||||
|
||||
adapter?.items = items
|
||||
adapter?.allItems = items
|
||||
|
||||
if (items.isNotNullNorEmpty() && b.list.adapter == null) {
|
||||
if (searchText.isNotBlank())
|
||||
adapter?.filter?.filter(searchText) {
|
||||
b.list.adapter = adapter
|
||||
// show/hide relevant views
|
||||
setSwipeToRefresh(messageType in Message.TYPE_RECEIVED..Message.TYPE_SENT)
|
||||
b.progressBar.isVisible = false
|
||||
b.list.isVisible = messages.isNotEmpty()
|
||||
b.noData.isVisible = messages.isEmpty()
|
||||
if (messages.isEmpty()) {
|
||||
return@Observer
|
||||
}
|
||||
else
|
||||
b.list.adapter = adapter
|
||||
|
||||
if (adapter.allItems.isEmpty()) {
|
||||
// items empty - add the search field
|
||||
adapter.allItems += MessagesSearch().also {
|
||||
it.searchText = searchText ?: ""
|
||||
}
|
||||
} else {
|
||||
// items not empty - remove all messages
|
||||
adapter.allItems.removeAll { it is MessageFull }
|
||||
}
|
||||
// add all messages
|
||||
adapter.allItems.addAll(messages)
|
||||
|
||||
// configure the adapter & recycler view
|
||||
if (b.list.adapter == null) {
|
||||
b.list.apply {
|
||||
setHasFixedSize(true)
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
addItemDecoration(SimpleDividerItemDecoration(context))
|
||||
if (messageType in Message.TYPE_RECEIVED..Message.TYPE_SENT)
|
||||
addOnScrollListener(onScrollListener)
|
||||
this.adapter = this@MessagesListFragment.adapter
|
||||
}
|
||||
}
|
||||
|
||||
setSwipeToRefresh(messageType in Message.TYPE_RECEIVED..Message.TYPE_SENT && items.isNullOrEmpty())
|
||||
val layoutManager = (b.list.layoutManager as? LinearLayoutManager) ?: return@Observer
|
||||
|
||||
(b.list.layoutManager as? LinearLayoutManager)?.let { layoutManager ->
|
||||
// reapply the filter
|
||||
val searchItem = adapter.items.firstOrNull { it is MessagesSearch } as? MessagesSearch
|
||||
adapter.filter.filter(searchText ?: searchItem?.searchText, null)
|
||||
|
||||
// restore the previously saved scroll position
|
||||
if (topPosition != NO_POSITION && topPosition > layoutManager.findLastCompletelyVisibleItemPosition()) {
|
||||
b.list.scrollToPosition(topPosition)
|
||||
} else if (bottomPosition != NO_POSITION && bottomPosition < layoutManager.findFirstVisibleItemPosition()) {
|
||||
@ -114,26 +131,15 @@ class MessagesListFragment : LazyFragment(), CoroutineScope {
|
||||
}
|
||||
topPosition = NO_POSITION
|
||||
bottomPosition = NO_POSITION
|
||||
}
|
||||
|
||||
// show/hide relevant views
|
||||
b.progressBar.isVisible = false
|
||||
if (items.isNullOrEmpty()) {
|
||||
b.list.isVisible = false
|
||||
b.noData.isVisible = true
|
||||
} else {
|
||||
b.list.isVisible = true
|
||||
b.noData.isVisible = false
|
||||
}
|
||||
})
|
||||
}; return true }
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
if (!isAdded)
|
||||
if (!isAdded || !this::adapter.isInitialized)
|
||||
return
|
||||
val layoutManager = (b.list.layoutManager as? LinearLayoutManager)
|
||||
val searchItem = adapter?.items?.firstOrNull { it is MessagesSearch } as? MessagesSearch
|
||||
val searchItem = adapter.items.firstOrNull { it is MessagesSearch } as? MessagesSearch
|
||||
|
||||
onPageDestroy?.invoke(position, Bundle(
|
||||
"topPosition" to layoutManager?.findFirstVisibleItemPosition(),
|
||||
|
@ -13,11 +13,9 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.AutoCompleteTextView
|
||||
import androidx.core.text.HtmlCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.google.android.material.button.MaterialButtonToggleGroup
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.hootsuite.nachos.chip.ChipInfo
|
||||
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
|
||||
@ -27,6 +25,7 @@ import org.greenrobot.eventbus.Subscribe
|
||||
import org.greenrobot.eventbus.ThreadMode
|
||||
import pl.szczodrzynski.edziennik.*
|
||||
import pl.szczodrzynski.edziennik.data.api.ERROR_MESSAGE_NOT_SENT
|
||||
import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_MOBIDZIENNIK
|
||||
import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask
|
||||
import pl.szczodrzynski.edziennik.data.api.events.MessageSentEvent
|
||||
import pl.szczodrzynski.edziennik.data.api.events.RecipientListGetEvent
|
||||
@ -39,16 +38,12 @@ import pl.szczodrzynski.edziennik.ui.dialogs.MessagesConfigDialog
|
||||
import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesUtils
|
||||
import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesUtils.getProfileImage
|
||||
import pl.szczodrzynski.edziennik.utils.Themes
|
||||
import pl.szczodrzynski.edziennik.utils.html.BetterHtml
|
||||
import pl.szczodrzynski.edziennik.utils.managers.TextStylingManager.StylingConfig
|
||||
import pl.szczodrzynski.edziennik.utils.models.Date
|
||||
import pl.szczodrzynski.edziennik.utils.models.Time
|
||||
import pl.szczodrzynski.edziennik.utils.span.BoldSpan
|
||||
import pl.szczodrzynski.edziennik.utils.span.ItalicSpan
|
||||
import pl.szczodrzynski.edziennik.utils.span.SubscriptSizeSpan
|
||||
import pl.szczodrzynski.edziennik.utils.span.SuperscriptSizeSpan
|
||||
import pl.szczodrzynski.edziennik.utils.span.*
|
||||
import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.text.replace
|
||||
|
||||
class MessagesComposeFragment : Fragment(), CoroutineScope {
|
||||
companion object {
|
||||
@ -63,14 +58,17 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
|
||||
override val coroutineContext: CoroutineContext
|
||||
get() = job + Dispatchers.Main
|
||||
|
||||
// private val manager
|
||||
// get() = app.messageManager
|
||||
private val textStylingManager
|
||||
get() = app.textStylingManager
|
||||
private val profileConfig by lazy { app.config.forProfile().ui }
|
||||
private val greetingText
|
||||
get() = profileConfig.messagesGreetingText ?: "\n\nZ poważaniem\n${app.profile.accountOwnerName}"
|
||||
|
||||
private var teachers = mutableListOf<Teacher>()
|
||||
|
||||
private var watchFormatChecked = true
|
||||
private var watchSelectionChanged = true
|
||||
private lateinit var stylingConfig: StylingConfig
|
||||
private val enableTextStyling
|
||||
get() = app.profile.loginStoreType != LoginStore.LOGIN_TYPE_VULCAN
|
||||
|
||||
@ -101,13 +99,6 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
|
||||
// do your job
|
||||
}
|
||||
|
||||
if (App.devMode) {
|
||||
b.textHtml.isVisible = true
|
||||
b.text.addTextChangedListener {
|
||||
b.textHtml.text = getHtmlText()
|
||||
}
|
||||
}
|
||||
|
||||
activity.bottomSheet.prependItem(
|
||||
BottomSheetPrimaryItem(true)
|
||||
.withTitle(R.string.menu_messages_config)
|
||||
@ -126,48 +117,11 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
|
||||
}
|
||||
}
|
||||
|
||||
private fun getHtmlText(): String {
|
||||
val text = b.text.text ?: return ""
|
||||
|
||||
// apparently setting the spans to a different Spannable calls the original EditText's
|
||||
// onSelectionChanged with selectionStart=-1, which in effect unchecks the format toggles
|
||||
watchSelectionChanged = false
|
||||
var textHtml = if (enableTextStyling) {
|
||||
val spanned = SpannableString(text)
|
||||
// remove zero-length spans, as they seem to affect
|
||||
// the whole line when converted to HTML
|
||||
spanned.getSpans(0, spanned.length, Any::class.java).forEach {
|
||||
val spanStart = spanned.getSpanStart(it)
|
||||
val spanEnd = spanned.getSpanEnd(it)
|
||||
if (spanStart == spanEnd && it::class.java in BetterHtml.customSpanClasses)
|
||||
spanned.removeSpan(it)
|
||||
}
|
||||
HtmlCompat.toHtml(spanned, HtmlCompat.TO_HTML_PARAGRAPH_LINES_INDIVIDUAL)
|
||||
.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")
|
||||
} else {
|
||||
text.toString()
|
||||
}
|
||||
watchSelectionChanged = true
|
||||
|
||||
if (app.profile.loginStoreType == LoginStore.LOGIN_TYPE_MOBIDZIENNIK) {
|
||||
textHtml = textHtml
|
||||
.replace("<br>", "<p> </p>")
|
||||
.replace("<b>", "<strong>")
|
||||
.replace("</b>", "</strong>")
|
||||
.replace("<i>", "<em>")
|
||||
.replace("</i>", "</em>")
|
||||
.replace("<u>", "<span style=\"text-decoration: underline;\">")
|
||||
.replace("</u>", "</span>")
|
||||
}
|
||||
|
||||
return textHtml
|
||||
private fun getMessageBody(): String {
|
||||
return if (enableTextStyling)
|
||||
textStylingManager.getHtmlText(stylingConfig)
|
||||
else
|
||||
b.text.text?.toString() ?: ""
|
||||
}
|
||||
|
||||
private fun getRecipientList() {
|
||||
@ -185,80 +139,6 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
private fun onFormatChecked(
|
||||
group: MaterialButtonToggleGroup,
|
||||
checkedId: Int,
|
||||
isChecked: Boolean
|
||||
) {
|
||||
if (!watchFormatChecked)
|
||||
return
|
||||
val span = when (checkedId) {
|
||||
R.id.fontStyleBold -> BoldSpan()
|
||||
R.id.fontStyleItalic -> ItalicSpan()
|
||||
R.id.fontStyleUnderline -> UnderlineSpan()
|
||||
R.id.fontStyleStrike -> StrikethroughSpan()
|
||||
R.id.fontStyleSubscript -> SubscriptSizeSpan(10, dip = true)
|
||||
R.id.fontStyleSuperscript -> SuperscriptSizeSpan(10, dip = true)
|
||||
else -> return
|
||||
}
|
||||
|
||||
// see comments in getHtmlText()
|
||||
watchSelectionChanged = false
|
||||
if (isChecked)
|
||||
BetterHtml.applyFormat(span = span, editText = b.text)
|
||||
else
|
||||
BetterHtml.removeFormat(span = span, editText = b.text)
|
||||
watchSelectionChanged = true
|
||||
|
||||
if (App.devMode)
|
||||
b.textHtml.text = getHtmlText()
|
||||
}
|
||||
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
private fun onFormatClear(view: View) {
|
||||
// shortened version on onFormatChecked(), removing all spans
|
||||
watchSelectionChanged = false
|
||||
BetterHtml.removeFormat(span = null, editText = b.text)
|
||||
watchSelectionChanged = true
|
||||
if (App.devMode)
|
||||
b.textHtml.text = getHtmlText()
|
||||
// force update of text style toggle states
|
||||
onSelectionChanged(b.text.selectionStart, b.text.selectionEnd)
|
||||
}
|
||||
|
||||
private fun onSelectionChanged(selectionStart: Int, selectionEnd: Int) {
|
||||
if (!watchSelectionChanged)
|
||||
return
|
||||
val spanned = b.text.text ?: return
|
||||
val spans = spanned.getSpans(selectionStart, selectionEnd, Any::class.java).mapNotNull {
|
||||
if (it::class.java !in BetterHtml.customSpanClasses)
|
||||
return@mapNotNull null
|
||||
val spanStart = spanned.getSpanStart(it)
|
||||
val spanEnd = spanned.getSpanEnd(it)
|
||||
// remove 0-length spans after navigating out of them
|
||||
if (spanStart == spanEnd)
|
||||
spanned.removeSpan(it)
|
||||
else if (spanned.getSpanFlags(it) hasSet SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
spanned.setSpan(it, spanStart, spanEnd, SPAN_EXCLUSIVE_INCLUSIVE)
|
||||
|
||||
// names are helpful here
|
||||
val isNotAfterWord = selectionEnd <= spanEnd
|
||||
val isSelectionInWord = selectionStart != selectionEnd && selectionStart >= spanStart
|
||||
val isCursorInWord = selectionStart == selectionEnd && selectionStart > spanStart
|
||||
val isChecked = (isCursorInWord || isSelectionInWord) && isNotAfterWord
|
||||
if (isChecked) it::class.java else null
|
||||
}
|
||||
watchFormatChecked = false
|
||||
b.fontStyleBold.isChecked = BoldSpan::class.java in spans
|
||||
b.fontStyleItalic.isChecked = ItalicSpan::class.java in spans
|
||||
b.fontStyleUnderline.isChecked = UnderlineSpan::class.java in spans
|
||||
b.fontStyleStrike.isChecked = StrikethroughSpan::class.java in spans
|
||||
b.fontStyleSubscript.isChecked = SubscriptSizeSpan::class.java in spans
|
||||
b.fontStyleSuperscript.isChecked = SuperscriptSizeSpan::class.java in spans
|
||||
watchFormatChecked = true
|
||||
}
|
||||
|
||||
private fun createView() {
|
||||
b.recipientsLayout.setBoxCornerRadii(0f, 0f, 0f, 0f)
|
||||
b.subjectLayout.setBoxCornerRadii(0f, 0f, 0f, 0f)
|
||||
@ -318,54 +198,66 @@ 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,
|
||||
),
|
||||
)
|
||||
|
||||
stylingConfig = StylingConfig(
|
||||
editText = b.text,
|
||||
fontStyleGroup = b.fontStyle,
|
||||
fontStyleClear = b.fontStyleClear,
|
||||
styles = styles,
|
||||
textHtml = if (App.devMode) b.textHtml else null,
|
||||
htmlCompatibleMode = app.profile.loginStoreType == LOGIN_TYPE_MOBIDZIENNIK,
|
||||
)
|
||||
|
||||
b.fontStyleLayout.isVisible = enableTextStyling
|
||||
b.fontStyleBold.isEnabled = false
|
||||
b.fontStyleItalic.isEnabled = false
|
||||
b.fontStyleUnderline.isEnabled = false
|
||||
b.fontStyleStrike.isEnabled = false
|
||||
b.fontStyleSubscript.isEnabled = false
|
||||
b.fontStyleSuperscript.isEnabled = false
|
||||
b.fontStyleClear.isEnabled = false
|
||||
b.text.setOnFocusChangeListener { _, hasFocus ->
|
||||
b.fontStyleBold.isEnabled = hasFocus
|
||||
b.fontStyleItalic.isEnabled = hasFocus
|
||||
b.fontStyleUnderline.isEnabled = hasFocus
|
||||
b.fontStyleStrike.isEnabled = hasFocus
|
||||
b.fontStyleSubscript.isEnabled = hasFocus
|
||||
b.fontStyleSuperscript.isEnabled = hasFocus
|
||||
b.fontStyleClear.isEnabled = hasFocus
|
||||
if (enableTextStyling) {
|
||||
textStylingManager.attach(stylingConfig)
|
||||
}
|
||||
|
||||
b.fontStyleBold.text = CommunityMaterial.Icon2.cmd_format_bold.character.toString()
|
||||
b.fontStyleItalic.text = CommunityMaterial.Icon2.cmd_format_italic.character.toString()
|
||||
b.fontStyleUnderline.text = CommunityMaterial.Icon2.cmd_format_underline.character.toString()
|
||||
b.fontStyleStrike.text = CommunityMaterial.Icon2.cmd_format_strikethrough.character.toString()
|
||||
b.fontStyleSubscript.text = CommunityMaterial.Icon2.cmd_format_subscript.character.toString()
|
||||
b.fontStyleSuperscript.text = CommunityMaterial.Icon2.cmd_format_superscript.character.toString()
|
||||
b.fontStyleBold.attachToastHint(R.string.hint_style_bold)
|
||||
b.fontStyleItalic.attachToastHint(R.string.hint_style_italic)
|
||||
b.fontStyleUnderline.attachToastHint(R.string.hint_style_underline)
|
||||
b.fontStyleStrike.attachToastHint(R.string.hint_style_strike)
|
||||
b.fontStyleSubscript.attachToastHint(R.string.hint_style_subscript)
|
||||
b.fontStyleSuperscript.attachToastHint(R.string.hint_style_superscript)
|
||||
|
||||
/*b.fontStyleBold.shapeAppearanceModel = b.fontStyleBold.shapeAppearanceModel
|
||||
.toBuilder()
|
||||
.setBottomLeftCornerSize(0f)
|
||||
.build()
|
||||
b.fontStyleSuperscript.shapeAppearanceModel = b.fontStyleBold.shapeAppearanceModel
|
||||
.toBuilder()
|
||||
.setBottomRightCornerSize(0f)
|
||||
.build()
|
||||
b.fontStyleClear.shapeAppearanceModel = b.fontStyleClear.shapeAppearanceModel
|
||||
.toBuilder()
|
||||
.setTopLeftCornerSize(0f)
|
||||
.setTopRightCornerSize(0f)
|
||||
.build()*/
|
||||
|
||||
b.fontStyle.addOnButtonCheckedListener(this::onFormatChecked)
|
||||
b.fontStyleClear.setOnClickListener(this::onFormatClear)
|
||||
b.text.setSelectionChangedListener(this::onSelectionChanged)
|
||||
if (App.devMode) {
|
||||
b.textHtml.isVisible = true
|
||||
b.text.addTextChangedListener {
|
||||
b.textHtml.text = getMessageBody()
|
||||
}
|
||||
}
|
||||
|
||||
activity.navView.bottomBar.apply {
|
||||
fabEnable = true
|
||||
@ -533,7 +425,7 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
|
||||
if (b.textLayout.counterMaxLength != -1 && b.text.length() > b.textLayout.counterMaxLength)
|
||||
return
|
||||
|
||||
val textHtml = getHtmlText()
|
||||
val body = getMessageBody()
|
||||
|
||||
activity.bottomSheet.hideKeyboard()
|
||||
|
||||
@ -541,7 +433,7 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
|
||||
.setTitle(R.string.messages_compose_confirm_title)
|
||||
.setMessage(R.string.messages_compose_confirm_text)
|
||||
.setPositiveButton(R.string.send) { _, _ ->
|
||||
EdziennikTask.messageSend(App.profileId, recipients, subject.trim(), textHtml).enqueue(activity)
|
||||
EdziennikTask.messageSend(App.profileId, recipients, subject.trim(), body).enqueue(activity)
|
||||
}
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show()
|
||||
|
@ -13,15 +13,16 @@ class MessagesComparator : Comparator<Any> {
|
||||
return 0
|
||||
|
||||
return when {
|
||||
// standard sorting
|
||||
// descending sorting (1. true, 2. false)
|
||||
o1.isStarred && !o2.isStarred -> -1
|
||||
!o1.isStarred && o2.isStarred -> 1
|
||||
// ascending sorting
|
||||
o1.filterWeight > o2.filterWeight -> 1
|
||||
o1.filterWeight < o2.filterWeight -> -1
|
||||
else -> when {
|
||||
// reversed sorting
|
||||
// descending sorting
|
||||
o1.addedDate > o2.addedDate -> -1
|
||||
o1.addedDate < o2.addedDate -> 1
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -55,6 +55,11 @@ class MessageViewHolder(
|
||||
b.messageDate.setTextAppearance(activity, style)
|
||||
b.messageDate.typeface = typeface
|
||||
|
||||
if (adapter.onStarClick == null) {
|
||||
b.messageStar.isVisible = false
|
||||
}
|
||||
b.messageStar.detachToastHint()
|
||||
|
||||
val messageInfo = MessagesUtils.getMessageInfo(app, item, 48, 24, 18, 12)
|
||||
b.messageProfileBackground.setImageBitmap(messageInfo.profileImage)
|
||||
b.messageSender.text = messageInfo.profileName
|
||||
@ -75,5 +80,11 @@ class MessageViewHolder(
|
||||
adapter.onItemClick?.let { listener ->
|
||||
b.root.onClick { listener(item) }
|
||||
}
|
||||
adapter.onStarClick?.let { listener ->
|
||||
b.messageStar.isVisible = true
|
||||
adapter.manager.setStarIcon(b.messageStar, item)
|
||||
b.messageStar.onClick { listener(item) }
|
||||
b.messageStar.attachToastHint(R.string.hint_message_star)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ object BetterHtml {
|
||||
val customSpanClasses = listOf(
|
||||
BoldSpan::class.java,
|
||||
ItalicSpan::class.java,
|
||||
UnderlineSpan::class.java,
|
||||
UnderlineCustomSpan::class.java,
|
||||
StrikethroughSpan::class.java,
|
||||
SubscriptSizeSpan::class.java,
|
||||
SuperscriptSizeSpan::class.java,
|
||||
@ -101,8 +101,9 @@ object BetterHtml {
|
||||
Typeface.ITALIC -> ItalicSpan()
|
||||
else -> null
|
||||
}
|
||||
is SubscriptSpan -> SubscriptSizeSpan(size = 10, dip = true)
|
||||
is SuperscriptSpan -> SuperscriptSizeSpan(size = 10, dip = true)
|
||||
is UnderlineSpan -> UnderlineCustomSpan()
|
||||
is SubscriptSpan -> SubscriptSizeSpan()
|
||||
is SuperscriptSpan -> SuperscriptSizeSpan()
|
||||
else -> null
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright (c) Kuba Szczodrzyński 2021-10-7.
|
||||
*/
|
||||
|
||||
package pl.szczodrzynski.edziennik.utils.managers
|
||||
|
||||
import android.os.Bundle
|
||||
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
|
||||
import com.mikepenz.iconics.utils.colorRes
|
||||
import com.mikepenz.iconics.view.IconicsImageView
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import pl.szczodrzynski.edziennik.App
|
||||
import pl.szczodrzynski.edziennik.R
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Message
|
||||
import pl.szczodrzynski.edziennik.data.db.full.MessageFull
|
||||
import pl.szczodrzynski.navlib.colorAttr
|
||||
|
||||
class MessageManager(private val app: App) {
|
||||
|
||||
suspend fun getMessage(profileId: Int, args: Bundle?): MessageFull? {
|
||||
val id = args?.getLong("messageId") ?: return null
|
||||
val json = args.getString("message")
|
||||
val addedDate = args.getLong("sentDate")
|
||||
return getMessage(profileId, id, json, addedDate)
|
||||
}
|
||||
|
||||
suspend fun getMessage(
|
||||
profileId: Int,
|
||||
id: Long,
|
||||
json: String?,
|
||||
sentDate: Long = 0L
|
||||
): MessageFull? {
|
||||
val message = if (json != null) {
|
||||
app.gson.fromJson(json, MessageFull::class.java)?.also {
|
||||
if (sentDate > 0L) {
|
||||
it.addedDate = sentDate
|
||||
}
|
||||
}
|
||||
} else {
|
||||
withContext(Dispatchers.IO) {
|
||||
app.db.messageDao().getByIdNow(profileId, id)
|
||||
}
|
||||
} ?: return null
|
||||
|
||||
// make recipients ID-unique
|
||||
// this helps when multiple profiles receive the same message
|
||||
// (there are multiple -1 recipients for the same message ID)
|
||||
val recipientsDistinct = message.recipients?.distinctBy { it.id } ?: return null
|
||||
message.recipients?.clear()
|
||||
message.recipients?.addAll(recipientsDistinct)
|
||||
|
||||
// load recipients for sent messages
|
||||
val teachers = withContext(Dispatchers.IO) {
|
||||
app.db.teacherDao().getAllNow(profileId)
|
||||
}
|
||||
|
||||
message.recipients?.forEach { recipient ->
|
||||
// store the account name as a recipient
|
||||
if (recipient.id == -1L)
|
||||
recipient.fullName = app.profile.accountName ?: app.profile.studentNameLong
|
||||
|
||||
// lookup a teacher by the recipient ID
|
||||
if (recipient.fullName == null)
|
||||
recipient.fullName = teachers.firstOrNull { it.id == recipient.id }?.fullName ?: ""
|
||||
|
||||
// unset the readByEveryone flag
|
||||
if (recipient.readDate < 1 && message.type == Message.TYPE_SENT)
|
||||
message.readByEveryone = false
|
||||
}
|
||||
|
||||
// store the account name as sender for sent messages
|
||||
if (message.type == Message.TYPE_SENT && message.senderName == null) {
|
||||
message.senderName = app.profile.accountName ?: app.profile.studentNameLong
|
||||
}
|
||||
|
||||
// set the message as seen
|
||||
if (message.body != null && !message.seen) {
|
||||
app.db.metadataDao().setSeen(profileId, message, true)
|
||||
}
|
||||
//msg.recipients = app.db.messageRecipientDao().getAllByMessageId(msg.profileId, msg.id)
|
||||
|
||||
return message
|
||||
}
|
||||
|
||||
fun setStarIcon(image: IconicsImageView, message: Message) {
|
||||
if (message.isStarred) {
|
||||
image.icon?.colorRes = R.color.md_amber_500
|
||||
image.icon?.icon = CommunityMaterial.Icon3.cmd_star
|
||||
} else {
|
||||
image.icon?.colorAttr(image.context, android.R.attr.textColorSecondary)
|
||||
image.icon?.icon = CommunityMaterial.Icon3.cmd_star_outline
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun starMessage(message: Message, isStarred: Boolean) {
|
||||
message.isStarred = isStarred
|
||||
withContext(Dispatchers.Default) {
|
||||
app.db.messageDao().replace(message)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun markAsDeleted(message: Message) {
|
||||
message.type = Message.TYPE_DELETED
|
||||
withContext(Dispatchers.Default) {
|
||||
app.db.messageDao().replace(message)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,195 @@
|
||||
/*
|
||||
* Copyright (c) Kuba Szczodrzyński 2021-10-7.
|
||||
*/
|
||||
|
||||
package pl.szczodrzynski.edziennik.utils.managers
|
||||
|
||||
import android.text.Spanned
|
||||
import android.widget.Button
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.text.HtmlCompat
|
||||
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.utils.TextInputKeyboardEdit
|
||||
import pl.szczodrzynski.edziennik.utils.html.BetterHtml
|
||||
|
||||
class TextStylingManager(private val app: App) {
|
||||
companion object {
|
||||
private const val TAG = "TextStylingManager"
|
||||
}
|
||||
|
||||
data class StylingConfig(
|
||||
val editText: TextInputKeyboardEdit,
|
||||
val fontStyleGroup: MaterialButtonToggleGroup,
|
||||
val fontStyleClear: Button,
|
||||
val styles: List<Style>,
|
||||
val textHtml: TextView? = null,
|
||||
val htmlCompatibleMode: Boolean = false,
|
||||
) {
|
||||
data class Style(
|
||||
val button: MaterialButton,
|
||||
val spanClass: Class<*>,
|
||||
val icon: IIcon,
|
||||
@StringRes
|
||||
val hint: Int,
|
||||
) {
|
||||
fun newInstance(): Any = spanClass.newInstance()
|
||||
}
|
||||
|
||||
var watchStyleChecked = true
|
||||
var watchSelectionChanged = true
|
||||
}
|
||||
|
||||
fun attach(config: StylingConfig) {
|
||||
enableButtons(config, enable = false)
|
||||
config.editText.setOnFocusChangeListener { _, hasFocus ->
|
||||
enableButtons(config, enable = hasFocus)
|
||||
}
|
||||
|
||||
config.styles.forEach {
|
||||
it.button.text = it.icon.character.toString()
|
||||
it.button.attachToastHint(it.hint)
|
||||
}
|
||||
|
||||
config.fontStyleGroup.addOnButtonCheckedListener { _, checkedId, isChecked ->
|
||||
val style = config.styles.firstOrNull {
|
||||
it.button.id == checkedId
|
||||
} ?: return@addOnButtonCheckedListener
|
||||
onStyleChecked(config, style, isChecked)
|
||||
}
|
||||
|
||||
config.fontStyleClear.setOnClickListener {
|
||||
onStyleClear(config)
|
||||
}
|
||||
|
||||
config.editText.setSelectionChangedListener { selectionStart, selectionEnd ->
|
||||
onSelectionChanged(config, selectionStart, selectionEnd)
|
||||
}
|
||||
|
||||
/*b.fontStyleBold.shapeAppearanceModel = b.fontStyleBold.shapeAppearanceModel
|
||||
.toBuilder()
|
||||
.setBottomLeftCornerSize(0f)
|
||||
.build()
|
||||
b.fontStyleSuperscript.shapeAppearanceModel = b.fontStyleBold.shapeAppearanceModel
|
||||
.toBuilder()
|
||||
.setBottomRightCornerSize(0f)
|
||||
.build()
|
||||
b.fontStyleClear.shapeAppearanceModel = b.fontStyleClear.shapeAppearanceModel
|
||||
.toBuilder()
|
||||
.setTopLeftCornerSize(0f)
|
||||
.setTopRightCornerSize(0f)
|
||||
.build()*/
|
||||
}
|
||||
|
||||
fun getHtmlText(config: StylingConfig): String {
|
||||
val spanned = config.editText.text ?: return ""
|
||||
|
||||
// apparently setting the spans to a different Spannable calls the original EditText's
|
||||
// onSelectionChanged with selectionStart=-1, which in effect unchecks the format toggles
|
||||
config.watchSelectionChanged = false
|
||||
|
||||
// remove zero-length spans, as they seem to affect
|
||||
// the whole line when converted to HTML
|
||||
spanned.getSpans(0, spanned.length, Any::class.java).forEach {
|
||||
val spanStart = spanned.getSpanStart(it)
|
||||
val spanEnd = spanned.getSpanEnd(it)
|
||||
if (spanStart == spanEnd && it::class.java in BetterHtml.customSpanClasses)
|
||||
spanned.removeSpan(it)
|
||||
}
|
||||
var textHtml = HtmlCompat.toHtml(spanned, HtmlCompat.TO_HTML_PARAGRAPH_LINES_INDIVIDUAL)
|
||||
.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")
|
||||
|
||||
config.watchSelectionChanged = true
|
||||
|
||||
if (config.htmlCompatibleMode) {
|
||||
textHtml = textHtml
|
||||
.replace("<br>", "<p> </p>")
|
||||
.replace("<b>", "<strong>")
|
||||
.replace("</b>", "</strong>")
|
||||
.replace("<i>", "<em>")
|
||||
.replace("</i>", "</em>")
|
||||
.replace("<u>", "<span style=\"text-decoration: underline;\">")
|
||||
.replace("</u>", "</span>")
|
||||
}
|
||||
|
||||
return textHtml
|
||||
}
|
||||
|
||||
private fun onStyleChecked(
|
||||
config: StylingConfig,
|
||||
style: StylingConfig.Style,
|
||||
isChecked: Boolean
|
||||
) {
|
||||
if (!config.watchStyleChecked)
|
||||
return
|
||||
val span = style.newInstance()
|
||||
|
||||
// see comments in getHtmlText()
|
||||
config.watchSelectionChanged = false
|
||||
if (isChecked)
|
||||
BetterHtml.applyFormat(span, config.editText)
|
||||
else
|
||||
BetterHtml.removeFormat(span, config.editText)
|
||||
config.watchSelectionChanged = true
|
||||
|
||||
config.textHtml?.text = getHtmlText(config)
|
||||
}
|
||||
|
||||
private fun onStyleClear(config: StylingConfig) {
|
||||
// shortened version on onStyleChecked(), removing all spans
|
||||
config.watchSelectionChanged = false
|
||||
BetterHtml.removeFormat(span = null, config.editText)
|
||||
config.watchSelectionChanged = true
|
||||
config.textHtml?.text = getHtmlText(config)
|
||||
// force update of text style toggle states
|
||||
onSelectionChanged(config, config.editText.selectionStart, config.editText.selectionEnd)
|
||||
}
|
||||
|
||||
private fun onSelectionChanged(config: StylingConfig, selectionStart: Int, selectionEnd: Int) {
|
||||
if (!config.watchSelectionChanged)
|
||||
return
|
||||
val spanned = config.editText.text ?: return
|
||||
val spans = spanned.getSpans(selectionStart, selectionEnd, Any::class.java).mapNotNull {
|
||||
if (it::class.java !in BetterHtml.customSpanClasses)
|
||||
return@mapNotNull null
|
||||
val spanStart = spanned.getSpanStart(it)
|
||||
val spanEnd = spanned.getSpanEnd(it)
|
||||
// remove 0-length spans after navigating out of them
|
||||
if (spanStart == spanEnd)
|
||||
spanned.removeSpan(it)
|
||||
else if (spanned.getSpanFlags(it) hasSet Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
spanned.setSpan(it, spanStart, spanEnd, Spanned.SPAN_EXCLUSIVE_INCLUSIVE)
|
||||
|
||||
// names are helpful here
|
||||
val isNotAfterWord = selectionEnd <= spanEnd
|
||||
val isSelectionInWord = selectionStart != selectionEnd && selectionStart >= spanStart
|
||||
val isCursorInWord = selectionStart == selectionEnd && selectionStart > spanStart
|
||||
val isChecked = (isCursorInWord || isSelectionInWord) && isNotAfterWord
|
||||
if (isChecked) it::class.java else null
|
||||
}
|
||||
config.watchStyleChecked = false
|
||||
config.styles.forEach {
|
||||
it.button.isChecked = it.spanClass in spans
|
||||
}
|
||||
config.watchStyleChecked = true
|
||||
}
|
||||
|
||||
private fun enableButtons(config: StylingConfig, enable: Boolean) {
|
||||
config.fontStyleClear.isEnabled = enable
|
||||
config.styles.forEach {
|
||||
it.button.isEnabled = enable
|
||||
}
|
||||
}
|
||||
}
|
@ -7,10 +7,10 @@ package pl.szczodrzynski.edziennik.utils.span
|
||||
import android.text.TextPaint
|
||||
import android.text.style.SubscriptSpan
|
||||
|
||||
class SubscriptSizeSpan(
|
||||
private val size: Int,
|
||||
private val dip: Boolean,
|
||||
) : SubscriptSpan() {
|
||||
class SubscriptSizeSpan : SubscriptSpan() {
|
||||
|
||||
private val size = 10
|
||||
private val dip = true
|
||||
|
||||
override fun updateDrawState(textPaint: TextPaint) {
|
||||
super.updateDrawState(textPaint)
|
||||
|
@ -7,10 +7,10 @@ package pl.szczodrzynski.edziennik.utils.span
|
||||
import android.text.TextPaint
|
||||
import android.text.style.SuperscriptSpan
|
||||
|
||||
class SuperscriptSizeSpan(
|
||||
private val size: Int,
|
||||
private val dip: Boolean,
|
||||
) : SuperscriptSpan() {
|
||||
class SuperscriptSizeSpan : SuperscriptSpan() {
|
||||
|
||||
private val size = 10
|
||||
private val dip = true
|
||||
|
||||
override fun updateDrawState(textPaint: TextPaint) {
|
||||
super.updateDrawState(textPaint)
|
||||
|
@ -0,0 +1,9 @@
|
||||
/*
|
||||
* Copyright (c) Kuba Szczodrzyński 2021-10-7.
|
||||
*/
|
||||
|
||||
package pl.szczodrzynski.edziennik.utils.span
|
||||
|
||||
import android.text.style.UnderlineSpan
|
||||
|
||||
class UnderlineCustomSpan : UnderlineSpan()
|
@ -25,6 +25,7 @@
|
||||
android:drawablePadding="16dp"
|
||||
android:fontFamily="sans-serif-light"
|
||||
android:gravity="center"
|
||||
android:padding="16dp"
|
||||
android:text="@string/attendances_no_data"
|
||||
android:textSize="24sp"
|
||||
android:visibility="gone"
|
||||
|
@ -153,6 +153,7 @@
|
||||
android:drawablePadding="16dp"
|
||||
android:fontFamily="sans-serif-light"
|
||||
android:gravity="center"
|
||||
android:padding="16dp"
|
||||
android:text="@string/attendances_no_data"
|
||||
android:textSize="24sp"
|
||||
android:visibility="gone"
|
||||
|
@ -30,6 +30,7 @@
|
||||
android:drawablePadding="16dp"
|
||||
android:fontFamily="sans-serif-light"
|
||||
android:gravity="center"
|
||||
android:padding="16dp"
|
||||
android:text="@string/grades_no_data"
|
||||
android:textSize="24sp"
|
||||
android:visibility="gone"
|
||||
|
@ -25,6 +25,7 @@
|
||||
android:drawablePadding="16dp"
|
||||
android:fontFamily="sans-serif-light"
|
||||
android:gravity="center"
|
||||
android:padding="16dp"
|
||||
android:text="@string/homework_no_data"
|
||||
android:textSize="24sp"
|
||||
android:visibility="gone"
|
||||
|
@ -2,21 +2,22 @@
|
||||
<!--
|
||||
~ Copyright (c) Kuba Szczodrzyński 2019-11-12.
|
||||
-->
|
||||
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/colorSurface_6dp"
|
||||
android:minHeight="?attr/actionBarSize"
|
||||
android:orientation="horizontal"
|
||||
android:background="@color/colorSurface_6dp">
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/closeButton"
|
||||
@ -55,7 +56,7 @@
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="visible"
|
||||
tools:visibility="gone"/>
|
||||
tools:visibility="gone" />
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:id="@+id/content"
|
||||
@ -81,6 +82,7 @@
|
||||
<FrameLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/profileBackground"
|
||||
android:layout_width="64dp"
|
||||
@ -112,26 +114,38 @@
|
||||
<TextView
|
||||
android:id="@+id/sender"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:background="?selectableItemBackground"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="2"
|
||||
android:minHeight="64dp"
|
||||
android:paddingHorizontal="8dp"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingVertical="8dp"
|
||||
android:textAppearance="@style/NavView.TextView.Subtitle"
|
||||
tools:text="Allegro - wysyłamy duużo wiadomości!!! Masz nowe oferty! Możesz kupić nowego laptopa! Ale super! Ehh, to jest nadawca a nie temat więc nwm czemu to tutaj wpisałem" />
|
||||
|
||||
<com.mikepenz.iconics.view.IconicsImageView
|
||||
android:id="@+id/messageStar"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:padding="4dp"
|
||||
app:iiv_color="?android:textColorSecondary"
|
||||
app:iiv_icon="cmd-star-outline"
|
||||
app:iiv_size="24dp"
|
||||
tools:background="@android:drawable/star_off" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/date"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginRight="8dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginHorizontal="8dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:textAppearance="@style/NavView.TextView.Small"
|
||||
tools:text="14:26" />
|
||||
|
||||
tools:text="20 lis 2021\n14:26" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
@ -188,122 +202,61 @@
|
||||
android:visibility="visible"
|
||||
tools:visibility="visible">
|
||||
|
||||
<LinearLayout
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/replyButton"
|
||||
android:layout_width="match_parent"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginHorizontal="4dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/bg_rounded_ripple"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:paddingHorizontal="4dp"
|
||||
android:paddingVertical="8dp">
|
||||
|
||||
<com.mikepenz.iconics.view.IconicsImageView
|
||||
android:id="@+id/replyIcon"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
app:iiv_color="?android:textColorSecondary"
|
||||
app:iiv_icon="cmd-reply-outline"
|
||||
tools:srcCompat="@android:drawable/ic_menu_revert" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="Odpowiedz"
|
||||
android:textAppearance="@style/NavView.TextView.Small" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/forwardButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginHorizontal="4dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/bg_rounded_ripple"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:paddingHorizontal="4dp"
|
||||
android:paddingVertical="8dp">
|
||||
|
||||
<com.mikepenz.iconics.view.IconicsImageView
|
||||
android:id="@+id/forwardIcon"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
app:iiv_color="?android:textColorSecondary"
|
||||
app:iiv_icon="cmd-arrow-right"
|
||||
tools:srcCompat="@android:drawable/ic_media_ff" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="Przekaż dalej"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:text="@string/message_reply"
|
||||
android:textAllCaps="false"
|
||||
android:textAppearance="@style/NavView.TextView.Small" />
|
||||
tools:drawableTop="@android:drawable/sym_action_email" />
|
||||
|
||||
</LinearLayout>
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/forwardButton"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginHorizontal="4dp"
|
||||
android:layout_weight="1"
|
||||
android:paddingHorizontal="4dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:text="@string/message_forward"
|
||||
android:textAllCaps="false"
|
||||
tools:drawableTop="@android:drawable/stat_sys_phone_call_forward" />
|
||||
|
||||
<LinearLayout
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/deleteButton"
|
||||
android:layout_width="match_parent"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginHorizontal="4dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/bg_rounded_ripple"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:paddingHorizontal="4dp"
|
||||
android:paddingVertical="8dp"
|
||||
android:visibility="visible">
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:text="@string/message_delete"
|
||||
android:textAllCaps="false"
|
||||
tools:drawableTop="@android:drawable/ic_menu_delete" />
|
||||
|
||||
<com.mikepenz.iconics.view.IconicsImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
app:iiv_color="?android:textColorSecondary"
|
||||
app:iiv_icon="cmd-delete-outline"
|
||||
tools:srcCompat="@android:drawable/ic_menu_delete" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="Usuń"
|
||||
android:textAppearance="@style/NavView.TextView.Small" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/downloadButton"
|
||||
android:layout_width="match_parent"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginHorizontal="4dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/bg_rounded_ripple"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:paddingHorizontal="4dp"
|
||||
android:paddingVertical="8dp"
|
||||
android:visibility="visible">
|
||||
|
||||
<com.mikepenz.iconics.view.IconicsImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
app:iiv_color="?android:textColorSecondary"
|
||||
app:iiv_icon="cmd-download-outline"
|
||||
tools:srcCompat="@android:drawable/ic_menu_delete" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="Pobierz ponownie"
|
||||
android:textAppearance="@style/NavView.TextView.Small" />
|
||||
|
||||
</LinearLayout>
|
||||
android:paddingTop="16dp"
|
||||
android:text="@string/message_download"
|
||||
android:textAllCaps="false"
|
||||
android:visibility="gone"
|
||||
tools:drawableTop="@android:drawable/stat_sys_download" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
@ -25,6 +25,7 @@
|
||||
android:drawablePadding="16dp"
|
||||
android:fontFamily="sans-serif-light"
|
||||
android:gravity="center"
|
||||
android:padding="16dp"
|
||||
android:text="@string/messages_no_data"
|
||||
android:textSize="24sp"
|
||||
android:visibility="gone"
|
||||
|
@ -2,10 +2,9 @@
|
||||
<!--
|
||||
~ Copyright (c) Kuba Szczodrzyński 2020-4-4.
|
||||
-->
|
||||
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
@ -40,9 +39,11 @@
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="8dp"
|
||||
android:paddingEnd="32dp"
|
||||
android:paddingRight="32dp"
|
||||
android:singleLine="true"
|
||||
android:textStyle="normal"
|
||||
android:textAppearance="@style/NavView.TextView.Helper"
|
||||
android:textStyle="normal"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/messageProfileBackground"
|
||||
app:layout_constraintTop_toBottomOf="@+id/messageSender"
|
||||
@ -72,6 +73,8 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="8dp"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:paddingEnd="32dp"
|
||||
android:paddingRight="32dp"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="@style/NavView.TextView.Helper"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
@ -80,6 +83,23 @@
|
||||
app:layout_constraintTop_toBottomOf="@+id/messageSubject"
|
||||
tools:text="Znajdź produkty, których szukasz. Witaj Kuba Szczodrzyński (Client" />
|
||||
|
||||
<com.mikepenz.iconics.view.IconicsImageView
|
||||
android:id="@+id/messageStar"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginRight="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:padding="4dp"
|
||||
app:iiv_color="?android:textColorSecondary"
|
||||
app:iiv_icon="cmd-star-outline"
|
||||
app:iiv_size="24dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/messageDate"
|
||||
tools:background="@android:drawable/star_off" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/messageDate"
|
||||
android:layout_width="wrap_content"
|
||||
@ -106,6 +126,6 @@
|
||||
app:layout_constraintBottom_toBottomOf="@+id/messageDate"
|
||||
app:layout_constraintEnd_toStartOf="@+id/messageDate"
|
||||
app:layout_constraintTop_toTopOf="@+id/messageDate"
|
||||
tools:srcCompat="@tools:sample/avatars[4]" />
|
||||
tools:background="@tools:sample/avatars[4]" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</layout>
|
||||
|
@ -25,6 +25,7 @@
|
||||
android:drawablePadding="16dp"
|
||||
android:fontFamily="sans-serif-light"
|
||||
android:gravity="center"
|
||||
android:padding="16dp"
|
||||
android:text="@string/notifications_no_data"
|
||||
android:textSize="24sp"
|
||||
android:visibility="gone"
|
||||
|
@ -30,6 +30,7 @@
|
||||
android:drawablePadding="16dp"
|
||||
android:fontFamily="sans-serif-light"
|
||||
android:gravity="center"
|
||||
android:padding="16dp"
|
||||
android:text="@string/grades_no_data"
|
||||
android:textSize="24sp"
|
||||
android:visibility="gone"
|
||||
|
@ -25,6 +25,7 @@
|
||||
android:drawablePadding="16dp"
|
||||
android:fontFamily="sans-serif-light"
|
||||
android:gravity="center"
|
||||
android:padding="16dp"
|
||||
android:text="@string/grades_no_data"
|
||||
android:textSize="24sp"
|
||||
android:visibility="gone"
|
||||
|
@ -744,7 +744,7 @@
|
||||
<string name="messages_compose_text_hint">Napisz wiadomość…</string>
|
||||
<string name="messages_compose_title">Napisz wiadomość</string>
|
||||
<string name="messages_compose_to_hint">Do</string>
|
||||
<string name="messages_date_time_format" translatable="false">%s, %s</string>
|
||||
<string name="messages_date_time_format" translatable="false">%s\n%s</string>
|
||||
<string name="messages_delete_confirmation">Czy chcesz usunąć wiadomość?</string>
|
||||
<string name="messages_delete_confirmation_text">Spowoduje to przeniesienie wiadomości do zakładki \"Usunięte\" w aplikacji. Zmiany nie wpłyną na wiadomość w e-dzienniku (nie zostanie ona tam usunięta).</string>
|
||||
<string name="messages_download_error">Błąd pobierania wiadomości</string>
|
||||
@ -1471,4 +1471,9 @@
|
||||
<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="hint_message_star">Oznacz gwiazdką</string>
|
||||
<string name="message_forward">Przekaż dalej</string>
|
||||
<string name="message_delete">Usuń</string>
|
||||
<string name="message_reply">Odpowiedz</string>
|
||||
<string name="message_download">Pobierz ponownie</string>
|
||||
</resources>
|
||||
|
Loading…
Reference in New Issue
Block a user