From 27f04936a9e8272fa9035d0d437137e446fcbdd4 Mon Sep 17 00:00:00 2001 From: john Date: Sat, 11 Apr 2026 17:01:27 +0800 Subject: [PATCH 1/4] Fix type mismatch and unresolved reference in NotallyActivity - Convert textSize string to float in bindLabels call - Fix pin lambda to call bindPinned instead of shadowed variable --- .../java/com/omgodse/notally/activities/NotallyActivity.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/omgodse/notally/activities/NotallyActivity.kt b/app/src/main/java/com/omgodse/notally/activities/NotallyActivity.kt index 619704f2..a5554cf0 100644 --- a/app/src/main/java/com/omgodse/notally/activities/NotallyActivity.kt +++ b/app/src/main/java/com/omgodse/notally/activities/NotallyActivity.kt @@ -554,7 +554,7 @@ abstract class NotallyActivity(private val type: Type) : AppCompatActivity() { binding.Toolbar.setNavigationOnClickListener { finish() } val menu = binding.Toolbar.menu - val pin = menu.add(R.string.pin, R.drawable.pin) { item -> pin(item) } + val pin = menu.add(R.string.pin, R.drawable.pin) { item -> bindPinned(item) } bindPinned(pin) menu.add(R.string.share, R.drawable.share) { share() } @@ -605,7 +605,7 @@ abstract class NotallyActivity(private val type: Type) : AppCompatActivity() { binding.EnterBody.setTextSize(TypedValue.COMPLEX_UNIT_SP, body) model.labels.observe(this, Observer { labels -> - Operations.bindLabels(binding.LabelGroup, labels, model.textSize) + Operations.bindLabels(binding.LabelGroup, labels, TextSize.getDisplayBodySize(model.textSize)) }) setupColor() From db4abf0aa817b112b3721a7d3adccc08ae5a6e98 Mon Sep 17 00:00:00 2001 From: john Date: Sat, 11 Apr 2026 17:52:28 +0800 Subject: [PATCH 2/4] Add search term highlighting when viewing notes - Pass search keyword when navigating from search results - Highlight matching text in title, body, and list items - Skip BackgroundColorSpan when saving to prevent persistence - Clear search keyword when leaving search view --- .../notally/activities/MainActivity.kt | 2 ++ .../omgodse/notally/activities/MakeList.kt | 2 +- .../notally/activities/NotallyActivity.kt | 28 ++++++++++++++++- .../omgodse/notally/activities/TakeNote.kt | 10 ++++++- .../notally/fragments/NotallyFragment.kt | 4 +++ .../notally/miscellaneous/Constants.kt | 1 + .../recyclerview/adapter/MakeListAdapter.kt | 9 ++++-- .../recyclerview/viewholder/MakeListVH.kt | 30 +++++++++++++++++-- .../notally/viewmodels/NotallyModel.kt | 3 ++ 9 files changed, 82 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/omgodse/notally/activities/MainActivity.kt b/app/src/main/java/com/omgodse/notally/activities/MainActivity.kt index 05c253f5..f59799cd 100644 --- a/app/src/main/java/com/omgodse/notally/activities/MainActivity.kt +++ b/app/src/main/java/com/omgodse/notally/activities/MainActivity.kt @@ -400,6 +400,8 @@ class MainActivity : AppCompatActivity() { inputManager.showSoftInput(binding.EnterSearchKeyword, InputMethodManager.SHOW_IMPLICIT) } else { binding.EnterSearchKeyword.visibility = View.GONE + binding.EnterSearchKeyword.setText(String()) + model.keyword = String() inputManager.hideSoftInputFromWindow(binding.EnterSearchKeyword.windowToken, 0) } } diff --git a/app/src/main/java/com/omgodse/notally/activities/MakeList.kt b/app/src/main/java/com/omgodse/notally/activities/MakeList.kt index ced4b7bc..5e6c86f2 100644 --- a/app/src/main/java/com/omgodse/notally/activities/MakeList.kt +++ b/app/src/main/java/com/omgodse/notally/activities/MakeList.kt @@ -54,7 +54,7 @@ class MakeList : NotallyActivity(Type.LIST) { override fun checkedChanged(position: Int, checked: Boolean) { model.items[position].checked = checked } - }) + }, searchKeyword) binding.RecyclerView.adapter = adapter } diff --git a/app/src/main/java/com/omgodse/notally/activities/NotallyActivity.kt b/app/src/main/java/com/omgodse/notally/activities/NotallyActivity.kt index a5554cf0..91c39441 100644 --- a/app/src/main/java/com/omgodse/notally/activities/NotallyActivity.kt +++ b/app/src/main/java/com/omgodse/notally/activities/NotallyActivity.kt @@ -16,6 +16,7 @@ import android.os.Bundle import android.provider.Settings import android.text.Editable import android.text.Spannable +import android.text.SpannableString import android.text.style.BackgroundColorSpan import android.util.TypedValue import android.view.KeyEvent @@ -69,6 +70,7 @@ abstract class NotallyActivity(private val type: Type) : AppCompatActivity() { internal lateinit var binding: ActivityNotallyBinding internal val model: NotallyModel by viewModels() + internal var searchKeyword: String = String() override fun finish() { lifecycleScope.launch { @@ -96,6 +98,7 @@ abstract class NotallyActivity(private val type: Type) : AppCompatActivity() { lifecycleScope.launch { if (model.isFirstInstance) { + searchKeyword = intent.getStringExtra(Constants.SearchKeyword) ?: String() val persistedId = savedInstanceState?.getLong("id") val selectedId = intent.getLongExtra(Constants.SelectedBaseNote, 0L) val id = persistedId ?: selectedId @@ -194,7 +197,30 @@ abstract class NotallyActivity(private val type: Type) : AppCompatActivity() { val formatter = DateFormat.getDateInstance(DateFormat.FULL) binding.DateCreated.text = formatter.format(model.timestamp) - binding.EnterTitle.setText(model.title) + val title = model.title + if (searchKeyword.isNotEmpty() && title.isNotEmpty()) { + val spannable = SpannableString(title) + highlightText(spannable, searchKeyword) + binding.EnterTitle.setText(spannable) + } else { + binding.EnterTitle.setText(title) + } + } + + protected fun highlightText(spannable: Spannable, keyword: String) { + val highlightColor = getColor(R.color.LightBlue100) + var index = 0 + while (index < spannable.length) { + val matchIndex = spannable.toString().indexOf(keyword, index, ignoreCase = true) + if (matchIndex == -1) break + spannable.setSpan( + BackgroundColorSpan(highlightColor), + matchIndex, + matchIndex + keyword.length, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) + index = matchIndex + keyword.length + } } diff --git a/app/src/main/java/com/omgodse/notally/activities/TakeNote.kt b/app/src/main/java/com/omgodse/notally/activities/TakeNote.kt index bbefc960..4a52e518 100644 --- a/app/src/main/java/com/omgodse/notally/activities/TakeNote.kt +++ b/app/src/main/java/com/omgodse/notally/activities/TakeNote.kt @@ -3,6 +3,7 @@ package com.omgodse.notally.activities import android.content.Intent import android.graphics.Typeface import android.net.Uri +import android.text.SpannableString import android.text.Spanned import android.text.style.CharacterStyle import android.text.style.StrikethroughSpan @@ -47,7 +48,14 @@ class TakeNote : NotallyActivity(Type.NOTE) { override fun setStateFromModel() { super.setStateFromModel() - binding.EnterBody.text = model.body + val body = model.body + if (searchKeyword.isNotEmpty() && body.isNotEmpty()) { + val spannable = SpannableString(body) + highlightText(spannable, searchKeyword) + binding.EnterBody.setText(spannable) + } else { + binding.EnterBody.text = body + } } diff --git a/app/src/main/java/com/omgodse/notally/fragments/NotallyFragment.kt b/app/src/main/java/com/omgodse/notally/fragments/NotallyFragment.kt index 1623632b..ef156685 100644 --- a/app/src/main/java/com/omgodse/notally/fragments/NotallyFragment.kt +++ b/app/src/main/java/com/omgodse/notally/fragments/NotallyFragment.kt @@ -138,6 +138,10 @@ abstract class NotallyFragment : Fragment(), ItemListener { private fun goToActivity(activity: Class<*>, baseNote: BaseNote) { val intent = Intent(requireContext(), activity) intent.putExtra(Constants.SelectedBaseNote, baseNote.id) + val keyword = model.keyword + if (keyword.isNotEmpty()) { + intent.putExtra(Constants.SearchKeyword, keyword) + } startActivity(intent) } diff --git a/app/src/main/java/com/omgodse/notally/miscellaneous/Constants.kt b/app/src/main/java/com/omgodse/notally/miscellaneous/Constants.kt index c43ceb05..28b49eb2 100644 --- a/app/src/main/java/com/omgodse/notally/miscellaneous/Constants.kt +++ b/app/src/main/java/com/omgodse/notally/miscellaneous/Constants.kt @@ -3,4 +3,5 @@ package com.omgodse.notally.miscellaneous object Constants { const val SelectedLabel = "SelectedLabel" const val SelectedBaseNote = "SelectedBaseNote" + const val SearchKeyword = "SearchKeyword" } \ No newline at end of file diff --git a/app/src/main/java/com/omgodse/notally/recyclerview/adapter/MakeListAdapter.kt b/app/src/main/java/com/omgodse/notally/recyclerview/adapter/MakeListAdapter.kt index a5de812e..a0db2385 100644 --- a/app/src/main/java/com/omgodse/notally/recyclerview/adapter/MakeListAdapter.kt +++ b/app/src/main/java/com/omgodse/notally/recyclerview/adapter/MakeListAdapter.kt @@ -1,9 +1,13 @@ package com.omgodse.notally.recyclerview.adapter +import android.text.SpannableString +import android.text.Spannable +import android.text.style.BackgroundColorSpan import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView +import com.omgodse.notally.R import com.omgodse.notally.databinding.RecyclerListItemBinding import com.omgodse.notally.recyclerview.DragCallback import com.omgodse.notally.recyclerview.ListItemListener @@ -14,7 +18,8 @@ class MakeListAdapter( private val textSize: String, elevation: Float, val list: ArrayList, - private val listener: ListItemListener + private val listener: ListItemListener, + private val searchKeyword: String = String() ) : RecyclerView.Adapter() { private val callback = DragCallback(elevation, this) @@ -29,7 +34,7 @@ class MakeListAdapter( override fun onBindViewHolder(holder: MakeListVH, position: Int) { val item = list[position] - holder.bind(item) + holder.bind(item, searchKeyword) } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MakeListVH { diff --git a/app/src/main/java/com/omgodse/notally/recyclerview/viewholder/MakeListVH.kt b/app/src/main/java/com/omgodse/notally/recyclerview/viewholder/MakeListVH.kt index 18f2ca1a..05a17244 100644 --- a/app/src/main/java/com/omgodse/notally/recyclerview/viewholder/MakeListVH.kt +++ b/app/src/main/java/com/omgodse/notally/recyclerview/viewholder/MakeListVH.kt @@ -1,10 +1,14 @@ package com.omgodse.notally.recyclerview.viewholder +import android.text.Spannable +import android.text.SpannableString +import android.text.style.BackgroundColorSpan import android.util.TypedValue import android.view.MotionEvent import androidx.core.widget.doAfterTextChanged import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView +import com.omgodse.notally.R import com.omgodse.notally.databinding.RecyclerListItemBinding import com.omgodse.notally.miscellaneous.setOnNextAction import com.omgodse.notally.preferences.TextSize @@ -47,9 +51,31 @@ class MakeListVH( } } - fun bind(item: ListItem) { + fun bind(item: ListItem, searchKeyword: String = String()) { binding.root.reset() - binding.EditText.setText(item.body) + if (searchKeyword.isNotEmpty() && item.body.isNotEmpty()) { + val spannable = SpannableString(item.body) + highlightText(spannable, searchKeyword) + binding.EditText.setText(spannable) + } else { + binding.EditText.setText(item.body) + } binding.CheckBox.isChecked = item.checked } + + private fun highlightText(spannable: Spannable, keyword: String) { + val highlightColor = itemView.context.getColor(R.color.LightBlue100) + var index = 0 + while (index < spannable.length) { + val matchIndex = spannable.toString().indexOf(keyword, index, ignoreCase = true) + if (matchIndex == -1) break + spannable.setSpan( + BackgroundColorSpan(highlightColor), + matchIndex, + matchIndex + keyword.length, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) + index = matchIndex + keyword.length + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/omgodse/notally/viewmodels/NotallyModel.kt b/app/src/main/java/com/omgodse/notally/viewmodels/NotallyModel.kt index 63e92b7e..cc7294c4 100644 --- a/app/src/main/java/com/omgodse/notally/viewmodels/NotallyModel.kt +++ b/app/src/main/java/com/omgodse/notally/viewmodels/NotallyModel.kt @@ -10,6 +10,7 @@ import android.net.Uri import android.text.Editable import android.text.SpannableStringBuilder import android.text.Spanned +import android.text.style.BackgroundColorSpan import android.text.style.CharacterStyle import android.text.style.StrikethroughSpan import android.text.style.StyleSpan @@ -328,6 +329,8 @@ class NotallyModel(private val app: Application) : AndroidViewModel(app) { private fun getFilteredSpans(spanned: Spanned): ArrayList { val representations = LinkedHashSet() spanned.getSpans().forEach { span -> + if (span is BackgroundColorSpan) return@forEach + val end = spanned.getSpanEnd(span) val start = spanned.getSpanStart(span) val representation = SpanRepresentation(false, false, false, false, false, start, end) From 1c07941a6fa598b04d09e937c5954ae63e3816a7 Mon Sep 17 00:00:00 2001 From: john Date: Sat, 11 Apr 2026 18:30:30 +0800 Subject: [PATCH 3/4] Fix empty note creation when no content is entered - Remove createBaseNote call from setState - Only save note when content is not empty - Handle new note creation in saveNote method --- .../notally/viewmodels/NotallyModel.kt | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/omgodse/notally/viewmodels/NotallyModel.kt b/app/src/main/java/com/omgodse/notally/viewmodels/NotallyModel.kt index cc7294c4..df85e40c 100644 --- a/app/src/main/java/com/omgodse/notally/viewmodels/NotallyModel.kt +++ b/app/src/main/java/com/omgodse/notally/viewmodels/NotallyModel.kt @@ -278,14 +278,9 @@ class NotallyModel(private val app: Application) : AndroidViewModel(app) { audios.value = baseNote.audios reminder.value = baseNote.reminder } else { - createBaseNote() Toast.makeText(app, R.string.cant_find_note, Toast.LENGTH_LONG).show() } - } else createBaseNote() - } - - private suspend fun createBaseNote() { - id = withContext(Dispatchers.IO) { baseNoteDao.insert(getBaseNote()) } + } } @@ -303,7 +298,20 @@ class NotallyModel(private val app: Application) : AndroidViewModel(app) { } suspend fun saveNote(): Long { - return withContext(Dispatchers.IO) { baseNoteDao.insert(getBaseNote()) } + if (isEmpty()) return 0L + return withContext(Dispatchers.IO) { + val savedId = baseNoteDao.insert(getBaseNote()) + if (isNewNote) { + id = savedId + isNewNote = false + } + savedId + } + } + + private fun isEmpty(): Boolean { + val bodyText = body.trimEnd().toString() + return title.isEmpty() && bodyText.isEmpty() && items.none { it.body.isNotEmpty() } } private suspend fun updateImages() { From 245159f00673ebfc9fbd31c6f84c3877d52de69f Mon Sep 17 00:00:00 2001 From: john Date: Sat, 11 Apr 2026 18:31:45 +0800 Subject: [PATCH 4/4] Add swipe-to-select for bulk note selection - Swipe across notes to select/deselect them - Start selection mode by swiping past 30% of an item width --- .../notally/fragments/NotallyFragment.kt | 19 ++++++ .../recyclerview/SwipeSelectionListener.kt | 67 +++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 app/src/main/java/com/omgodse/notally/recyclerview/SwipeSelectionListener.kt diff --git a/app/src/main/java/com/omgodse/notally/fragments/NotallyFragment.kt b/app/src/main/java/com/omgodse/notally/fragments/NotallyFragment.kt index ef156685..3fdcedca 100644 --- a/app/src/main/java/com/omgodse/notally/fragments/NotallyFragment.kt +++ b/app/src/main/java/com/omgodse/notally/fragments/NotallyFragment.kt @@ -17,6 +17,7 @@ import com.omgodse.notally.activities.TakeNote import com.omgodse.notally.databinding.FragmentNotesBinding import com.omgodse.notally.miscellaneous.Constants import com.omgodse.notally.recyclerview.ItemListener +import com.omgodse.notally.recyclerview.SwipeSelectionListener import com.omgodse.notally.recyclerview.adapter.BaseNoteAdapter import com.omgodse.notally.room.BaseNote import com.omgodse.notally.room.Item @@ -132,6 +133,24 @@ abstract class NotallyFragment : Fragment(), ItemListener { binding?.RecyclerView?.layoutManager = if (model.preferences.view.value == ViewPref.grid) { StaggeredGridLayoutManager(2, RecyclerView.VERTICAL) } else LinearLayoutManager(requireContext()) + + binding?.RecyclerView?.addOnItemTouchListener(SwipeSelectionListener(binding?.RecyclerView) { position -> + onItemIntercept(position) + }) + } + + private fun onItemIntercept(position: Int) { + if (position != -1) { + adapter?.currentList?.getOrNull(position)?.let { item -> + if (item is BaseNote) { + if (!model.actionMode.isEnabled()) { + model.actionMode.add(item.id, item) + adapter?.notifyItemChanged(position, 0) + } + handleNoteSelection(item.id, position, item) + } + } + } } diff --git a/app/src/main/java/com/omgodse/notally/recyclerview/SwipeSelectionListener.kt b/app/src/main/java/com/omgodse/notally/recyclerview/SwipeSelectionListener.kt new file mode 100644 index 00000000..506d24db --- /dev/null +++ b/app/src/main/java/com/omgodse/notally/recyclerview/SwipeSelectionListener.kt @@ -0,0 +1,67 @@ +package com.omgodse.notally.recyclerview + +import android.annotation.SuppressLint +import android.view.MotionEvent +import android.view.View +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView + +class SwipeSelectionListener( + private val recyclerView: RecyclerView?, + private val onItemIntercept: (Int) -> Unit +) : RecyclerView.OnItemTouchListener { + + private var isSwipeSelecting = false + private var lastInterceptedPosition = -1 + + @SuppressLint("ClickableViewAccessibility") + override fun onTouchEvent(rv: RecyclerView, e: MotionEvent) { + when (e.actionMasked) { + MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { + isSwipeSelecting = false + lastInterceptedPosition = -1 + } + } + } + + override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean { + when (e.actionMasked) { + MotionEvent.ACTION_DOWN -> { + isSwipeSelecting = false + lastInterceptedPosition = -1 + } + MotionEvent.ACTION_MOVE -> { + if (!isSwipeSelecting) { + val childView = findChildViewUnder(e.x, e.y) + if (childView != null) { + val position = recyclerView?.getChildAdapterPosition(childView) ?: -1 + if (position != RecyclerView.NO_POSITION) { + val dx = abs(e.x - (childView.left + childView.width / 2f)) + if (dx > childView.width * 0.3f) { + isSwipeSelecting = true + lastInterceptedPosition = position + onItemIntercept(position) + } + } + } + } else { + val childView = findChildViewUnder(e.x, e.y) + if (childView != null) { + val position = recyclerView?.getChildAdapterPosition(childView) ?: -1 + if (position != RecyclerView.NO_POSITION && position != lastInterceptedPosition) { + lastInterceptedPosition = position + onItemIntercept(position) + } + } + } + } + } + return false + } + + override fun onRequestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {} + + private fun findChildViewUnder(x: Float, y: Float): View? { + return recyclerView?.findChildViewUnder(x, y) + } +}