Skip to content

Commit

Permalink
Note item creation refactoring, prioritize highlights in list notes
Browse files Browse the repository at this point in the history
- Moved item creation logic to separate class to make it testable, especially for list notes
- List note items with highlights are prioritized to show as many as possible in preview.
  • Loading branch information
maltaisn committed Aug 28, 2021
1 parent f0add4d commit 171a3cd
Show file tree
Hide file tree
Showing 24 changed files with 1,218 additions and 309 deletions.
4 changes: 2 additions & 2 deletions app/src/main/kotlin/com/maltaisn/notes/model/PrefsManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ class PrefsManager @Inject constructor(
val sortSettings: SortSettings
get() = SortSettings(sortField, sortDirection)

fun getMaximumPreviewLines(layoutMode: NoteListLayoutMode, noteType: NoteType): Int {
val key = when (layoutMode) {
fun getMaximumPreviewLines(noteType: NoteType): Int {
val key = when (listLayoutMode) {
NoteListLayoutMode.LIST -> when (noteType) {
NoteType.TEXT -> PREVIEW_LINES_TEXT_LIST
NoteType.LIST -> PREVIEW_LINES_LIST_LIST
Expand Down
9 changes: 5 additions & 4 deletions app/src/main/kotlin/com/maltaisn/notes/model/entity/Note.kt
Original file line number Diff line number Diff line change
Expand Up @@ -129,22 +129,23 @@ data class Note(

/**
* If note is a list note, returns a list of the items in it.
* The items text is trimmed.
*/
val listItems: List<ListNoteItem>
val listItems: MutableList<ListNoteItem>
get() {
check(type == NoteType.LIST) { "Cannot get list items for non-list note." }

val checked = (metadata as ListNoteMetadata).checked
val items = content.split('\n')
if (items.size == 1 && checked.isEmpty()) {
// No items
return emptyList()
return mutableListOf()
}

check(checked.size == items.size) { "Invalid list note data." }

return items.mapIndexed { i, text ->
ListNoteItem(text, checked[i])
return items.mapIndexedTo(mutableListOf()) { i, text ->
ListNoteItem(text.trim(), checked[i])
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ import java.text.DateFormat
/**
* Interface implemented by any item that can have its focus position changed.
*/
interface EditFocusableViewHolder {
sealed interface EditFocusableViewHolder {
fun setFocus(pos: Int)
}

Expand Down
28 changes: 17 additions & 11 deletions app/src/main/kotlin/com/maltaisn/notes/ui/home/HomeViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import com.maltaisn.notes.sync.R
import com.maltaisn.notes.ui.AssistedSavedStateViewModelFactory
import com.maltaisn.notes.ui.Event
import com.maltaisn.notes.ui.navigation.HomeDestination
import com.maltaisn.notes.ui.note.NoteItemFactory
import com.maltaisn.notes.ui.note.NoteViewModel
import com.maltaisn.notes.ui.note.PlaceholderData
import com.maltaisn.notes.ui.note.SwipeAction
Expand All @@ -54,8 +55,9 @@ class HomeViewModel @AssistedInject constructor(
labelsRepository: LabelsRepository,
prefs: PrefsManager,
reminderAlarmManager: ReminderAlarmManager,
noteItemFactory: NoteItemFactory,
private val buildTypeBehavior: BuildTypeBehavior,
) : NoteViewModel(savedStateHandle, notesRepository, labelsRepository, prefs, reminderAlarmManager),
) : NoteViewModel(savedStateHandle, notesRepository, labelsRepository, prefs, noteItemFactory, reminderAlarmManager),
NoteAdapter.Callback {

var currentDestination: HomeDestination = HomeDestination.Status(NoteStatus.ACTIVE)
Expand Down Expand Up @@ -298,10 +300,8 @@ class HomeViewModel @AssistedInject constructor(
addedNotPinnedHeader = true
}

val checked = isNoteSelected(note)
// Omit the filtered label from the note since all notes have it.
val labels = noteWithLabels.labels - label
this += NoteItem(note.id, note, labels, checked)
addNoteItem(noteWithLabels, excludeLabel = label)
}
}

Expand Down Expand Up @@ -329,8 +329,7 @@ class HomeViewModel @AssistedInject constructor(
var addedUpcomingHeader = false

for (noteWithLabels in notes) {
val note = noteWithLabels.note
val reminderTime = (note.reminder ?: continue).next.time
val reminderTime = (noteWithLabels.note.reminder ?: continue).next.time

if (!addedOverdueHeader && reminderTime <= now) {
// Reminder is past, add overdue header before it
Expand All @@ -347,16 +346,23 @@ class HomeViewModel @AssistedInject constructor(
}

// Show "Mark as done" action button.
val checked = isNoteSelected(note)
this += NoteItem(note.id, note, noteWithLabels.labels, checked,
showMarkAsDone = reminderTime <= now)
addNoteItem(noteWithLabels, showMarkAsDone = reminderTime <= now)
}
}

private fun MutableList<NoteListItem>.addNoteItem(noteWithLabels: NoteWithLabels) {
private fun MutableList<NoteListItem>.addNoteItem(
noteWithLabels: NoteWithLabels,
showMarkAsDone: Boolean = false,
excludeLabel: Label? = null
) {
val note = noteWithLabels.note
val checked = isNoteSelected(note)
this += NoteItem(note.id, note, noteWithLabels.labels, checked)
val labels = if (excludeLabel == null) {
noteWithLabels.labels
} else {
noteWithLabels.labels - excludeLabel
}
this += noteItemFactory.createItem(note, labels, checked, showMarkAsDone)
}

override fun updatePlaceholder() = when (val destination = currentDestination) {
Expand Down
112 changes: 39 additions & 73 deletions app/src/main/kotlin/com/maltaisn/notes/ui/note/HighlightHelper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,97 +16,63 @@

package com.maltaisn.notes.ui.note

import android.text.SpannableString
import android.text.style.BackgroundColorSpan
import android.text.style.ForegroundColorSpan
import androidx.core.text.set
import com.maltaisn.notes.model.entity.ListNoteItem

/**
* Helper class to add highlight spans on search results.
*/
object HighlightHelper {

private const val START_ELLIPSIS = "\u2026\uFEFF"
const val START_ELLIPSIS = "\u2026\uFEFF"

/**
* Find a [max] of highlight positions for matches of a [query] in a [text].
*/
fun findHighlightsInString(text: String, query: String, max: Int = Int.MAX_VALUE) = buildList<IntRange> {
var i = 0
while (i < text.length) {
i = text.indexOf(query, i, ignoreCase = true)
if (i == -1) {
break
}
this += i..(i + query.length)
if (size == max) {
break
fun findHighlightsInString(text: String, query: String, max: Int = Int.MAX_VALUE): MutableList<IntRange> {
val highlights = mutableListOf<IntRange>()
if (max > 0) {
var i = 0
while (i < text.length) {
i = text.indexOf(query, i, ignoreCase = true)
if (i == -1) {
break
}
highlights += i..(i + query.length)
if (highlights.size == max) {
break
}
i++
}
i++
}
return highlights
}

/**
* Group highlights of a list note by item.
* Highlight indices are also shifted to the item's content.
* Ellipsize start of text of first highlight is beyond threshold.
* Leave a certain number of characters between the ellipsis and the highlight (the "distance")
* If start is ellipsized, highlights are shifted according, in place.
*/
fun splitListNoteHighlightsByItem(items: List<ListNoteItem>, highlights: List<IntRange>): List<List<IntRange>> {
var currHighlightIndex = 0
var contentStartPos = 0
val itemHighlights = mutableListOf<List<IntRange>>()
for (item in items) {
val contentEndPos = contentStartPos + item.content.length + 1
itemHighlights += buildList<IntRange> {
while (currHighlightIndex < highlights.size) {
val highlight = highlights[currHighlightIndex]
if (highlight.first >= contentStartPos && highlight.last < contentEndPos) {
this += (highlight.first - contentStartPos)..(highlight.last - contentStartPos)
currHighlightIndex++
} else {
break
fun getStartEllipsizedText(
text: String,
highlights: MutableList<IntRange>,
startEllipsisThreshold: Int,
startEllipsisDistance: Int
): Highlighted {
var ellipsizedText = text
if (highlights.isNotEmpty()) {
val firstIndex = highlights.first().first
if (firstIndex > startEllipsisThreshold) {
var highlightShift = firstIndex - startEllipsisDistance
while (text[highlightShift].isWhitespace()) {
highlightShift++
}
highlightShift -= START_ELLIPSIS.length
if (highlightShift > 0) {
ellipsizedText = START_ELLIPSIS + text.substring(highlightShift + START_ELLIPSIS.length)
for ((i, highlight) in highlights.withIndex()) {
highlights[i] = (highlight.first - highlightShift)..(highlight.last - highlightShift)
}
}
}
contentStartPos = contentEndPos
}
return itemHighlights
return Highlighted(ellipsizedText, highlights)
}

/**
* Creates a spannable string of a [text] with background spans of a [bgColor] for [highlights].
*/
fun getHighlightedText(text: String, highlights: List<IntRange>, bgColor: Int, fgColor: Int,
startEllipsisThreshold: Int, startEllipsisDistance: Int): CharSequence {
if (highlights.isEmpty()) {
return text
}

// Ellipsize start of text of first highlight is beyond threshold.
// Leave a certain number of characters between the ellipsis and the highlight (the "distance")
var textEllipsis = text
val firstIndex = highlights.first().first
var highlightShift = 0
if (firstIndex > startEllipsisThreshold) {
highlightShift = firstIndex - START_ELLIPSIS.length - startEllipsisDistance
if (highlightShift > 0) {
textEllipsis = START_ELLIPSIS + text.substring(firstIndex - startEllipsisDistance)
} else {
highlightShift = 0
}
}

val highlightedText = SpannableString(textEllipsis)
for (highlight in highlights) {
val range = if (highlightShift == 0) {
highlight
} else {
(highlight.first - highlightShift)..(highlight.last - highlightShift)
}
highlightedText[range] = BackgroundColorSpan(bgColor)
highlightedText[range] = ForegroundColorSpan(fgColor)
}
return highlightedText
}

}
25 changes: 25 additions & 0 deletions app/src/main/kotlin/com/maltaisn/notes/ui/note/Highlighted.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright 2021 Nicolas Maltais
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.maltaisn.notes.ui.note

/**
* Text with a list of highlighted ranges.
*/
data class Highlighted(
val content: String,
val highlights: List<IntRange> = emptyList()
)
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ abstract class NoteFragment : Fragment(), ActionMode.Callback, ConfirmDialog.Cal
NoteListLayoutMode.LIST -> R.integer.note_list_layout_span_count
NoteListLayoutMode.GRID -> R.integer.note_grid_layout_span_count
})
adapter.listLayoutMode = mode
adapter.updateForListLayoutChange()
}

viewModel.editItemEvent.observeEvent(viewLifecycleOwner) { noteId ->
Expand Down
Loading

0 comments on commit 171a3cd

Please sign in to comment.