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.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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
Loading…
Reference in a new issue