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
This commit is contained in:
OxygenCobalt 2022-07-30 10:06:58 -06:00
parent 7467d89a45
commit 54be8dc2dc
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
4 changed files with 111 additions and 42 deletions

View file

@ -25,15 +25,15 @@ import android.view.View
import androidx.core.view.isInvisible import androidx.core.view.isInvisible
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.shape.MaterialShapeDrawable import com.google.android.material.shape.MaterialShapeDrawable
import java.util.*
import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.IntegerTable
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.ItemQueueSongBinding import org.oxycblt.auxio.databinding.ItemQueueSongBinding
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.ui.recycler.* import org.oxycblt.auxio.ui.recycler.*
import org.oxycblt.auxio.util.* import org.oxycblt.auxio.util.*
class QueueAdapter(listener: QueueItemListener) : class QueueAdapter(listener: QueueItemListener) :
MonoAdapter<Song, QueueItemListener, QueueSongViewHolder>(listener) { MonoAdapter<QueueViewModel.QueueSong, QueueItemListener, QueueSongViewHolder>(listener) {
override val data = SyncBackingData(this, QueueSongViewHolder.DIFFER) override val data = SyncBackingData(this, QueueSongViewHolder.DIFFER)
override val creator = QueueSongViewHolder.CREATOR override val creator = QueueSongViewHolder.CREATOR
} }
@ -46,7 +46,7 @@ interface QueueItemListener {
class QueueSongViewHolder class QueueSongViewHolder
private constructor( private constructor(
private val binding: ItemQueueSongBinding, private val binding: ItemQueueSongBinding,
) : BindingViewHolder<Song, QueueItemListener>(binding.root) { ) : BindingViewHolder<QueueViewModel.QueueSong, QueueItemListener>(binding.root) {
val bodyView: View val bodyView: View
get() = binding.body get() = binding.body
val backgroundView: View val backgroundView: View
@ -58,6 +58,9 @@ private constructor(
elevation = binding.context.getDimenSafe(R.dimen.elevation_normal) * 5 elevation = binding.context.getDimenSafe(R.dimen.elevation_normal) * 5
} }
val isEnabled: Boolean
get() = binding.songDragHandle.isEnabled
init { init {
binding.body.background = binding.body.background =
LayerDrawable( LayerDrawable(
@ -72,10 +75,10 @@ private constructor(
} }
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
override fun bind(item: Song, listener: QueueItemListener) { override fun bind(item: QueueViewModel.QueueSong, listener: QueueItemListener) {
binding.songAlbumCover.bind(item) binding.songAlbumCover.bind(item.song)
binding.songName.textSafe = item.resolveName(binding.context) binding.songName.textSafe = item.song.resolveName(binding.context)
binding.songInfo.textSafe = item.resolveIndividualArtistName(binding.context) binding.songInfo.textSafe = item.song.resolveIndividualArtistName(binding.context)
binding.background.isInvisible = true binding.background.isInvisible = true
@ -84,6 +87,18 @@ private constructor(
binding.body.setOnClickListener { listener.onClick(this) } 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 // Roll our own drag handlers as the default ones suck
binding.songDragHandle.setOnTouchListener { _, motionEvent -> binding.songDragHandle.setOnTouchListener { _, motionEvent ->
binding.songDragHandle.performClick() binding.songDragHandle.performClick()
@ -104,6 +119,19 @@ private constructor(
QueueSongViewHolder(ItemQueueSongBinding.inflate(context.inflater)) QueueSongViewHolder(ItemQueueSongBinding.inflate(context.inflater))
} }
val DIFFER = SongViewHolder.DIFFER val DIFFER =
object : SimpleItemCallback<QueueViewModel.QueueSong>() {
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
}
} }
} }

View file

@ -42,9 +42,16 @@ class QueueDragCallback(private val playbackModel: QueueViewModel) : ItemTouchHe
override fun getMovementFlags( override fun getMovementFlags(
recyclerView: RecyclerView, recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder viewHolder: RecyclerView.ViewHolder
): Int = ): Int {
makeFlag(ItemTouchHelper.ACTION_STATE_DRAG, ItemTouchHelper.UP or ItemTouchHelper.DOWN) or val queueHolder = viewHolder as QueueSongViewHolder
makeFlag(ItemTouchHelper.ACTION_STATE_SWIPE, ItemTouchHelper.START) 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( override fun interpolateOutOfBoundsScroll(
recyclerView: RecyclerView, recyclerView: RecyclerView,

View file

@ -22,7 +22,9 @@ import android.view.LayoutInflater
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import java.util.*
import org.oxycblt.auxio.databinding.FragmentQueueBinding import org.oxycblt.auxio.databinding.FragmentQueueBinding
import org.oxycblt.auxio.ui.fragment.ViewBindingFragment import org.oxycblt.auxio.ui.fragment.ViewBindingFragment
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately
@ -64,13 +66,28 @@ class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), QueueItemList
touchHelper.startDrag(viewHolder) touchHelper.startDrag(viewHolder)
} }
private fun updateQueue(queue: QueueViewModel.QueueData) { private fun updateQueue(queue: List<QueueViewModel.QueueSong>) {
if (queue.nonTrivial) { val instructions = queueModel.instructions
// nonTrivial implies that using a synced submitList would be slow, replace the list if (instructions != null) {
// instead. if (instructions.replace) {
queueAdapter.data.replaceList(queue.queue) 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 { } else {
queueAdapter.data.submitList(queue.queue) queueAdapter.data.submitList(queue)
} }
} }
} }

View file

@ -23,15 +23,21 @@ import kotlinx.coroutines.flow.StateFlow
import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.state.PlaybackStateManager 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 { class QueueViewModel : ViewModel(), PlaybackStateManager.Callback {
private val playbackManager = PlaybackStateManager.getInstance() private val playbackManager = PlaybackStateManager.getInstance()
data class QueueData(val queue: List<Song>, 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)) private val _queue = MutableStateFlow(listOf<QueueSong>())
val queue: StateFlow<QueueData> = _queue val queue: StateFlow<List<QueueSong>> = _queue
data class QueueInstructions(val replace: Boolean, val scrollTo: Int?)
var instructions: QueueInstructions? = null
init { init {
playbackManager.addCallback(this) 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. * Go to an item in the queue using it's recyclerview adapter index. No-ops if out of bounds.
*/ */
fun goto(adapterIndex: Int) { fun goto(adapterIndex: Int) {
val index = adapterIndex + (playbackManager.queue.size - _queue.value.queue.size) if (adapterIndex !in playbackManager.queue.indices) {
logD(adapterIndex) return
logD(playbackManager.queue.size - _queue.value.queue.size)
if (index in playbackManager.queue.indices) {
playbackManager.goto(index)
} }
playbackManager.goto(adapterIndex)
} }
/** Remove a queue item using it's recyclerview adapter index. */ /** Remove a queue item using it's recyclerview adapter index. */
fun removeQueueDataItem(adapterIndex: Int) { fun removeQueueDataItem(adapterIndex: Int) {
val index = adapterIndex + (playbackManager.queue.size - _queue.value.queue.size) if (adapterIndex <= playbackManager.index ||
if (index in playbackManager.queue.indices) { adapterIndex !in playbackManager.queue.indices) {
playbackManager.removeQueueItem(index) return
} }
playbackManager.removeQueueItem(adapterIndex)
} }
/** Move queue items using their recyclerview adapter indices. */ /** Move queue items using their recyclerview adapter indices. */
fun moveQueueDataItems(adapterFrom: Int, adapterTo: Int): Boolean { fun moveQueueDataItems(adapterFrom: Int, adapterTo: Int): Boolean {
val delta = (playbackManager.queue.size - _queue.value.queue.size) if (adapterFrom <= playbackManager.index || adapterTo <= playbackManager.index) {
val from = adapterFrom + delta return false
val to = adapterTo + delta
if (from in playbackManager.queue.indices && to in playbackManager.queue.indices) {
playbackManager.moveQueueItem(from, to)
return true
} }
playbackManager.moveQueueItem(adapterFrom, adapterTo)
return false return false
} }
fun finishInstructions() {
instructions = null
}
override fun onIndexMoved(index: Int) { 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<Song>) { override fun onQueueChanged(queue: List<Song>) {
_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<Song>) { override fun onQueueReworked(index: Int, queue: List<Song>) {
_queue.value = QueueData(generateQueue(index, queue), true) instructions = QueueInstructions(true, index + 1)
_queue.value = generateQueue(index, queue)
} }
override fun onNewPlayback(index: Int, queue: List<Song>, parent: MusicParent?) { override fun onNewPlayback(index: Int, queue: List<Song>, 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<Song>) = private fun generateQueue(index: Int, queue: List<Song>): List<QueueSong> {
queue.slice(index + 1..playbackManager.queue.lastIndex) 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() { override fun onCleared() {
super.onCleared() super.onCleared()