From 3f7982884adebb91ef8edbd4bdfd4d995a941a78 Mon Sep 17 00:00:00 2001 From: Kalpesh Chandora Date: Sat, 22 Feb 2025 19:22:43 +0530 Subject: [PATCH 1/3] Add support for search occurrences count and search navigation --- .../com/pluto/utilities/spannable/SpanUtil.kt | 28 +++++++++-- .../interceptor/ui/ContentFragment.kt | 42 +++++++++++++--- .../pluto_network___ic_arrow_down.xml | 11 ++++ .../drawable/pluto_network___ic_arrow_up.xml | 11 ++++ .../pluto_network___fragment_content.xml | 50 ++++++++++++++++++- .../core/lib/src/main/res/values/strings.xml | 2 + 6 files changed, 133 insertions(+), 11 deletions(-) create mode 100644 pluto-plugins/plugins/network/core/lib/src/main/res/drawable/pluto_network___ic_arrow_down.xml create mode 100644 pluto-plugins/plugins/network/core/lib/src/main/res/drawable/pluto_network___ic_arrow_up.xml 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..c15c9c0b8 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{ASCII}]".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..4d84316ac 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,47 @@ internal class ContentFragment : Fragment(R.layout.pluto_network___fragment_cont binding.editSearch.doOnTextChanged { text, _, _, _ -> viewLifecycleOwner.lifecycleScope.launchWhenResumed { text?.toString()?.let { search -> + currentHighlightIndex = 0 argumentData?.let { binding.content.setSpan { - append(highlight(it.content, search.trim())) + 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(search.trim()) + scrollToText(currentHighlightIndex, search.trim()) } } } + + 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 { @@ -91,13 +121,13 @@ internal class ContentFragment : Fragment(R.layout.pluto_network___fragment_cont /** * 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 +138,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 From 189d21a199449ae3470f5767d8827cf1731e3787 Mon Sep 17 00:00:00 2001 From: Kalpesh Chandora Date: Sat, 22 Feb 2025 22:16:04 +0530 Subject: [PATCH 2/3] Separate out search logic --- .../interceptor/ui/ContentFragment.kt | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) 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 4d84316ac..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 @@ -53,21 +53,7 @@ internal class ContentFragment : Fragment(R.layout.pluto_network___fragment_cont binding.editSearch.doOnTextChanged { text, _, _, _ -> viewLifecycleOwner.lifecycleScope.launchWhenResumed { text?.toString()?.let { search -> - 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()) + processSearch(search) } } } @@ -118,6 +104,24 @@ 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 */ From 271ba236adf3e938baead470d3326c6f4b98b130 Mon Sep 17 00:00:00 2001 From: Kalpesh Chandora Date: Sat, 22 Feb 2025 23:50:17 +0530 Subject: [PATCH 3/3] Update normalise method to use correct replacement --- .../lib/src/main/java/com/pluto/utilities/spannable/SpanUtil.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 c15c9c0b8..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 @@ -141,7 +141,7 @@ class Builder(val context: Context) { private fun CharSequence.normalise(): String { return Normalizer.normalize(this, Normalizer.Form.NFD) - .replace("[^\\p{ASCII}]".toRegex(), "") + .replace("\\p{InCombiningDiacriticalMarks}+".toRegex(), "") } fun build(): CharSequence {