diff --git a/app/src/main/java/dev/arkbuilders/arkmemo/models/GraphicNote.kt b/app/src/main/java/dev/arkbuilders/arkmemo/models/GraphicNote.kt index ddafa1a3..c18173dc 100644 --- a/app/src/main/java/dev/arkbuilders/arkmemo/models/GraphicNote.kt +++ b/app/src/main/java/dev/arkbuilders/arkmemo/models/GraphicNote.kt @@ -17,4 +17,5 @@ data class GraphicNote( override var resource: Resource? = null, override var pendingForDelete: Boolean = false, var thumb: Bitmap? = null, + override var selected: Boolean = false, ) : Note, Parcelable diff --git a/app/src/main/java/dev/arkbuilders/arkmemo/models/Note.kt b/app/src/main/java/dev/arkbuilders/arkmemo/models/Note.kt index 2ad3cfe5..85ecbefc 100644 --- a/app/src/main/java/dev/arkbuilders/arkmemo/models/Note.kt +++ b/app/src/main/java/dev/arkbuilders/arkmemo/models/Note.kt @@ -7,4 +7,5 @@ interface Note { val description: String var resource: Resource? var pendingForDelete: Boolean + var selected: Boolean } diff --git a/app/src/main/java/dev/arkbuilders/arkmemo/models/TextNote.kt b/app/src/main/java/dev/arkbuilders/arkmemo/models/TextNote.kt index c8af2df8..6d162c97 100644 --- a/app/src/main/java/dev/arkbuilders/arkmemo/models/TextNote.kt +++ b/app/src/main/java/dev/arkbuilders/arkmemo/models/TextNote.kt @@ -13,4 +13,5 @@ data class TextNote( @IgnoredOnParcel override var resource: Resource? = null, override var pendingForDelete: Boolean = false, + override var selected: Boolean = false, ) : Note, Parcelable diff --git a/app/src/main/java/dev/arkbuilders/arkmemo/models/VoiceNote.kt b/app/src/main/java/dev/arkbuilders/arkmemo/models/VoiceNote.kt index b3f08a80..1e586a46 100644 --- a/app/src/main/java/dev/arkbuilders/arkmemo/models/VoiceNote.kt +++ b/app/src/main/java/dev/arkbuilders/arkmemo/models/VoiceNote.kt @@ -22,4 +22,5 @@ class VoiceNote( var waitToBeResumed: Boolean = false, var currentPlayingPos: Int = 0, var currentMaxAmplitude: Int = 0, + override var selected: Boolean = false, ) : Note, Parcelable diff --git a/app/src/main/java/dev/arkbuilders/arkmemo/repo/NotesRepo.kt b/app/src/main/java/dev/arkbuilders/arkmemo/repo/NotesRepo.kt index 605be774..ab92ae34 100644 --- a/app/src/main/java/dev/arkbuilders/arkmemo/repo/NotesRepo.kt +++ b/app/src/main/java/dev/arkbuilders/arkmemo/repo/NotesRepo.kt @@ -13,4 +13,6 @@ interface NotesRepo { suspend fun read(): List suspend fun delete(note: Note) + + suspend fun delete(notes: List) } diff --git a/app/src/main/java/dev/arkbuilders/arkmemo/repo/NotesRepoHelper.kt b/app/src/main/java/dev/arkbuilders/arkmemo/repo/NotesRepoHelper.kt index 85b7ea0d..63d4c426 100644 --- a/app/src/main/java/dev/arkbuilders/arkmemo/repo/NotesRepoHelper.kt +++ b/app/src/main/java/dev/arkbuilders/arkmemo/repo/NotesRepoHelper.kt @@ -91,6 +91,13 @@ class NotesRepoHelper return UserNoteProperties(title, description) } + suspend fun deleteNotes(notes: List): Unit = + withContext(Dispatchers.IO) { + notes.forEach { note -> + deleteNote(note) + } + } + suspend fun deleteNote(note: Note): Unit = withContext(Dispatchers.IO) { val id = note.resource?.id diff --git a/app/src/main/java/dev/arkbuilders/arkmemo/repo/graphics/GraphicNotesRepo.kt b/app/src/main/java/dev/arkbuilders/arkmemo/repo/graphics/GraphicNotesRepo.kt index c1d0302d..055773da 100644 --- a/app/src/main/java/dev/arkbuilders/arkmemo/repo/graphics/GraphicNotesRepo.kt +++ b/app/src/main/java/dev/arkbuilders/arkmemo/repo/graphics/GraphicNotesRepo.kt @@ -70,6 +70,10 @@ class GraphicNotesRepo helper.deleteNote(note) } + override suspend fun delete(notes: List) { + helper.deleteNotes(notes) + } + override suspend fun read(): List = withContext(iODispatcher) { readStorage() diff --git a/app/src/main/java/dev/arkbuilders/arkmemo/repo/text/TextNotesRepo.kt b/app/src/main/java/dev/arkbuilders/arkmemo/repo/text/TextNotesRepo.kt index 0ac224bf..93521be0 100644 --- a/app/src/main/java/dev/arkbuilders/arkmemo/repo/text/TextNotesRepo.kt +++ b/app/src/main/java/dev/arkbuilders/arkmemo/repo/text/TextNotesRepo.kt @@ -46,6 +46,10 @@ class TextNotesRepo write(note) { callback(it) } } + override suspend fun delete(notes: List) { + helper.deleteNotes(notes) + } + override suspend fun delete(note: TextNote) { helper.deleteNote(note) } diff --git a/app/src/main/java/dev/arkbuilders/arkmemo/repo/voices/VoiceNotesRepo.kt b/app/src/main/java/dev/arkbuilders/arkmemo/repo/voices/VoiceNotesRepo.kt index abda016f..e83a4e30 100644 --- a/app/src/main/java/dev/arkbuilders/arkmemo/repo/voices/VoiceNotesRepo.kt +++ b/app/src/main/java/dev/arkbuilders/arkmemo/repo/voices/VoiceNotesRepo.kt @@ -42,6 +42,10 @@ class VoiceNotesRepo readStorage() } + override suspend fun delete(notes: List) { + helper.deleteNotes(notes) + } + override suspend fun delete(note: VoiceNote) { helper.deleteNote(note) } diff --git a/app/src/main/java/dev/arkbuilders/arkmemo/ui/adapters/NotesListAdapter.kt b/app/src/main/java/dev/arkbuilders/arkmemo/ui/adapters/NotesListAdapter.kt index 65d5892d..02439ef1 100644 --- a/app/src/main/java/dev/arkbuilders/arkmemo/ui/adapters/NotesListAdapter.kt +++ b/app/src/main/java/dev/arkbuilders/arkmemo/ui/adapters/NotesListAdapter.kt @@ -4,9 +4,11 @@ import android.graphics.drawable.BitmapDrawable import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.CompoundButton import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat import androidx.core.content.res.ResourcesCompat +import androidx.lifecycle.MutableLiveData import androidx.recyclerview.widget.RecyclerView import by.kirich1409.viewbindingdelegate.viewBinding import com.google.android.material.shape.CornerFamily @@ -35,6 +37,8 @@ class NotesListAdapter( private val onPlayPauseClick: (path: String, pos: Int?, stopCallback: ((pos: Int) -> Unit)?) -> Unit, ) : RecyclerView.Adapter() { private lateinit var activity: MainActivity + private var mActionMode = false + private var checkedByItemClick = false lateinit var observeItemSideEffect: () -> ArkMediaPlayerSideEffect lateinit var observeItemState: () -> ArkMediaPlayerState @@ -46,6 +50,13 @@ class NotesListAdapter( activity.resources.getDimension(R.dimen.corner_radius_big) } + var onItemLongPressed: ((pos: Int, note: Note) -> Unit)? = null + var onItemClicked: (() -> Unit)? = null + + private val selectedNotesCount by lazy { MutableLiveData() } + val observableSelectedNotesCount by lazy { selectedNotesCount } + val selectedNotesForDelete = mutableListOf() + fun setActivity(activity: AppCompatActivity) { this.activity = activity as MainActivity } @@ -87,13 +98,19 @@ class NotesListAdapter( } holder.btnPlayPause.setOnClickListener { - onPlayPauseClick(note.path.toString(), position) { stopPos -> + onPlayPauseClick(note.path.toString(), holder.bindingAdapterPosition) { stopPos -> + val realPos = + if (holder.bindingAdapterPosition >= 0) { + holder.bindingAdapterPosition + } else { + position + } showPlaybackIdleState(holder) - (notes[position] as VoiceNote).isPlaying = false + (notes[realPos] as VoiceNote).isPlaying = false holder.layoutAudioView.animAudioPlaying.resetWave() holder.layoutAudioView.animAudioPlaying.invalidateWave(0) holder.tvPlayingPosition.gone() - notifyItemChanged(position) + notifyItemChanged(realPos) } handleMediaPlayerSideEffect(observeItemSideEffect(), holder) note.isPlaying = !note.isPlaying @@ -141,6 +158,13 @@ class NotesListAdapter( } else { holder.tvDelete.gone() } + + holder.cbDelete.isChecked = note.selected + if (mActionMode) { + holder.cbDelete.visible() + } else { + holder.cbDelete.gone() + } } override fun getItemCount() = notes.size @@ -212,12 +236,45 @@ class NotesListAdapter( return notes } + fun removeNote(noteToRemove: Note) { + notes.remove(noteToRemove) + selectedNotesCount.postValue(notes.size) + } + + fun removeNotes(notesToRemove: List) { + notes.removeAll(notesToRemove) + } + fun setNotes(notes: List) { this.notes = notes.toMutableList() } - fun removeNote(noteToRemove: Note) { - notes.remove(noteToRemove) + fun toggleActionMode(pos: Int) { + mActionMode = !mActionMode + var selectedCount = 0 + notes.forEachIndexed { index, note -> + note.selected = mActionMode && index == pos + if (note.selected) { + selectedCount++ + selectedNotesForDelete.add(note) + } + } + selectedNotesCount.postValue(selectedCount) + notifyDataSetChanged() + } + + fun toggleSelectAllItems(selected: Boolean) { + notes.forEach { it.selected = selected } + selectedNotesForDelete.clear() + selectedNotesCount.postValue( + if (selected) { + selectedNotesForDelete.addAll(notes) + notes.size + } else { + 0 + }, + ) + notifyDataSetChanged() } inner class NoteViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { @@ -232,10 +289,17 @@ class NotesListAdapter( val tvPlayingPosition = binding.layoutAudioView.tvPlayingPosition val ivGraphicThumb = binding.ivGraphicsThumb val tvDelete = binding.tvDelete + val cbDelete = binding.cbDelete + var isSwiping: Boolean = false private val clickNoteToEditListener = View.OnClickListener { + if (mActionMode) { + checkedByItemClick = true + binding.cbDelete.toggle() + return@OnClickListener + } var tag = EditTextNotesFragment.TAG when (val selectedNote = notes[bindingAdapterPosition]) { is TextNote -> activity.fragment = EditTextNotesFragment.newInstance(selectedNote) @@ -248,11 +312,43 @@ class NotesListAdapter( tag = ArkRecorderFragment.TAG } } + onItemClicked?.invoke() activity.replaceFragment(activity.fragment, tag) } + private val noteCheckedListener = + CompoundButton.OnCheckedChangeListener { buttonView, isChecked -> + if (!buttonView.isPressed && !checkedByItemClick) return@OnCheckedChangeListener + checkedByItemClick = false + + val selectedNote = notes[bindingAdapterPosition] + selectedNote.selected = isChecked + if (isChecked) { + selectedNotesCount.value?.let { count -> + selectedNotesCount.postValue(count + 1) + } + selectedNotesForDelete.add(selectedNote) + } else { + selectedNotesCount.value?.let { count -> + selectedNotesCount.postValue(count - 1) + } + selectedNotesForDelete.remove(selectedNote) + } + + buttonView.post { + notifyItemChanged(bindingAdapterPosition) + } + } + init { binding.root.setOnClickListener(clickNoteToEditListener) + binding.root.setOnLongClickListener { + onItemLongPressed?.invoke(bindingAdapterPosition, notes[bindingAdapterPosition]) + true + } + binding.cbDelete.setOnCheckedChangeListener(noteCheckedListener) + binding.layoutAudioView.root.setBackgroundResource(R.drawable.bg_audio_view_note_item) + binding.viewItemContent.clipToOutline = true } } } diff --git a/app/src/main/java/dev/arkbuilders/arkmemo/ui/dialogs/CommonActionDialog.kt b/app/src/main/java/dev/arkbuilders/arkmemo/ui/dialogs/CommonActionDialog.kt index adb90cd2..06813c9c 100644 --- a/app/src/main/java/dev/arkbuilders/arkmemo/ui/dialogs/CommonActionDialog.kt +++ b/app/src/main/java/dev/arkbuilders/arkmemo/ui/dialogs/CommonActionDialog.kt @@ -14,8 +14,8 @@ import dev.arkbuilders.arkmemo.databinding.DialogCommonActionBinding * It's a basic dialog with customizable title, message, one positive button and one negative button */ class CommonActionDialog( - @StringRes private val title: Int, - @StringRes private val message: Int, + private val title: String, + private val message: String, @StringRes private val positiveText: Int, @StringRes private val negativeText: Int, private val isAlert: Boolean = false, @@ -48,8 +48,8 @@ class CommonActionDialog( mBinding.tvPositive.setBackgroundResource(R.drawable.bg_red_button) } - mBinding.tvTitle.setText(title) - mBinding.tvMessage.setText(message) + mBinding.tvTitle.text = title + mBinding.tvMessage.text = message mBinding.tvPositive.setText(positiveText) mBinding.tvNegative.setText(negativeText) mBinding.ivClose.setOnClickListener { diff --git a/app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/ArkRecorderFragment.kt b/app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/ArkRecorderFragment.kt index fc5e0d44..641d4874 100644 --- a/app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/ArkRecorderFragment.kt +++ b/app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/ArkRecorderFragment.kt @@ -227,15 +227,16 @@ class ArkRecorderFragment : BaseEditNoteFragment() { path = arkRecorderViewModel.getRecordingPath(), ) CommonActionDialog( - title = R.string.delete_note, - message = R.string.ark_memo_delete_warn, + title = getString(R.string.delete_note), + message = resources.getQuantityString(R.plurals.delete_batch_note_message, 1), positiveText = R.string.action_delete, negativeText = R.string.ark_memo_cancel, isAlert = true, onPositiveClick = { - notesViewModel.onDeleteConfirmed(note) {} - toast(requireContext(), getString(R.string.note_deleted)) - activity.onBackPressedDispatcher.onBackPressed() + notesViewModel.onDeleteConfirmed(listOf(note)) { + toast(requireContext(), getString(R.string.note_deleted)) + activity.onBackPressedDispatcher.onBackPressed() + } }, onNegativeClicked = { }, @@ -521,8 +522,8 @@ class ArkRecorderFragment : BaseEditNoteFragment() { ) ) { CommonActionDialog( - title = R.string.dialog_replace_recording_title, - message = R.string.dialog_replace_recording_message, + title = getString(R.string.dialog_replace_recording_title), + message = getString(R.string.dialog_replace_recording_message), positiveText = R.string.dialog_replace_recording_positive_text, negativeText = R.string.discard, onPositiveClick = { diff --git a/app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/BaseEditNoteFragment.kt b/app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/BaseEditNoteFragment.kt index 20f5f38f..f5842a0c 100644 --- a/app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/BaseEditNoteFragment.kt +++ b/app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/BaseEditNoteFragment.kt @@ -115,8 +115,8 @@ abstract class BaseEditNoteFragment : BaseFragment() { ) { val saveNoteDialog = CommonActionDialog( - title = R.string.dialog_save_note_title, - message = R.string.dialog_save_note_message, + title = getString(R.string.dialog_save_note_title), + message = getString(R.string.dialog_save_note_message), positiveText = R.string.save, negativeText = R.string.discard, isAlert = false, @@ -138,15 +138,16 @@ abstract class BaseEditNoteFragment : BaseFragment() { fun showDeleteNoteDialog(note: Note) { CommonActionDialog( - title = R.string.delete_note, - message = R.string.ark_memo_delete_warn, + title = getString(R.string.delete_note), + message = resources.getQuantityString(R.plurals.delete_batch_note_message, 1), positiveText = R.string.action_delete, negativeText = R.string.ark_memo_cancel, isAlert = true, onPositiveClick = { - notesViewModel.onDeleteConfirmed(note) {} - hostActivity.onBackPressedDispatcher.onBackPressed() - toast(requireContext(), getString(R.string.note_deleted)) + notesViewModel.onDeleteConfirmed(listOf(note)) { + hostActivity.onBackPressedDispatcher.onBackPressed() + toast(requireContext(), getString(R.string.note_deleted)) + } }, onNegativeClicked = { }, diff --git a/app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/EditTextNotesFragment.kt b/app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/EditTextNotesFragment.kt index 3c9b2641..fba1b95f 100644 --- a/app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/EditTextNotesFragment.kt +++ b/app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/EditTextNotesFragment.kt @@ -70,7 +70,7 @@ class EditTextNotesFragment : BaseEditNoteFragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) notesViewModel.init {} - observeSaveResult(notesViewModel.getSaveNoteResultLiveData()) + if (arguments != null) { requireArguments().getParcelableCompat(NOTE_KEY, TextNote::class.java)?.let { note = it @@ -155,6 +155,7 @@ class EditTextNotesFragment : BaseEditNoteFragment() { ?: binding.toolbar.ivRightActionIcon.gone() view.viewTreeObserver.addOnWindowFocusChangeListener(windowFocusedListener) + observeSaveResult(notesViewModel.getSaveNoteResultLiveData()) } override fun isContentChanged(): Boolean { diff --git a/app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/NotesFragment.kt b/app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/NotesFragment.kt index 53798acb..c69c1d98 100644 --- a/app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/NotesFragment.kt +++ b/app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/NotesFragment.kt @@ -52,6 +52,9 @@ class NotesFragment : BaseFragment() { private var playingAudioPosition = -1 private var lastNoteItemPosition = 0 + private var mIsActionMode = false + private var selectedCountForDelete = 0 + private val newTextNoteClickListener = View.OnClickListener { onFloatingActionButtonClicked() @@ -93,24 +96,23 @@ class NotesFragment : BaseFragment() { notesAdapter?.getNotes()?.getOrNull(deletePosition)?.apply { pendingForDelete = true } ?: return - val noteViewHolder = viewHolder as? NotesListAdapter.NoteViewHolder noteViewHolder?.isSwiping = true - binding.rvPinnedNotes.adapter?.notifyItemChanged(deletePosition) CommonActionDialog( - title = R.string.delete_note, - message = R.string.ark_memo_delete_warn, + title = getString(R.string.delete_note), + message = resources.getQuantityString(R.plurals.delete_batch_note_message, 1), positiveText = R.string.action_delete, negativeText = R.string.ark_memo_cancel, isAlert = true, onPositiveClick = { noteViewHolder?.isSwiping = false - notesViewModel.onDeleteConfirmed(noteToDelete) { + notesViewModel.onDeleteConfirmed(listOf(noteToDelete)) { notesAdapter?.removeNote(noteToDelete) toast(requireContext(), getString(R.string.note_deleted)) binding.rvPinnedNotes.adapter?.notifyItemRemoved(deletePosition) + checkForEmptyState() } }, onNegativeClicked = { @@ -228,6 +230,7 @@ class NotesFragment : BaseFragment() { arkMediaPlayerViewModel.onPlayOrPauseClick(path, pos, onStop) }, ) + observeSelectedNoteForDelete() } else { notesAdapter?.setNotes(notes) } @@ -236,6 +239,14 @@ class NotesFragment : BaseFragment() { observePlayerState() observePlayerSideEffect() notesAdapter?.setActivity(activity) + notesAdapter?.onItemLongPressed = { pos, note -> + toggleActionMode(pos = pos) + } + notesAdapter?.onItemClicked = { + if (mIsActionMode) { + toggleActionMode() + } + } binding.rvPinnedNotes.apply { this.layoutManager = layoutManager this.adapter = notesAdapter @@ -247,20 +258,23 @@ class NotesFragment : BaseFragment() { } } } - mItemTouchHelper = ItemTouchHelper(mItemTouchCallback) mItemTouchHelper?.attachToRecyclerView(binding.rvPinnedNotes) - if (notes.isNotEmpty()) { - binding.layoutBottomControl.visible() - binding.groupEmptyState.gone() - binding.rvPinnedNotes.visible() - binding.edtSearch.visible() - } else { + showEmptyState(isEmpty = notes.isEmpty()) + } + + private fun showEmptyState(isEmpty: Boolean) { + if (isEmpty) { binding.layoutBottomControl.gone() binding.groupEmptyState.visible() binding.rvPinnedNotes.gone() binding.edtSearch.gone() + } else { + binding.layoutBottomControl.visible() + binding.groupEmptyState.gone() + binding.rvPinnedNotes.visible() + binding.edtSearch.visible() } } @@ -298,7 +312,6 @@ class NotesFragment : BaseFragment() { arkMediaPlayerViewModel.playerSideEffect.collectLatest { sideEffect -> sideEffect ?: return@collectLatest notesAdapter?.observeItemSideEffect = { sideEffect } - if (sideEffect == ArkMediaPlayerSideEffect.StopPlaying) { mItemTouchHelper?.attachToRecyclerView(binding.rvPinnedNotes) } @@ -333,6 +346,9 @@ class NotesFragment : BaseFragment() { activity.fragment = this observeClipboardContent() binding.rvPinnedNotes.layoutManager?.scrollToPosition(lastNoteItemPosition) + if (notesAdapter?.observableSelectedNotesCount?.hasActiveObservers() == false) { + observeSelectedNoteForDelete() + } } private fun createTextNote() { @@ -414,16 +430,115 @@ class NotesFragment : BaseFragment() { } } + private fun toggleActionMode(pos: Int = -1) { + if (mIsActionMode) { + binding.groupActionModeTexts.gone() + binding.layoutBottomControl.visible() + binding.edtSearch.visible() + binding.ivSettings.visible() + notesAdapter?.toggleSelectAllItems(false) + } else { + binding.groupActionModeTexts.visible() + updateSelectStateTexts(selectedCountForDelete) + binding.layoutBottomControl.gone() + binding.edtSearch.gone() + binding.ivSettings.gone() + binding.tvActionModeCancel.setOnClickListener { + toggleActionMode() + } + binding.tvActionModeSelectAll.setOnClickListener { + notesAdapter?.toggleSelectAllItems( + selected = selectedCountForDelete != notesAdapter?.getNotes()?.size, + ) + updateSelectStateTexts(selectedCountForDelete) + } + binding.btnDelete.setOnClickListener { + showBatchDeletionDialog() + } + } + (binding.rvPinnedNotes.adapter as? NotesListAdapter)?.toggleActionMode(pos) + mIsActionMode = !mIsActionMode + } + + private fun showBatchDeletionDialog() { + CommonActionDialog( + title = + resources.getQuantityString( + R.plurals.delete_note_count, + selectedCountForDelete, + selectedCountForDelete, + ), + message = resources.getQuantityString(R.plurals.delete_batch_note_message, selectedCountForDelete), + positiveText = R.string.action_delete, + negativeText = R.string.ark_memo_cancel, + isAlert = true, + onPositiveClick = { + binding.pbLoading.visible() + val selectedNotes = notesAdapter?.selectedNotesForDelete ?: emptyList() + notesViewModel.onDeleteConfirmed(selectedNotes) { + notesAdapter?.removeNotes(selectedNotes) + binding.pbLoading.gone() + toast(requireContext(), getString(R.string.note_deleted)) + binding.rvPinnedNotes.adapter?.notifyDataSetChanged() + toggleActionMode() + checkForEmptyState() + } + }, + onNegativeClicked = {}, + onCloseClicked = {}, + ).show(childFragmentManager, CommonActionDialog.TAG) + } + + private fun updateSelectStateTexts(selectedCount: Int) { + binding.tvSelectedNoteCount.text = + resources.getQuantityString( + R.plurals.selected_note_count, selectedCount, selectedCount, + ) + binding.tvActionModeSelectAll.text = + if (selectedCount == (notesAdapter?.getNotes()?.size ?: 0)) { + getString(R.string.deselect_all) + } else { + getString(R.string.select_all) + } + } + + private fun changeDeleteButtonState(enabled: Boolean) { + if (enabled) { + binding.btnDelete.isClickable = true + binding.btnDelete.alpha = 1f + } else { + binding.btnDelete.isClickable = false + binding.btnDelete.alpha = 0.4f + } + } + + private fun observeSelectedNoteForDelete() { + notesAdapter?.observableSelectedNotesCount?.observe(viewLifecycleOwner) { count -> + selectedCountForDelete = count + updateSelectStateTexts(count) + + changeDeleteButtonState(enabled = count > 0) + } + } + override fun onBackPressed() { if (binding.edtSearch.isFocused) { binding.edtSearch.text.clear() binding.edtSearch.clearFocus() binding.rvPinnedNotes.layoutManager?.scrollToPosition(lastNoteItemPosition) + } else if (mIsActionMode) { + toggleActionMode() } else { activity.onBackPressedDispatcher.onBackPressed() } } + private fun checkForEmptyState() { + if (notesAdapter?.getNotes()?.isEmpty() == true) { + showEmptyState(true) + } + } + companion object { const val TAG = "NotesFragment" } diff --git a/app/src/main/java/dev/arkbuilders/arkmemo/ui/viewmodels/NotesViewModel.kt b/app/src/main/java/dev/arkbuilders/arkmemo/ui/viewmodels/NotesViewModel.kt index 6b59f99e..57374c40 100644 --- a/app/src/main/java/dev/arkbuilders/arkmemo/ui/viewmodels/NotesViewModel.kt +++ b/app/src/main/java/dev/arkbuilders/arkmemo/ui/viewmodels/NotesViewModel.kt @@ -57,7 +57,8 @@ class NotesViewModel notes.value = textNotesRepo.read() + graphicNotesRepo.read() + voiceNotesRepo.read() notes.value.let { withContext(Dispatchers.Main) { - onSuccess(it.sortedByDescending { note -> note.resource?.modified }) + notes.value = it.sortedByDescending { note -> note.resource?.modified } + onSuccess(notes.value) } } } @@ -104,7 +105,7 @@ class NotesViewModel result == SaveNoteResult.SUCCESS_UPDATED ) { if (result == SaveNoteResult.SUCCESS_NEW) { - parentNote?.let { onDeleteConfirmed(parentNote) {} } + parentNote?.let { onDeleteConfirmed(listOf(parentNote)) {} } } add(note, noteResId) } @@ -134,19 +135,20 @@ class NotesViewModel } fun onDeleteConfirmed( - note: Note, + notes: List, onSuccess: () -> Unit, ) { viewModelScope.launch(iODispatcher) { - when (note) { - is TextNote -> textNotesRepo.delete(note) - is GraphicNote -> graphicNotesRepo.delete(note) - is VoiceNote -> voiceNotesRepo.delete(note) + notes.forEach { note -> + when (note) { + is TextNote -> textNotesRepo.delete(note) + is GraphicNote -> graphicNotesRepo.delete(note) + is VoiceNote -> voiceNotesRepo.delete(note) + } } - this@NotesViewModel.notes.value = this@NotesViewModel.notes.value.toMutableList() - .apply { remove(note) } + .apply { removeAll(notes) } withContext(Dispatchers.Main) { onSuccess.invoke() } diff --git a/app/src/main/res/drawable/bg_audio_view_note_item.xml b/app/src/main/res/drawable/bg_audio_view_note_item.xml new file mode 100644 index 00000000..739f49e8 --- /dev/null +++ b/app/src/main/res/drawable/bg_audio_view_note_item.xml @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_delete_tag.xml b/app/src/main/res/drawable/bg_delete_tag.xml new file mode 100644 index 00000000..8f74f8aa --- /dev/null +++ b/app/src/main/res/drawable/bg_delete_tag.xml @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ripple_note_item.xml b/app/src/main/res/drawable/ripple_note_item.xml new file mode 100644 index 00000000..4aa0a653 --- /dev/null +++ b/app/src/main/res/drawable/ripple_note_item.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/font/font.xml b/app/src/main/res/font/font.xml new file mode 100644 index 00000000..c06f89eb --- /dev/null +++ b/app/src/main/res/font/font.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/adapter_text_note.xml b/app/src/main/res/layout/adapter_text_note.xml index 6d05f9a2..93e90b81 100644 --- a/app/src/main/res/layout/adapter_text_note.xml +++ b/app/src/main/res/layout/adapter_text_note.xml @@ -4,105 +4,124 @@ android:layout_height="wrap_content" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" - android:background="@drawable/bg_big_radius" android:clickable="true" - android:layout_marginBottom="@dimen/note_item_padding" - android:foreground="?attr/selectableItemBackgroundBorderless"> + android:layout_marginBottom="@dimen/note_item_padding"> - - - + app:layout_constraintBottom_toBottomOf="parent" + android:buttonTint="@color/yellow_700" + android:visibility="gone"/> - - - + android:background="@drawable/bg_big_radius" + android:id="@+id/view_item_content" + android:foreground="?attr/selectableItemBackgroundBorderless" + app:layout_constraintBottom_toBottomOf="parent"> - + - + - + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_common_action.xml b/app/src/main/res/layout/dialog_common_action.xml index 10195b12..045ec13e 100644 --- a/app/src/main/res/layout/dialog_common_action.xml +++ b/app/src/main/res/layout/dialog_common_action.xml @@ -3,6 +3,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" android:layout_gravity="center" android:fitsSystemWindows="true"> @@ -38,7 +39,7 @@ app:layout_constraintStart_toStartOf="@+id/tv_title" app:layout_constraintTop_toBottomOf="@+id/iv_close" app:layout_constraintEnd_toEndOf="@+id/iv_close" - android:text="@string/ark_memo_delete_warn" + tools:text="Are you sure you want to delete this note? This action cannot be undone." android:id="@+id/tv_message"/> + xmlns:tools="http://schemas.android.com/tools" + android:animateLayoutChanges="true"> + android:layout_marginStart="@dimen/home_horizontal_margin" /> + + + + + + + + + + + + + + + + + app:layout_constraintTop_toBottomOf="@+id/barrier_top_note_list"/> + android:layout_height="wrap_content" + xmlns:tools="http://schemas.android.com/tools"> + + + + #FFFAEB #FEF0C7 #FEC84B + #DC6803 #B54708 #DC6803 #344054 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f9c4cbd7..e68d460b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -27,7 +27,6 @@ Nothing to paste OK Cancel - Are you sure you want to delete this note? This action cannot be undone. Note already exists Note saved successfully Title @@ -104,4 +103,19 @@ Please allow the permissions to record the audio Play/Pause + + %d Item selected + %d Items selected + + + Delete %d note + Delete %d notes + + + Are you sure you want to delete this note? This action cannot be undone. + Are you sure you want to delete these notes? This action cannot be undone. + + Select All + Deselect All + \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index a9c3e1ff..289f7ec8 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -1,5 +1,5 @@ - + + +