queue: add ability to play songs [#92]
Add the ability to jump to arbitrary points in the queue. This comes at the cost of the long-press option to move items, since they simply cannot co-exist without visual issues.
This commit is contained in:
parent
eca385aea5
commit
affa8c1c11
8 changed files with 58 additions and 38 deletions
|
@ -12,6 +12,7 @@ at the cost of longer loading times
|
||||||
- Added Last Added sorting
|
- Added Last Added sorting
|
||||||
- Search now takes sort tags and file names in account [#184]
|
- Search now takes sort tags and file names in account [#184]
|
||||||
- Added option to clear playback state in settings
|
- Added option to clear playback state in settings
|
||||||
|
- Added ability to play songs from queue
|
||||||
|
|
||||||
#### What's Improved
|
#### What's Improved
|
||||||
- App now exposes an (immutable) queue.
|
- App now exposes an (immutable) queue.
|
||||||
|
|
|
@ -72,6 +72,11 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
|
||||||
indexingNotification = IndexingNotification(this)
|
indexingNotification = IndexingNotification(this)
|
||||||
observingNotification = ObservingNotification(this)
|
observingNotification = ObservingNotification(this)
|
||||||
|
|
||||||
|
wakeLock =
|
||||||
|
getSystemServiceSafe(PowerManager::class)
|
||||||
|
.newWakeLock(
|
||||||
|
PowerManager.PARTIAL_WAKE_LOCK, BuildConfig.APPLICATION_ID + ".IndexerService")
|
||||||
|
|
||||||
settings = Settings(this, this)
|
settings = Settings(this, this)
|
||||||
indexerContentObserver = SystemContentObserver()
|
indexerContentObserver = SystemContentObserver()
|
||||||
|
|
||||||
|
@ -81,11 +86,6 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
|
||||||
onStartIndexing()
|
onStartIndexing()
|
||||||
}
|
}
|
||||||
|
|
||||||
wakeLock =
|
|
||||||
getSystemServiceSafe(PowerManager::class)
|
|
||||||
.newWakeLock(
|
|
||||||
PowerManager.PARTIAL_WAKE_LOCK, BuildConfig.APPLICATION_ID + ".IndexerService")
|
|
||||||
|
|
||||||
logD("Service created.")
|
logD("Service created.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -36,6 +36,7 @@ import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||||
import org.oxycblt.auxio.playback.state.RepeatMode
|
import org.oxycblt.auxio.playback.state.RepeatMode
|
||||||
import org.oxycblt.auxio.settings.Settings
|
import org.oxycblt.auxio.settings.Settings
|
||||||
import org.oxycblt.auxio.util.application
|
import org.oxycblt.auxio.util.application
|
||||||
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.logE
|
import org.oxycblt.auxio.util.logE
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -195,6 +196,19 @@ class PlaybackViewModel(application: Application) :
|
||||||
playbackManager.prev()
|
playbackManager.prev()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 - _nextUp.value.size)
|
||||||
|
logD(adapterIndex)
|
||||||
|
logD(playbackManager.queue.size - _nextUp.value.size)
|
||||||
|
|
||||||
|
if (index in playbackManager.queue.indices) {
|
||||||
|
playbackManager.goto(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** 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 - _nextUp.value.size)
|
val index = adapterIndex + (playbackManager.queue.size - _nextUp.value.size)
|
||||||
|
|
|
@ -19,32 +19,26 @@ package org.oxycblt.auxio.playback.queue
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.drawable.ColorDrawable
|
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.View
|
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 org.oxycblt.auxio.IntegerTable
|
import org.oxycblt.auxio.IntegerTable
|
||||||
|
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.music.Song
|
||||||
import org.oxycblt.auxio.ui.recycler.BindingViewHolder
|
import org.oxycblt.auxio.ui.recycler.*
|
||||||
import org.oxycblt.auxio.ui.recycler.MonoAdapter
|
import org.oxycblt.auxio.util.*
|
||||||
import org.oxycblt.auxio.ui.recycler.SongViewHolder
|
|
||||||
import org.oxycblt.auxio.ui.recycler.SyncBackingData
|
|
||||||
import org.oxycblt.auxio.util.context
|
|
||||||
import org.oxycblt.auxio.util.disableDropShadowCompat
|
|
||||||
import org.oxycblt.auxio.util.inflater
|
|
||||||
import org.oxycblt.auxio.util.stateList
|
|
||||||
import org.oxycblt.auxio.util.textSafe
|
|
||||||
|
|
||||||
class QueueAdapter(listener: QueueItemListener) :
|
class QueueAdapter(private val listener: QueueItemListener) :
|
||||||
MonoAdapter<Song, QueueItemListener, QueueSongViewHolder>(listener) {
|
MonoAdapter<Song, 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
|
||||||
}
|
}
|
||||||
|
|
||||||
interface QueueItemListener {
|
interface QueueItemListener {
|
||||||
|
fun onClick(viewHolder: RecyclerView.ViewHolder)
|
||||||
fun onPickUp(viewHolder: RecyclerView.ViewHolder)
|
fun onPickUp(viewHolder: RecyclerView.ViewHolder)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,13 +51,13 @@ private constructor(
|
||||||
val backgroundView: View
|
val backgroundView: View
|
||||||
get() = binding.background
|
get() = binding.background
|
||||||
|
|
||||||
init {
|
val backgroundDrawable =
|
||||||
binding.body.background =
|
MaterialShapeDrawable.createWithElevationOverlay(binding.root.context).apply {
|
||||||
MaterialShapeDrawable.createWithElevationOverlay(binding.root.context).apply {
|
fillColor = binding.context.getAttrColorSafe(R.attr.colorSurface).stateList
|
||||||
fillColor = (binding.body.background as ColorDrawable).color.stateList
|
}
|
||||||
}
|
|
||||||
|
|
||||||
binding.root.disableDropShadowCompat()
|
init {
|
||||||
|
binding.body.background = backgroundDrawable
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
|
@ -77,6 +71,8 @@ private constructor(
|
||||||
binding.songName.requestLayout()
|
binding.songName.requestLayout()
|
||||||
binding.songInfo.requestLayout()
|
binding.songInfo.requestLayout()
|
||||||
|
|
||||||
|
binding.body.setOnClickListener { listener.onClick(this) }
|
||||||
|
|
||||||
// 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()
|
||||||
|
@ -85,11 +81,6 @@ private constructor(
|
||||||
true
|
true
|
||||||
} else false
|
} else false
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.body.setOnLongClickListener {
|
|
||||||
listener.onPickUp(this)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -22,7 +22,6 @@ import android.view.animation.AccelerateDecelerateInterpolator
|
||||||
import androidx.core.view.isInvisible
|
import androidx.core.view.isInvisible
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.google.android.material.shape.MaterialShapeDrawable
|
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
@ -87,7 +86,7 @@ class QueueDragCallback(private val playbackModel: PlaybackViewModel) : ItemTouc
|
||||||
if (shouldLift && isCurrentlyActive && actionState == ItemTouchHelper.ACTION_STATE_DRAG) {
|
if (shouldLift && isCurrentlyActive && actionState == ItemTouchHelper.ACTION_STATE_DRAG) {
|
||||||
logD("Lifting queue item")
|
logD("Lifting queue item")
|
||||||
|
|
||||||
val bg = holder.bodyView.background as MaterialShapeDrawable
|
val bg = holder.backgroundDrawable
|
||||||
val elevation = recyclerView.context.getDimenSafe(R.dimen.elevation_small)
|
val elevation = recyclerView.context.getDimenSafe(R.dimen.elevation_small)
|
||||||
holder.itemView
|
holder.itemView
|
||||||
.animate()
|
.animate()
|
||||||
|
@ -125,7 +124,7 @@ class QueueDragCallback(private val playbackModel: PlaybackViewModel) : ItemTouc
|
||||||
if (holder.itemView.translationZ != 0f) {
|
if (holder.itemView.translationZ != 0f) {
|
||||||
logD("Dropping queue item")
|
logD("Dropping queue item")
|
||||||
|
|
||||||
val bg = holder.bodyView.background as MaterialShapeDrawable
|
val bg = holder.backgroundDrawable
|
||||||
holder.itemView
|
holder.itemView
|
||||||
.animate()
|
.animate()
|
||||||
.translationZ(0.0f)
|
.translationZ(0.0f)
|
||||||
|
|
|
@ -61,6 +61,10 @@ class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), QueueItemList
|
||||||
binding.queueRecycler.adapter = null
|
binding.queueRecycler.adapter = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onClick(viewHolder: RecyclerView.ViewHolder) {
|
||||||
|
playbackModel.goto(viewHolder.bindingAdapterPosition)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onPickUp(viewHolder: RecyclerView.ViewHolder) {
|
override fun onPickUp(viewHolder: RecyclerView.ViewHolder) {
|
||||||
touchHelper.startDrag(viewHolder)
|
touchHelper.startDrag(viewHolder)
|
||||||
}
|
}
|
||||||
|
|
|
@ -200,9 +200,9 @@ class PlaybackStateManager private constructor() {
|
||||||
// Increment the index, if it cannot be incremented any further, then
|
// Increment the index, if it cannot be incremented any further, then
|
||||||
// repeat and pause/resume playback depending on the setting
|
// repeat and pause/resume playback depending on the setting
|
||||||
if (index < _queue.lastIndex) {
|
if (index < _queue.lastIndex) {
|
||||||
goto(index + 1, true)
|
gotoImpl(index + 1, true)
|
||||||
} else {
|
} else {
|
||||||
goto(0, repeatMode == RepeatMode.ALL)
|
gotoImpl(0, repeatMode == RepeatMode.ALL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -214,11 +214,16 @@ class PlaybackStateManager private constructor() {
|
||||||
rewind()
|
rewind()
|
||||||
isPlaying = true
|
isPlaying = true
|
||||||
} else {
|
} else {
|
||||||
goto(max(index - 1, 0), true)
|
gotoImpl(max(index - 1, 0), true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun goto(idx: Int, play: Boolean) {
|
@Synchronized
|
||||||
|
fun goto(index: Int) {
|
||||||
|
gotoImpl(index, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun gotoImpl(idx: Int, play: Boolean) {
|
||||||
index = idx
|
index = idx
|
||||||
seekTo(0)
|
seekTo(0)
|
||||||
notifyIndexMoved()
|
notifyIndexMoved()
|
||||||
|
|
|
@ -21,11 +21,15 @@
|
||||||
android:src="@drawable/ic_delete_24"
|
android:src="@drawable/ic_delete_24"
|
||||||
app:tint="?attr/colorSurface" />
|
app:tint="?attr/colorSurface" />
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<FrameLayout
|
||||||
android:id="@+id/body"
|
android:id="@+id/body"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content">
|
||||||
android:background="?attr/colorSurface">
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?attr/selectableItemBackground">
|
||||||
|
|
||||||
<org.oxycblt.auxio.image.StyledImageView
|
<org.oxycblt.auxio.image.StyledImageView
|
||||||
android:id="@+id/song_album_cover"
|
android:id="@+id/song_album_cover"
|
||||||
|
@ -73,5 +77,7 @@
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="@+id/song_album_cover" />
|
app:layout_constraintTop_toTopOf="@+id/song_album_cover" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|
Loading…
Reference in a new issue