@@ -34,6 +34,8 @@ import com.philkes.notallyx.presentation.extractColor
3434import com.philkes.notallyx.presentation.getQuantityString
3535import com.philkes.notallyx.presentation.setControlsContrastColorForAllViews
3636import com.philkes.notallyx.presentation.view.misc.ItemListener
37+ import com.philkes.notallyx.presentation.view.misc.highlightableview.HighlightableTextView
38+ import com.philkes.notallyx.presentation.view.misc.highlightableview.SEARCH_SNIPPET_ITEM_LINES
3739import com.philkes.notallyx.presentation.view.note.listitem.init
3840import com.philkes.notallyx.presentation.viewmodel.preference.DateFormat
3941import com.philkes.notallyx.presentation.viewmodel.preference.NotesSortBy
@@ -56,6 +58,12 @@ class BaseNoteVH(
5658 listener : ItemListener ,
5759) : RecyclerView.ViewHolder(binding.root) {
5860
61+ private var searchKeyword: String = " "
62+
63+ fun setSearchKeyword (keyword : String ) {
64+ this .searchKeyword = keyword
65+ }
66+
5967 init {
6068 val title = preferences.textSize.displayTitleSize
6169 val body = preferences.textSize.displayBodySize
@@ -95,8 +103,8 @@ class BaseNoteVH(
95103 updateCheck(checked, baseNote.color)
96104
97105 when (baseNote.type) {
98- Type .NOTE -> bindNote(baseNote.body, baseNote.spans, baseNote.title.isEmpty() )
99- Type .LIST -> bindList(baseNote.items, baseNote.title.isEmpty() )
106+ Type .NOTE -> bindNote(baseNote, searchKeyword )
107+ Type .LIST -> bindList(baseNote, searchKeyword )
100108 }
101109 val (date, datePrefixResId) =
102110 when (sortBy) {
@@ -111,12 +119,18 @@ class BaseNoteVH(
111119 setFiles(baseNote.files)
112120
113121 binding.Title .apply {
114- text = baseNote.title
115122 isVisible = baseNote.title.isNotEmpty()
116123 updatePadding(
117124 bottom =
118125 if (baseNote.hasNoContents() || shouldOnlyDisplayTitle(baseNote)) 0 else 8 .dp
119126 )
127+ if (searchKeyword.isNotBlank()) {
128+ val snippet = extractSearchSnippet(baseNote.title, searchKeyword)
129+ if (snippet != null ) {
130+ showSearchSnippet(snippet)
131+ } else text = baseNote.title
132+ } else text = baseNote.title
133+
120134 setCompoundDrawablesWithIntrinsicBounds(
121135 if (baseNote.type == Type .LIST && preferences.maxItems < 1 )
122136 R .drawable.checkbox_small
@@ -148,9 +162,23 @@ class BaseNoteVH(
148162 binding.RemindersView .isVisible = baseNote.reminders.any { it.hasUpcomingNotification() }
149163 }
150164
151- private fun bindNote (body : String , spans : List < SpanRepresentation >, isTitleEmpty : Boolean ) {
165+ private fun bindNote (baseNote : BaseNote , keyword : String ) {
152166 binding.LinearLayout .visibility = GONE
167+ if (keyword.isBlank()) {
168+ bindNote(baseNote.body, baseNote.spans, baseNote.title.isEmpty())
169+ return
170+ }
171+ binding.Note .apply {
172+ val snippet = extractSearchSnippet(baseNote.body, keyword)
173+ if (snippet == null ) {
174+ bindNote(baseNote.body, baseNote.spans, baseNote.title.isEmpty())
175+ } else {
176+ showSearchSnippet(snippet)
177+ }
178+ }
179+ }
153180
181+ private fun bindNote (body : String , spans : List <SpanRepresentation >, isTitleEmpty : Boolean ) {
154182 binding.Note .apply {
155183 text = body.applySpans(spans)
156184 if (preferences.maxLines < 1 ) {
@@ -162,44 +190,91 @@ class BaseNoteVH(
162190 }
163191 }
164192
165- private fun bindList (items : List <ListItem >, isTitleEmpty : Boolean ) {
193+ /* * Shows a snippet of ListItems around the ListItem that contains keyword */
194+ private fun LinearLayout.bindListSearch (
195+ initializedItems : List <ListItem >,
196+ keyword : String ,
197+ isTitleEmpty : Boolean ,
198+ ) {
199+ binding.LinearLayout .visibility = VISIBLE
200+ val keywordItemIdx =
201+ initializedItems.indexOfFirst { it.body.contains(keyword, ignoreCase = true ) }
202+ if (keywordItemIdx == - 1 ) {
203+ return bindList(initializedItems, isTitleEmpty)
204+ }
205+ val listItemViews = children.filterIsInstance(HighlightableTextView ::class .java).toList()
206+ listItemViews.forEach { it.visibility = GONE }
207+ val startItemIdx = (keywordItemIdx - SEARCH_SNIPPET_ITEM_LINES ).coerceAtLeast(0 )
208+ val endItemIdx =
209+ (keywordItemIdx + SEARCH_SNIPPET_ITEM_LINES ).coerceAtMost(initializedItems.lastIndex)
210+ (startItemIdx.. endItemIdx).forEachIndexed { viewIdx, itemIdx ->
211+ listItemViews[viewIdx].apply {
212+ val item = initializedItems[itemIdx]
213+ text = item.body
214+ if (itemIdx == keywordItemIdx) {
215+ highlight(keyword)
216+ }
217+ handleChecked(this , item.checked)
218+ visibility = VISIBLE
219+ updateLayoutParams<LinearLayout .LayoutParams > {
220+ marginStart = if (item.isChild) 20 .dp else 0
221+ }
222+ }
223+ }
224+ bindItemsRemaining(initializedItems.size, endItemIdx - startItemIdx + 1 )
225+ }
226+
227+ private fun bindList (baseNote : BaseNote , keyword : String ) {
228+ binding.Note .visibility = GONE
229+ val initializedItems = baseNote.items.init ()
230+ if (baseNote.items.isEmpty()) {
231+ binding.LinearLayout .visibility = GONE
232+ return
233+ }
234+ if (keyword.isBlank()) {
235+ bindList(initializedItems, baseNote.title.isEmpty())
236+ return
237+ }
238+ binding.LinearLayout .bindListSearch(initializedItems, keyword, baseNote.title.isEmpty())
239+ }
240+
241+ private fun bindItemsRemaining (totalItems : Int , displayedItems : Int ) {
242+ if (displayedItems > 0 && totalItems > displayedItems) {
243+ binding.ItemsRemaining .apply {
244+ visibility = VISIBLE
245+ text = (totalItems - displayedItems).toString()
246+ }
247+ } else binding.ItemsRemaining .visibility = GONE
248+ }
249+
250+ private fun bindList (initializedItems : List <ListItem >, isTitleEmpty : Boolean ) {
166251 binding.apply {
167- Note .visibility = GONE
168- if (items .isEmpty()) {
252+ bindItemsRemaining(initializedItems.size, preferences.maxItems)
253+ if (initializedItems .isEmpty()) {
169254 LinearLayout .visibility = GONE
170255 } else {
171256 LinearLayout .visibility = VISIBLE
172257 val forceShowFirstItem = preferences.maxItems < 1 && isTitleEmpty
173- val initializedItems = items.init ()
174258 val filteredList =
175259 initializedItems.take(if (forceShowFirstItem) 1 else preferences.maxItems)
176- LinearLayout .children.forEachIndexed { index, view ->
177- if (view.id != R .id.ItemsRemaining ) {
260+ LinearLayout .children
261+ .filterIsInstance(HighlightableTextView ::class .java)
262+ .forEachIndexed { index, view ->
178263 if (index < filteredList.size) {
179264 val item = filteredList[index]
180- ( view as TextView ) .apply {
265+ view.apply {
181266 text = item.body
182267 handleChecked(this , item.checked)
183268 visibility = VISIBLE
184- if (item.isChild) {
185- updateLayoutParams<LinearLayout .LayoutParams > {
186- marginStart = 20 .dp
187- }
269+ updateLayoutParams<LinearLayout .LayoutParams > {
270+ marginStart = if (item.isChild) 20 .dp else 0
188271 }
189272 if (index == filteredList.lastIndex) {
190273 updatePadding(bottom = 0 )
191274 }
192275 }
193276 } else view.visibility = GONE
194277 }
195- }
196-
197- if (preferences.maxItems > 0 && items.size > preferences.maxItems) {
198- ItemsRemaining .apply {
199- visibility = VISIBLE
200- text = (items.size - preferences.maxItems).toString()
201- }
202- } else ItemsRemaining .visibility = GONE
203278 }
204279 }
205280 }
0 commit comments