diff --git a/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/spannable/SpanUtil.kt b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/spannable/SpanUtil.kt index 694e3ee11..bc1855f8f 100644 --- a/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/spannable/SpanUtil.kt +++ b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/spannable/SpanUtil.kt @@ -55,12 +55,15 @@ class Builder(val context: Context) { is String -> SpannableString(s).apply { setSpan(o, 0, length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) } + is SpannableStringBuilder -> s.apply { setSpan(o, 0, length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) } + is SpannableString -> s.apply { setSpan(o, 0, length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) } + else -> throw IllegalArgumentException("unhandled type $o") } @@ -92,14 +95,20 @@ class Builder(val context: Context) { fun highlight(span: CharSequence, search: String?): CharSequence { if (search.isNullOrEmpty()) return span - val normalizedText = Normalizer.normalize(span, Normalizer.Form.NFD) - .replace("\\p{InCombiningDiacriticalMarks}+".toRegex(), "") - .lowercase() + val normalizedText = span.normalise().lowercase() val startIndexes = normalizedText.allOccurrences(search) if (startIndexes.isNotEmpty()) { + return highlight(span, search, startIndexes) + } + return span + } + + fun highlight(span: CharSequence, search: String?, indexes: List): CharSequence { + if (search.isNullOrEmpty()) return span + if (indexes.isNotEmpty()) { val highlighted: Spannable = SpannableString(span) - startIndexes.forEach { + indexes.forEach { highlighted.setSpan( BackgroundColorSpan(context.color(R.color.pluto___text_highlight)), it, @@ -112,6 +121,12 @@ class Builder(val context: Context) { return span } + fun occurrences(span: CharSequence, search: String?): List { + if (search.isNullOrEmpty()) return emptyList() + val normalizedText = span.normalise().lowercase() + return normalizedText.allOccurrences(search) + } + fun clickable(span: CharSequence, listener: ClickableSpan): CharSequence { return span(span, listener) } @@ -124,6 +139,11 @@ class Builder(val context: Context) { return span(span, StyleSpan(Typeface.ITALIC)) } + private fun CharSequence.normalise(): String { + return Normalizer.normalize(this, Normalizer.Form.NFD) + .replace("\\p{InCombiningDiacriticalMarks}+".toRegex(), "") + } + fun build(): CharSequence { return spanBuilder } diff --git a/pluto-plugins/plugins/network/core/lib/src/main/java/com/pluto/plugins/network/internal/interceptor/ui/ContentFragment.kt b/pluto-plugins/plugins/network/core/lib/src/main/java/com/pluto/plugins/network/internal/interceptor/ui/ContentFragment.kt index 24184b2b0..745e23cb3 100644 --- a/pluto-plugins/plugins/network/core/lib/src/main/java/com/pluto/plugins/network/internal/interceptor/ui/ContentFragment.kt +++ b/pluto-plugins/plugins/network/core/lib/src/main/java/com/pluto/plugins/network/internal/interceptor/ui/ContentFragment.kt @@ -29,6 +29,9 @@ internal class ContentFragment : Fragment(R.layout.pluto_network___fragment_cont private val argumentData: ContentFormatterData? get() = arguments?.getParcelable(DATA) + private var currentHighlightIndex = 0 + private var occurrences = emptyList() + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) onBackPressed { handleBackPress() } @@ -50,20 +53,33 @@ internal class ContentFragment : Fragment(R.layout.pluto_network___fragment_cont binding.editSearch.doOnTextChanged { text, _, _, _ -> viewLifecycleOwner.lifecycleScope.launchWhenResumed { text?.toString()?.let { search -> - argumentData?.let { - binding.content.setSpan { - append(highlight(it.content, search.trim())) - append("\n") - } - } - - scrollToText(search.trim()) + processSearch(search) } } } + + binding.previousHighlight.setOnDebounceClickListener { + if (occurrences.isNotEmpty()) { + currentHighlightIndex = (currentHighlightIndex - 1 + occurrences.size) % occurrences.size + scrollToText(occurrences[currentHighlightIndex], binding.editSearch.text.toString()) + } + } + + binding.nextHighlight.setOnDebounceClickListener { + if (occurrences.isNotEmpty()) { + currentHighlightIndex = (currentHighlightIndex + 1) % occurrences.size + scrollToText(occurrences[currentHighlightIndex], binding.editSearch.text.toString()) + } + } + binding.share.setOnDebounceClickListener { argumentData?.let { - contentSharer.share(Shareable(title = "Share content", content = it.content.toString())) + contentSharer.share( + Shareable( + title = "Share content", + content = it.content.toString() + ) + ) } } argumentData?.let { @@ -88,16 +104,34 @@ internal class ContentFragment : Fragment(R.layout.pluto_network___fragment_cont } } + private fun processSearch(search: String) { + currentHighlightIndex = 0 + argumentData?.let { + binding.content.setSpan { + occurrences = occurrences(it.content, search.trim()) + append(highlight(it.content, search.trim(), occurrences)) + append("\n") + binding.searchCount.visibility = if (search.isEmpty()) View.GONE else VISIBLE + binding.searchCount.text = occurrences.size.toString() + val highlightsVisibility = if (occurrences.size < 2) View.GONE else VISIBLE + binding.previousHighlight.visibility = highlightsVisibility + binding.nextHighlight.visibility = highlightsVisibility + } + } + + scrollToText(currentHighlightIndex, search.trim()) + } + /** * helps to auto scroll to target search */ - private fun scrollToText(targetText: String) { + private fun scrollToText(startIndex: Int, targetText: String) { if (targetText.isEmpty()) { return } val contentText = binding.content.getText().toString().lowercase() - val index = contentText.indexOf(targetText.lowercase()) + val index = contentText.indexOf(targetText.lowercase(), startIndex) if (index != -1) { binding.content.post { @@ -108,7 +142,7 @@ internal class ContentFragment : Fragment(R.layout.pluto_network___fragment_cont val y = layout.getLineTop(lineNumber) binding.horizontalScroll.smoothScrollTo(x / 2, 0) - binding.contentNestedScrollView.smoothScrollTo(0, y / 2) + binding.contentNestedScrollView.smoothScrollTo(0, y) } } } diff --git a/pluto-plugins/plugins/network/core/lib/src/main/res/drawable/pluto_network___ic_arrow_down.xml b/pluto-plugins/plugins/network/core/lib/src/main/res/drawable/pluto_network___ic_arrow_down.xml new file mode 100644 index 000000000..57a114b05 --- /dev/null +++ b/pluto-plugins/plugins/network/core/lib/src/main/res/drawable/pluto_network___ic_arrow_down.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/pluto-plugins/plugins/network/core/lib/src/main/res/drawable/pluto_network___ic_arrow_up.xml b/pluto-plugins/plugins/network/core/lib/src/main/res/drawable/pluto_network___ic_arrow_up.xml new file mode 100644 index 000000000..ac40d08dc --- /dev/null +++ b/pluto-plugins/plugins/network/core/lib/src/main/res/drawable/pluto_network___ic_arrow_up.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/pluto-plugins/plugins/network/core/lib/src/main/res/layout/pluto_network___fragment_content.xml b/pluto-plugins/plugins/network/core/lib/src/main/res/layout/pluto_network___fragment_content.xml index ac650877a..72b1f8ded 100644 --- a/pluto-plugins/plugins/network/core/lib/src/main/res/layout/pluto_network___fragment_content.xml +++ b/pluto-plugins/plugins/network/core/lib/src/main/res/layout/pluto_network___fragment_content.xml @@ -154,10 +154,57 @@ android:textColorHint="@color/pluto___text_dark_40" android:textSize="@dimen/pluto___text_small" app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toStartOf="@+id/clearSearch" + app:layout_constraintEnd_toStartOf="@+id/nextHighlight" app:layout_constraintStart_toEndOf="@+id/closeSearch" app:layout_constraintTop_toTopOf="parent" /> + + + + + + diff --git a/pluto-plugins/plugins/network/core/lib/src/main/res/values/strings.xml b/pluto-plugins/plugins/network/core/lib/src/main/res/values/strings.xml index 8efb9b2d1..99bdef37a 100644 --- a/pluto-plugins/plugins/network/core/lib/src/main/res/values/strings.xml +++ b/pluto-plugins/plugins/network/core/lib/src/main/res/values/strings.xml @@ -94,4 +94,6 @@ 1 items %d items + Next highlight + Previous highlight \ No newline at end of file