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:
parent
7467d89a45
commit
54be8dc2dc
4 changed files with 111 additions and 42 deletions
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in a new issue