From 54be8dc2dc944506f1b64895199de6fd723badda Mon Sep 17 00:00:00 2001 From: OxygenCobalt Date: Sat, 30 Jul 2022 10:06:58 -0600 Subject: [PATCH] queue: add ability to see previous items Add the ability to see (but not edit) previous items. This completes the new playback UI I've been working on for about 2 weeks now. I pray that there is no insane unfixable bug with this, please please please please please --- .../auxio/playback/queue/QueueAdapter.kt | 44 +++++++++--- .../auxio/playback/queue/QueueDragCallback.kt | 13 +++- .../auxio/playback/queue/QueueFragment.kt | 29 ++++++-- .../auxio/playback/queue/QueueViewModel.kt | 67 ++++++++++++------- 4 files changed, 111 insertions(+), 42 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt index 6f261b688..d0ca7aabb 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt @@ -25,15 +25,15 @@ import android.view.View import androidx.core.view.isInvisible import androidx.recyclerview.widget.RecyclerView import com.google.android.material.shape.MaterialShapeDrawable +import java.util.* import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.ItemQueueSongBinding -import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.ui.recycler.* import org.oxycblt.auxio.util.* class QueueAdapter(listener: QueueItemListener) : - MonoAdapter(listener) { + MonoAdapter(listener) { override val data = SyncBackingData(this, QueueSongViewHolder.DIFFER) override val creator = QueueSongViewHolder.CREATOR } @@ -46,7 +46,7 @@ interface QueueItemListener { class QueueSongViewHolder private constructor( private val binding: ItemQueueSongBinding, -) : BindingViewHolder(binding.root) { +) : BindingViewHolder(binding.root) { val bodyView: View get() = binding.body val backgroundView: View @@ -58,6 +58,9 @@ private constructor( elevation = binding.context.getDimenSafe(R.dimen.elevation_normal) * 5 } + val isEnabled: Boolean + get() = binding.songDragHandle.isEnabled + init { binding.body.background = LayerDrawable( @@ -72,10 +75,10 @@ private constructor( } @SuppressLint("ClickableViewAccessibility") - override fun bind(item: Song, listener: QueueItemListener) { - binding.songAlbumCover.bind(item) - binding.songName.textSafe = item.resolveName(binding.context) - binding.songInfo.textSafe = item.resolveIndividualArtistName(binding.context) + override fun bind(item: QueueViewModel.QueueSong, listener: QueueItemListener) { + binding.songAlbumCover.bind(item.song) + binding.songName.textSafe = item.song.resolveName(binding.context) + binding.songInfo.textSafe = item.song.resolveIndividualArtistName(binding.context) binding.background.isInvisible = true @@ -84,6 +87,18 @@ private constructor( binding.body.setOnClickListener { listener.onClick(this) } + if (item.previous) { + binding.songName.alpha = 0.5f + binding.songInfo.alpha = 0.5f + binding.songAlbumCover.alpha = 0.5f + binding.songDragHandle.isEnabled = false + } else { + binding.songName.alpha = 1f + binding.songInfo.alpha = 1f + binding.songAlbumCover.alpha = 1f + binding.songDragHandle.isEnabled = true + } + // Roll our own drag handlers as the default ones suck binding.songDragHandle.setOnTouchListener { _, motionEvent -> binding.songDragHandle.performClick() @@ -104,6 +119,19 @@ private constructor( QueueSongViewHolder(ItemQueueSongBinding.inflate(context.inflater)) } - val DIFFER = SongViewHolder.DIFFER + val DIFFER = + object : SimpleItemCallback() { + override fun areContentsTheSame( + oldItem: QueueViewModel.QueueSong, + newItem: QueueViewModel.QueueSong + ) = + super.areContentsTheSame(oldItem, newItem) && + oldItem.previous == newItem.previous + + override fun areItemsTheSame( + oldItem: QueueViewModel.QueueSong, + newItem: QueueViewModel.QueueSong + ) = oldItem.song == newItem.song && oldItem.previous == newItem.previous + } } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueDragCallback.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueDragCallback.kt index 816679e04..442ce1064 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueDragCallback.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueDragCallback.kt @@ -42,9 +42,16 @@ class QueueDragCallback(private val playbackModel: QueueViewModel) : ItemTouchHe override fun getMovementFlags( recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder - ): Int = - makeFlag(ItemTouchHelper.ACTION_STATE_DRAG, ItemTouchHelper.UP or ItemTouchHelper.DOWN) or - makeFlag(ItemTouchHelper.ACTION_STATE_SWIPE, ItemTouchHelper.START) + ): Int { + val queueHolder = viewHolder as QueueSongViewHolder + return if (queueHolder.isEnabled) { + makeFlag( + ItemTouchHelper.ACTION_STATE_DRAG, ItemTouchHelper.UP or ItemTouchHelper.DOWN) or + makeFlag(ItemTouchHelper.ACTION_STATE_SWIPE, ItemTouchHelper.START) + } else { + 0 + } + } override fun interpolateOutOfBoundsScroll( recyclerView: RecyclerView, diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt index a00b90bb2..38d078b05 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt @@ -22,7 +22,9 @@ import android.view.LayoutInflater import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import java.util.* import org.oxycblt.auxio.databinding.FragmentQueueBinding import org.oxycblt.auxio.ui.fragment.ViewBindingFragment import org.oxycblt.auxio.util.collectImmediately @@ -64,13 +66,28 @@ class QueueFragment : ViewBindingFragment(), QueueItemList touchHelper.startDrag(viewHolder) } - private fun updateQueue(queue: QueueViewModel.QueueData) { - if (queue.nonTrivial) { - // nonTrivial implies that using a synced submitList would be slow, replace the list - // instead. - queueAdapter.data.replaceList(queue.queue) + private fun updateQueue(queue: List) { + val instructions = queueModel.instructions + if (instructions != null) { + if (instructions.replace) { + queueAdapter.data.replaceList(queue) + } else { + queueAdapter.data.submitList(queue) + } + + if (instructions.scrollTo != null) { + val binding = requireBinding() + val lmm = binding.queueRecycler.layoutManager as LinearLayoutManager + val indices = + lmm.findFirstCompletelyVisibleItemPosition()..lmm + .findLastCompletelyVisibleItemPosition() + + if (instructions.scrollTo !in indices) { + requireBinding().queueRecycler.scrollToPosition(instructions.scrollTo) + } + } } else { - queueAdapter.data.submitList(queue.queue) + queueAdapter.data.submitList(queue) } } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueViewModel.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueViewModel.kt index 0f09405eb..c2da5ba27 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueViewModel.kt @@ -23,15 +23,21 @@ import kotlinx.coroutines.flow.StateFlow import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.state.PlaybackStateManager -import org.oxycblt.auxio.util.logD +import org.oxycblt.auxio.ui.recycler.Item class QueueViewModel : ViewModel(), PlaybackStateManager.Callback { private val playbackManager = PlaybackStateManager.getInstance() - data class QueueData(val queue: List, val nonTrivial: Boolean) + data class QueueSong(val song: Song, val previous: Boolean) : Item() { + override val id: Long + get() = song.id + } - private val _queue = MutableStateFlow(QueueData(listOf(), false)) - val queue: StateFlow = _queue + private val _queue = MutableStateFlow(listOf()) + val queue: StateFlow> = _queue + + data class QueueInstructions(val replace: Boolean, val scrollTo: Int?) + var instructions: QueueInstructions? = null init { playbackManager.addCallback(this) @@ -41,53 +47,64 @@ class QueueViewModel : ViewModel(), PlaybackStateManager.Callback { * Go to an item in the queue using it's recyclerview adapter index. No-ops if out of bounds. */ fun goto(adapterIndex: Int) { - val index = adapterIndex + (playbackManager.queue.size - _queue.value.queue.size) - logD(adapterIndex) - logD(playbackManager.queue.size - _queue.value.queue.size) - - if (index in playbackManager.queue.indices) { - playbackManager.goto(index) + if (adapterIndex !in playbackManager.queue.indices) { + return } + + playbackManager.goto(adapterIndex) } /** Remove a queue item using it's recyclerview adapter index. */ fun removeQueueDataItem(adapterIndex: Int) { - val index = adapterIndex + (playbackManager.queue.size - _queue.value.queue.size) - if (index in playbackManager.queue.indices) { - playbackManager.removeQueueItem(index) + if (adapterIndex <= playbackManager.index || + adapterIndex !in playbackManager.queue.indices) { + return } + + playbackManager.removeQueueItem(adapterIndex) } + /** Move queue items using their recyclerview adapter indices. */ fun moveQueueDataItems(adapterFrom: Int, adapterTo: Int): Boolean { - val delta = (playbackManager.queue.size - _queue.value.queue.size) - val from = adapterFrom + delta - val to = adapterTo + delta - if (from in playbackManager.queue.indices && to in playbackManager.queue.indices) { - playbackManager.moveQueueItem(from, to) - return true + if (adapterFrom <= playbackManager.index || adapterTo <= playbackManager.index) { + return false } + playbackManager.moveQueueItem(adapterFrom, adapterTo) + return false } + fun finishInstructions() { + instructions = null + } + override fun onIndexMoved(index: Int) { - _queue.value = QueueData(generateQueue(index, playbackManager.queue), false) + instructions = QueueInstructions(false, index + 1) + _queue.value = generateQueue(index, playbackManager.queue) } override fun onQueueChanged(queue: List) { - _queue.value = QueueData(generateQueue(playbackManager.index, queue), false) + instructions = QueueInstructions(false, null) + _queue.value = generateQueue(playbackManager.index, queue) } override fun onQueueReworked(index: Int, queue: List) { - _queue.value = QueueData(generateQueue(index, queue), true) + instructions = QueueInstructions(true, index + 1) + _queue.value = generateQueue(index, queue) } override fun onNewPlayback(index: Int, queue: List, parent: MusicParent?) { - _queue.value = QueueData(generateQueue(index, queue), true) + instructions = QueueInstructions(true, index + 1) + _queue.value = generateQueue(index, queue) } - private fun generateQueue(index: Int, queue: List) = - queue.slice(index + 1..playbackManager.queue.lastIndex) + private fun generateQueue(index: Int, queue: List): List { + val before = queue.slice(0..index).map { QueueSong(it, true) } + val after = + queue.slice(index + 1..playbackManager.queue.lastIndex).map { QueueSong(it, false) } + return before + after + } override fun onCleared() { super.onCleared()