Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import com.philkes.notallyx.databinding.FragmentNotesBinding
import com.philkes.notallyx.presentation.activity.main.MainActivity
import com.philkes.notallyx.presentation.activity.main.fragment.SearchFragment.Companion.EXTRA_INITIAL_FOLDER
import com.philkes.notallyx.presentation.activity.main.fragment.SearchFragment.Companion.EXTRA_INITIAL_LABEL
import com.philkes.notallyx.presentation.activity.note.EditActivity
import com.philkes.notallyx.presentation.activity.note.EditActivity.Companion.EXTRA_FOLDER_FROM
import com.philkes.notallyx.presentation.activity.note.EditActivity.Companion.EXTRA_FOLDER_TO
import com.philkes.notallyx.presentation.activity.note.EditActivity.Companion.EXTRA_NOTE_ID
Expand Down Expand Up @@ -187,6 +188,7 @@ abstract class NotallyFragment : Fragment(), ItemListener {
binding?.EnterSearchKeywordLayout?.visibility = View.VISIBLE
requestFocus()
activity?.showKeyboard(this)
notesAdapter?.setSearchKeyword(model.keyword)
} else {
// In other fragments, respect the preference
val alwaysShowSearchBar = model.preferences.alwaysShowSearchBar.value
Expand All @@ -195,16 +197,18 @@ abstract class NotallyFragment : Fragment(), ItemListener {
setText("")
clearFocus()
activity?.hideKeyboard(this)
notesAdapter?.setSearchKeyword("")
}
}
doAfterTextChanged { text ->
val isSearchFragment = navController.currentDestination?.id == R.id.Search
if (isSearchFragment) {
model.keyword = requireNotNull(text, { "text is null" }).trim().toString()
model.keyword = requireNotNull(text, { "text is null" }).toString()
notesAdapter?.apply { setSearchKeyword(model.keyword) }
}
if (text?.isNotEmpty() == true && !isSearchFragment) {
setText("")
model.keyword = text.trim().toString()
model.keyword = text.toString()
navController.navigate(
R.id.Search,
Bundle().apply {
Expand All @@ -213,6 +217,9 @@ abstract class NotallyFragment : Fragment(), ItemListener {
},
)
}
this@NotallyFragment.binding?.MainListView?.apply {
postOnAnimationDelayed({ scrollToPosition(0) }, 10)
}
}
}
}
Expand Down Expand Up @@ -299,6 +306,12 @@ abstract class NotallyFragment : Fragment(), ItemListener {
private fun goToActivity(activity: Class<*>, baseNote: BaseNote) {
val intent = Intent(requireContext(), activity)
intent.putExtra(EXTRA_SELECTED_BASE_NOTE, baseNote.id)
// If launched from Search fragment with a non-empty keyword, pass it to the editor to
// auto-highlight
val isInSearch = view?.findNavController()?.currentDestination?.id == R.id.Search
if (isInSearch && model.keyword.isNotBlank()) {
intent.putExtra(EditActivity.EXTRA_INITIAL_SEARCH_QUERY, model.keyword)
}
openNoteActivityResultLauncher.launch(intent)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,21 @@ abstract class EditActivity(private val type: Type) :
}
}

override fun onStart() {
super.onStart()
// If launched with an initial search query (from global search), auto-start in-note search
intent.getStringExtra(EXTRA_INITIAL_SEARCH_QUERY)?.let { initialQuery ->
if (initialQuery.isNotBlank()) {
binding.EnterSearchKeyword.postOnAnimation {
startSearch()
binding.EnterSearchKeyword.setText(initialQuery)
binding.EnterSearchKeyword.setSelection(initialQuery.length)
}
}
intent.removeExtra(EXTRA_INITIAL_SEARCH_QUERY)
}
}

private fun configureEdgeToEdgeInsets() {
WindowCompat.setDecorFitsSystemWindows(window, false)

Expand Down Expand Up @@ -1242,6 +1257,7 @@ abstract class EditActivity(private val type: Type) :
const val EXTRA_NOTE_ID = "notallyx.intent.extra.NOTE_ID"
const val EXTRA_FOLDER_FROM = "notallyx.intent.extra.FOLDER_FROM"
const val EXTRA_FOLDER_TO = "notallyx.intent.extra.FOLDER_TO"
const val EXTRA_INITIAL_SEARCH_QUERY = "notallyx.intent.extra.INITIAL_SEARCH_QUERY"

val DEFAULT_EXCEPTION_HANDLER = Thread.getDefaultUncaughtExceptionHandler()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ class BaseNoteAdapter(
private val listener: ItemListener,
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

private var searchKeyword: String = ""

private var list = SortedList(Item::class.java, notesSort.createCallback())

override fun getItemViewType(position: Int): Int {
Expand All @@ -45,13 +47,19 @@ class BaseNoteAdapter(
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (val item = list[position]) {
is Header -> (holder as HeaderVH).bind(item)
is BaseNote ->
(holder as BaseNoteVH).bind(
item,
imageRoot,
selectedIds.contains(item.id),
notesSort.sortedBy,
)
is BaseNote -> {
(holder as BaseNoteVH).apply {
setSearchKeyword(searchKeyword)
bind(item, imageRoot, selectedIds.contains(item.id), notesSort.sortedBy)
}
}
}
}

fun setSearchKeyword(keyword: String) {
if (searchKeyword != keyword) {
searchKeyword = keyword
notifyDataSetChanged()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ import com.philkes.notallyx.presentation.extractColor
import com.philkes.notallyx.presentation.getQuantityString
import com.philkes.notallyx.presentation.setControlsContrastColorForAllViews
import com.philkes.notallyx.presentation.view.misc.ItemListener
import com.philkes.notallyx.presentation.view.misc.highlightableview.HighlightableTextView
import com.philkes.notallyx.presentation.view.misc.highlightableview.SEARCH_SNIPPET_ITEM_LINES
import com.philkes.notallyx.presentation.view.note.listitem.init
import com.philkes.notallyx.presentation.viewmodel.preference.DateFormat
import com.philkes.notallyx.presentation.viewmodel.preference.NotesSortBy
Expand All @@ -56,6 +58,12 @@ class BaseNoteVH(
listener: ItemListener,
) : RecyclerView.ViewHolder(binding.root) {

private var searchKeyword: String = ""

fun setSearchKeyword(keyword: String) {
this.searchKeyword = keyword
}

init {
val title = preferences.textSize.displayTitleSize
val body = preferences.textSize.displayBodySize
Expand Down Expand Up @@ -95,8 +103,8 @@ class BaseNoteVH(
updateCheck(checked, baseNote.color)

when (baseNote.type) {
Type.NOTE -> bindNote(baseNote.body, baseNote.spans, baseNote.title.isEmpty())
Type.LIST -> bindList(baseNote.items, baseNote.title.isEmpty())
Type.NOTE -> bindNote(baseNote, searchKeyword)
Type.LIST -> bindList(baseNote, searchKeyword)
}
val (date, datePrefixResId) =
when (sortBy) {
Expand All @@ -111,12 +119,18 @@ class BaseNoteVH(
setFiles(baseNote.files)

binding.Title.apply {
text = baseNote.title
isVisible = baseNote.title.isNotEmpty()
updatePadding(
bottom =
if (baseNote.hasNoContents() || shouldOnlyDisplayTitle(baseNote)) 0 else 8.dp
)
if (searchKeyword.isNotBlank()) {
val snippet = extractSearchSnippet(baseNote.title, searchKeyword)
if (snippet != null) {
showSearchSnippet(snippet)
} else text = baseNote.title
} else text = baseNote.title

setCompoundDrawablesWithIntrinsicBounds(
if (baseNote.type == Type.LIST && preferences.maxItems < 1)
R.drawable.checkbox_small
Expand Down Expand Up @@ -148,9 +162,23 @@ class BaseNoteVH(
binding.RemindersView.isVisible = baseNote.reminders.any { it.hasUpcomingNotification() }
}

private fun bindNote(body: String, spans: List<SpanRepresentation>, isTitleEmpty: Boolean) {
private fun bindNote(baseNote: BaseNote, keyword: String) {
binding.LinearLayout.visibility = GONE
if (keyword.isBlank()) {
bindNote(baseNote.body, baseNote.spans, baseNote.title.isEmpty())
return
}
binding.Note.apply {
val snippet = extractSearchSnippet(baseNote.body, keyword)
if (snippet == null) {
bindNote(baseNote.body, baseNote.spans, baseNote.title.isEmpty())
} else {
showSearchSnippet(snippet)
}
}
}

private fun bindNote(body: String, spans: List<SpanRepresentation>, isTitleEmpty: Boolean) {
binding.Note.apply {
text = body.applySpans(spans)
if (preferences.maxLines < 1) {
Expand All @@ -162,44 +190,91 @@ class BaseNoteVH(
}
}

private fun bindList(items: List<ListItem>, isTitleEmpty: Boolean) {
/** Shows a snippet of ListItems around the ListItem that contains keyword */
private fun LinearLayout.bindListSearch(
initializedItems: List<ListItem>,
keyword: String,
isTitleEmpty: Boolean,
) {
binding.LinearLayout.visibility = VISIBLE
val keywordItemIdx =
initializedItems.indexOfFirst { it.body.contains(keyword, ignoreCase = true) }
if (keywordItemIdx == -1) {
return bindList(initializedItems, isTitleEmpty)
}
val listItemViews = children.filterIsInstance(HighlightableTextView::class.java).toList()
listItemViews.forEach { it.visibility = GONE }
val startItemIdx = (keywordItemIdx - SEARCH_SNIPPET_ITEM_LINES).coerceAtLeast(0)
val endItemIdx =
(keywordItemIdx + SEARCH_SNIPPET_ITEM_LINES).coerceAtMost(initializedItems.lastIndex)
(startItemIdx..endItemIdx).forEachIndexed { viewIdx, itemIdx ->
listItemViews[viewIdx].apply {
val item = initializedItems[itemIdx]
text = item.body
if (itemIdx == keywordItemIdx) {
highlight(keyword)
}
handleChecked(this, item.checked)
visibility = VISIBLE
updateLayoutParams<LinearLayout.LayoutParams> {
marginStart = if (item.isChild) 20.dp else 0
}
}
}
bindItemsRemaining(initializedItems.size, endItemIdx - startItemIdx + 1)
}

private fun bindList(baseNote: BaseNote, keyword: String) {
binding.Note.visibility = GONE
val initializedItems = baseNote.items.init()
if (baseNote.items.isEmpty()) {
binding.LinearLayout.visibility = GONE
return
}
if (keyword.isBlank()) {
bindList(initializedItems, baseNote.title.isEmpty())
return
}
binding.LinearLayout.bindListSearch(initializedItems, keyword, baseNote.title.isEmpty())
}

private fun bindItemsRemaining(totalItems: Int, displayedItems: Int) {
if (displayedItems > 0 && totalItems > displayedItems) {
binding.ItemsRemaining.apply {
visibility = VISIBLE
text = (totalItems - displayedItems).toString()
}
} else binding.ItemsRemaining.visibility = GONE
}

private fun bindList(initializedItems: List<ListItem>, isTitleEmpty: Boolean) {
binding.apply {
Note.visibility = GONE
if (items.isEmpty()) {
bindItemsRemaining(initializedItems.size, preferences.maxItems)
if (initializedItems.isEmpty()) {
LinearLayout.visibility = GONE
} else {
LinearLayout.visibility = VISIBLE
val forceShowFirstItem = preferences.maxItems < 1 && isTitleEmpty
val initializedItems = items.init()
val filteredList =
initializedItems.take(if (forceShowFirstItem) 1 else preferences.maxItems)
LinearLayout.children.forEachIndexed { index, view ->
if (view.id != R.id.ItemsRemaining) {
LinearLayout.children
.filterIsInstance(HighlightableTextView::class.java)
.forEachIndexed { index, view ->
if (index < filteredList.size) {
val item = filteredList[index]
(view as TextView).apply {
view.apply {
text = item.body
handleChecked(this, item.checked)
visibility = VISIBLE
if (item.isChild) {
updateLayoutParams<LinearLayout.LayoutParams> {
marginStart = 20.dp
}
updateLayoutParams<LinearLayout.LayoutParams> {
marginStart = if (item.isChild) 20.dp else 0
}
if (index == filteredList.lastIndex) {
updatePadding(bottom = 0)
}
}
} else view.visibility = GONE
}
}

if (preferences.maxItems > 0 && items.size > preferences.maxItems) {
ItemsRemaining.apply {
visibility = VISIBLE
text = (items.size - preferences.maxItems).toString()
}
} else ItemsRemaining.visibility = GONE
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.philkes.notallyx.presentation.view.misc
import android.content.Context
import android.util.AttributeSet
import android.view.KeyEvent
import com.philkes.notallyx.presentation.view.misc.highlightableview.HighlightableEditText

class EditTextAutoClearFocus(context: Context, attributeSet: AttributeSet) :
HighlightableEditText(context, attributeSet) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import com.philkes.notallyx.presentation.createTextWatcherWithHistory
import com.philkes.notallyx.presentation.removeSelectionFromSpans
import com.philkes.notallyx.presentation.setCancelButton
import com.philkes.notallyx.presentation.showAndFocus
import com.philkes.notallyx.presentation.view.misc.highlightableview.HighlightableEditText
import com.philkes.notallyx.utils.changehistory.ChangeHistory
import com.philkes.notallyx.utils.changehistory.EditTextState
import com.philkes.notallyx.utils.changehistory.EditTextWithHistoryChange
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.philkes.notallyx.presentation.view.misc.highlightableview

import android.text.style.BackgroundColorSpan
import androidx.annotation.ColorInt

class HighlightSpan(@ColorInt color: Int) : BackgroundColorSpan(color)
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
package com.philkes.notallyx.presentation.view.misc
package com.philkes.notallyx.presentation.view.misc.highlightableview

import android.content.Context
import android.text.Spanned
import android.text.style.BackgroundColorSpan
import android.text.style.CharacterStyle
import android.util.AttributeSet
import androidx.annotation.ColorInt
import androidx.appcompat.widget.AppCompatEditText
import com.philkes.notallyx.presentation.removeSelectionFromSpans
import com.philkes.notallyx.presentation.view.misc.EditTextWithWatcher
import com.philkes.notallyx.presentation.withAlpha

/**
Expand Down Expand Up @@ -95,5 +94,3 @@ open class HighlightableEditText(context: Context, attrs: AttributeSet) :
}
}
}

class HighlightSpan(@ColorInt color: Int) : BackgroundColorSpan(color)
Loading