fix: bug fixes (#88)

* enhancement: position of quotes

* fix: comment ordering

* fix: custom toolbar visibility

* fix: markdown spoiler expansion
This commit is contained in:
Diego Beraldin 2023-10-31 23:35:04 +01:00 committed by GitHub
parent f8250b31d2
commit df0336ab42
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 150 additions and 124 deletions

View File

@ -35,7 +35,7 @@ class CustomTextToolbar(
) { ) {
if (actionMode == null) { if (actionMode == null) {
status = TextToolbarStatus.Shown status = TextToolbarStatus.Shown
view.startActionMode( actionMode = view.startActionMode(
CustomTextActionModeCallback( CustomTextActionModeCallback(
rect = rect, rect = rect,
onCopy = { onCopy = {
@ -50,6 +50,7 @@ class CustomTextToolbar(
) )
} else { } else {
actionMode?.invalidate() actionMode?.invalidate()
actionMode = null
} }
} }
} }
@ -76,13 +77,21 @@ internal class CustomTextActionModeCallback(
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean = false override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean = false
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean { override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
when (item?.itemId) { val res = when (item?.itemId) {
ACTION_ID_COPY -> onCopy() ACTION_ID_COPY -> {
ACTION_ID_SEARCH -> onSearch() onCopy()
else -> return false true
}
ACTION_ID_SEARCH -> {
onSearch()
true
}
else -> false
} }
mode?.finish() mode?.finish()
return true return res
} }
override fun onDestroyActionMode(mode: ActionMode?) { override fun onDestroyActionMode(mode: ActionMode?) {

View File

@ -149,7 +149,8 @@ fun TextFormattingBar(
) )
) )
} }
it.copy(text = newText, selection = TextRange(index = selection.start + 2)) val newSelection = TextRange(index = selection.start + 2)
it.copy(text = newText, selection = newSelection)
} }
onTextFieldValueChanged(newValue) onTextFieldValueChanged(newValue)
}, },
@ -192,10 +193,19 @@ fun TextFormattingBar(
Icon( Icon(
modifier = Modifier.onClick { modifier = Modifier.onClick {
val newValue = textFieldValue.let { val newValue = textFieldValue.let {
it.copy( val selection = it.selection
text = it.text + "\n> ", val newText = buildString {
selection = TextRange(index = it.text.length + 3), append(it.text.substring(0, selection.start))
) append("\n> ")
append(
it.text.substring(
selection.end,
it.text.length
)
)
}
val newSelection = TextRange(index = selection.start + 3)
it.copy(text = newText, selection = newSelection)
} }
onTextFieldValueChanged(newValue) onTextFieldValueChanged(newValue)
}, },

View File

@ -646,18 +646,12 @@ private fun findNode(id: String, node: Node): Node? {
private fun linearize(node: Node, list: MutableList<CommentModel>) { private fun linearize(node: Node, list: MutableList<CommentModel>) {
if (node.children.isEmpty()) { if (node.comment != null) {
if (node.comment != null) { list.add(node.comment)
list.add(node.comment)
}
return
} }
for (c in node.children) { for (c in node.children) {
linearize(c, list) linearize(c, list)
} }
if (node.comment != null) {
list.add(node.comment)
}
} }
private fun List<CommentModel>.populateLoadMoreComments() = mapIndexed() { idx, comment -> private fun List<CommentModel>.populateLoadMoreComments() = mapIndexed() { idx, comment ->
@ -688,5 +682,5 @@ private fun List<CommentModel>.processCommentsToGetNestedOrder(
val result = mutableListOf<CommentModel>() val result = mutableListOf<CommentModel>()
linearize(root, result) linearize(root, result)
return result.reversed().toList() return result.toList()
} }

View File

@ -4,7 +4,6 @@ import android.text.SpannableStringBuilder
import android.text.Spanned import android.text.Spanned
import android.text.TextPaint import android.text.TextPaint
import android.text.style.ClickableSpan import android.text.style.ClickableSpan
import android.util.Log
import android.view.View import android.view.View
import android.widget.TextView import android.widget.TextView
import io.noties.markwon.AbstractMarkwonPlugin import io.noties.markwon.AbstractMarkwonPlugin
@ -17,7 +16,7 @@ data class SpoilerTitleSpan(val title: CharSequence)
class SpoilerCloseSpan class SpoilerCloseSpan
/* /*
* CREDITS: * Originally inspired by:
* https://github.com/dessalines/jerboa/blob/main/app/src/main/java/com/jerboa/util/markwon/MarkwonSpoilerPlugin.kt * https://github.com/dessalines/jerboa/blob/main/app/src/main/java/com/jerboa/util/markwon/MarkwonSpoilerPlugin.kt
*/ */
class MarkwonSpoilerPlugin private constructor(private val enableInteraction: Boolean) : class MarkwonSpoilerPlugin private constructor(private val enableInteraction: Boolean) :
@ -35,118 +34,132 @@ class MarkwonSpoilerPlugin private constructor(private val enableInteraction: Bo
} }
} }
private class SpoilerTextAddedListener : CorePlugin.OnTextAddedListener {
override fun onTextAdded(visitor: MarkwonVisitor, text: String, start: Int) {
val spoilerTitleRegex = Regex("(:::\\s+spoiler\\s+)(.*)")
// Find all spoiler "start" lines
val spoilerTitles = spoilerTitleRegex.findAll(text)
for (match in spoilerTitles) {
val spoilerTitle = match.groups[2]!!.value
visitor.builder().setSpan(
SpoilerTitleSpan(spoilerTitle),
start,
start + match.groups[2]!!.range.last,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE,
)
}
val spoilerCloseRegex = Regex("^(?!.*spoiler).*:::")
// Find all spoiler "end" lines
val spoilerCloses = spoilerCloseRegex.findAll(text)
for (match in spoilerCloses) {
visitor.builder()
.setSpan(SpoilerCloseSpan(), start, start + 3, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
}
}
override fun afterSetText(textView: TextView) { override fun afterSetText(textView: TextView) {
try { runCatching {
val spanned = SpannableStringBuilder(textView.text) val spanned = SpannableStringBuilder(textView.text)
val spoilerTitleSpans = val startSpans =
spanned.getSpans(0, spanned.length, SpoilerTitleSpan::class.java) spanned.getSpans(0, spanned.length, SpoilerTitleSpan::class.java)
val spoilerCloseSpans = .sortedBy { spanned.getSpanStart(it) }
val closeSpans =
spanned.getSpans(0, spanned.length, SpoilerCloseSpan::class.java) spanned.getSpans(0, spanned.length, SpoilerCloseSpan::class.java)
.sortedBy { spanned.getSpanStart(it) }
spoilerTitleSpans.sortBy { spanned.getSpanStart(it) } startSpans
spoilerCloseSpans.sortBy { spanned.getSpanStart(it) } .zip(closeSpans)
.forEach { (startSpan, closeSpan) ->
val spoilerStart = spanned.getSpanStart(startSpan)
val spoilerEnd = spanned.getSpanEnd(closeSpan)
spoilerTitleSpans.forEachIndexed { index, spoilerTitleSpan -> val spoilerTitle = getSpoilerTitle(false, startSpan.title.toString())
val spoilerStart = spanned.getSpanStart(spoilerTitleSpan)
var spoilerEnd = spanned.length val spoilerContent = spanned.subSequence(
if (index < spoilerCloseSpans.size) { spanned.getSpanEnd(startSpan) + 1,
val spoilerCloseSpan = spoilerCloseSpans[index] spoilerEnd - 3,
spoilerEnd = spanned.getSpanEnd(spoilerCloseSpan) )
// Remove spoiler content from span
spanned.replace(spoilerStart, spoilerEnd, spoilerTitle)
// Set span block title
spanned.setSpan(
spoilerTitle,
spoilerStart,
spoilerStart + spoilerTitle.length,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE,
)
val wrapper = SpoilerClickableSpan(
enableInteraction = enableInteraction,
textView = textView,
spoilerTitle = startSpan.title.toString(),
spoilerContent = spoilerContent.toString(),
)
// Set spoiler block type as ClickableSpan
spanned.setSpan(
wrapper,
spoilerStart,
spoilerStart + spoilerTitle.length,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE,
)
textView.text = spanned
} }
}
var open = false }
// The space at the end is necessary for the lengths to be the same }
// This reduces complexity as else it would need complex logic to determine the replacement length
val getSpoilerTitle = { openParam: Boolean -> private fun getSpoilerTitle(openParam: Boolean, title: String): String = if (openParam) {
if (openParam) "${spoilerTitleSpan.title}\n" else "${spoilerTitleSpan.title}\u200B" "${title}\n"
} } else {
// The space at the end is necessary for the lengths to be the same
val spoilerTitle = getSpoilerTitle(false) // This reduces complexity as else it would need complex logic to determine the replacement length
"${title}\u200B"
val spoilerContent = spanned.subSequence( }
spanned.getSpanEnd(spoilerTitleSpan) + 1,
spoilerEnd - 3, private class SpoilerClickableSpan(
) as SpannableStringBuilder private val enableInteraction: Boolean,
private val textView: TextView,
// Remove spoiler content from span private val spoilerTitle: String,
spanned.replace(spoilerStart, spoilerEnd, spoilerTitle) private val spoilerContent: String,
// Set span block title ) : ClickableSpan() {
spanned.setSpan(
spoilerTitle, private var open = false
spoilerStart,
spoilerStart + spoilerTitle.length, override fun onClick(view: View) {
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE, if (enableInteraction) {
) textView.cancelPendingInputEvents()
val spanned = SpannableStringBuilder(textView.text)
val wrapper = object : ClickableSpan() { val title = getSpoilerTitle(open, spoilerTitle)
override fun onClick(p0: View) { val start = spanned.indexOf(title).coerceAtLeast(0)
if (enableInteraction) {
textView.cancelPendingInputEvents() open = !open
open = !open
spanned.replace(
spanned.replace( start,
spoilerStart, start + title.length,
spoilerStart + spoilerTitle.length, getSpoilerTitle(open, spoilerTitle),
getSpoilerTitle(open), )
) if (open) {
if (open) { spanned.insert(start + title.length, spoilerContent)
spanned.insert(spoilerStart + spoilerTitle.length, spoilerContent) } else {
} else { spanned.replace(
spanned.replace( start + title.length,
spoilerStart + spoilerTitle.length, start + title.length + spoilerContent.length,
spoilerStart + spoilerTitle.length + spoilerContent.length, "",
"", )
) }
}
textView.text = spanned
textView.text = spanned AsyncDrawableScheduler.schedule(textView)
AsyncDrawableScheduler.schedule(textView) }
} }
}
override fun updateDrawState(ds: TextPaint) {
override fun updateDrawState(ds: TextPaint) { }
} }
}
private class SpoilerTextAddedListener : CorePlugin.OnTextAddedListener {
// Set spoiler block type as ClickableSpan override fun onTextAdded(visitor: MarkwonVisitor, text: String, start: Int) {
spanned.setSpan( val spoilerTitleRegex = Regex("(:::\\s+spoiler\\s+)(.*)")
wrapper, // Find all spoiler "start" lines
spoilerStart, val spoilerTitles = spoilerTitleRegex.findAll(text)
spoilerStart + spoilerTitle.length,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE, for (match in spoilerTitles) {
) val spoilerTitle = match.groups[2]!!.value
visitor.builder().setSpan(
textView.text = spanned SpoilerTitleSpan(spoilerTitle),
} start,
} catch (e: Exception) { start + match.groups[2]!!.range.last,
Log.w("MarkdownSpoilerPlugin", "Failed to parse spoiler tag. Format incorrect") Spanned.SPAN_EXCLUSIVE_EXCLUSIVE,
)
}
val spoilerCloseRegex = Regex("^(?!.*spoiler).*:::")
// Find all spoiler "end" lines
val spoilerCloses = spoilerCloseRegex.findAll(text)
for (match in spoilerCloses) {
visitor.builder()
.setSpan(SpoilerCloseSpan(), start, start + 3, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
} }
} }
} }