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.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<Song, QueueItemListener, QueueSongViewHolder>(listener) {
MonoAdapter<QueueViewModel.QueueSong, QueueItemListener, QueueSongViewHolder>(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<Song, QueueItemListener>(binding.root) {
) : BindingViewHolder<QueueViewModel.QueueSong, QueueItemListener>(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<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(
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,

View file

@ -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<FragmentQueueBinding>(), 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<QueueViewModel.QueueSong>) {
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)
}
}
}

View file

@ -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<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))
val queue: StateFlow<QueueData> = _queue
private val _queue = MutableStateFlow(listOf<QueueSong>())
val queue: StateFlow<List<QueueSong>> = _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<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>) {
_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?) {
_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>) =
queue.slice(index + 1..playbackManager.queue.lastIndex)
private fun generateQueue(index: Int, queue: List<Song>): List<QueueSong> {
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()