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 619704f2..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 + } } @@ -554,7 +580,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 +631,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() 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..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,12 +133,34 @@ 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) + } + } + } } 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/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) + } +} 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..df85e40c 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 @@ -277,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()) } + } } @@ -302,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() { @@ -328,6 +337,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)