Add ability to clear user queue
Add an option to clear the user queue in QueueFragment.
This commit is contained in:
parent
49897c53b4
commit
1d50d24c4f
11 changed files with 82 additions and 69 deletions
|
@ -8,7 +8,10 @@ import android.database.sqlite.SQLiteOpenHelper
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A bootstrapped SQLite database for managing the persistent playback state and queue.
|
* A SQLite database for managing the persistent playback state and queue.
|
||||||
|
* Yes, I know androidx has Room which supposedly makes database creation easier, but it also
|
||||||
|
* has a crippling bug where it will endlessly allocate rows even if you clear the entire db, so...
|
||||||
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class PlaybackStateDatabase(context: Context) : SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) {
|
class PlaybackStateDatabase(context: Context) : SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) {
|
||||||
override fun onCreate(db: SQLiteDatabase) {
|
override fun onCreate(db: SQLiteDatabase) {
|
||||||
|
@ -64,6 +67,9 @@ class PlaybackStateDatabase(context: Context) : SQLiteOpenHelper(context, DB_NAM
|
||||||
|
|
||||||
// --- INTERFACE FUNCTIONS ---
|
// --- INTERFACE FUNCTIONS ---
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the previously written [PlaybackState] and write a new one.
|
||||||
|
*/
|
||||||
fun writeState(state: PlaybackState) {
|
fun writeState(state: PlaybackState) {
|
||||||
val database = writableDatabase
|
val database = writableDatabase
|
||||||
database.beginTransaction()
|
database.beginTransaction()
|
||||||
|
@ -102,6 +108,11 @@ class PlaybackStateDatabase(context: Context) : SQLiteOpenHelper(context, DB_NAM
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the stored [PlaybackState] from the database, if there is one.
|
||||||
|
* @return The stored [PlaybackState], null if there isnt one,.
|
||||||
|
* @author OxygenCobalt
|
||||||
|
*/
|
||||||
fun readState(): PlaybackState? {
|
fun readState(): PlaybackState? {
|
||||||
val database = writableDatabase
|
val database = writableDatabase
|
||||||
|
|
||||||
|
@ -112,6 +123,7 @@ class PlaybackStateDatabase(context: Context) : SQLiteOpenHelper(context, DB_NAM
|
||||||
stateCursor = database.query(TABLE_NAME_STATE, null, null, null, null, null, null)
|
stateCursor = database.query(TABLE_NAME_STATE, null, null, null, null, null, null)
|
||||||
|
|
||||||
stateCursor?.use { cursor ->
|
stateCursor?.use { cursor ->
|
||||||
|
// Don't bother if the cursor [and therefore database] has nothing in it.
|
||||||
if (cursor.count == 0) return@use
|
if (cursor.count == 0) return@use
|
||||||
|
|
||||||
val songIndex = cursor.getColumnIndexOrThrow(PlaybackState.COLUMN_SONG_ID)
|
val songIndex = cursor.getColumnIndexOrThrow(PlaybackState.COLUMN_SONG_ID)
|
||||||
|
@ -127,6 +139,7 @@ class PlaybackStateDatabase(context: Context) : SQLiteOpenHelper(context, DB_NAM
|
||||||
val inUserQueueIndex =
|
val inUserQueueIndex =
|
||||||
cursor.getColumnIndexOrThrow(PlaybackState.COLUMN_IN_USER_QUEUE)
|
cursor.getColumnIndexOrThrow(PlaybackState.COLUMN_IN_USER_QUEUE)
|
||||||
|
|
||||||
|
// If there is something in it, get the first item from it, ignoring anything else.
|
||||||
cursor.moveToFirst()
|
cursor.moveToFirst()
|
||||||
|
|
||||||
state = PlaybackState(
|
state = PlaybackState(
|
||||||
|
@ -147,6 +160,11 @@ class PlaybackStateDatabase(context: Context) : SQLiteOpenHelper(context, DB_NAM
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write a list of [QueueItem]s to the database, clearing the previous queue present.
|
||||||
|
* @param queue The list of [QueueItem]s to be written.
|
||||||
|
* @author OxygenCobalt
|
||||||
|
*/
|
||||||
fun writeQueue(queue: List<QueueItem>) {
|
fun writeQueue(queue: List<QueueItem>) {
|
||||||
val database = readableDatabase
|
val database = readableDatabase
|
||||||
database.beginTransaction()
|
database.beginTransaction()
|
||||||
|
@ -196,6 +214,11 @@ class PlaybackStateDatabase(context: Context) : SQLiteOpenHelper(context, DB_NAM
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the database for any [QueueItem]s.
|
||||||
|
* @return A list of any stored [QueueItem]s.
|
||||||
|
* @author OxygenCobalt
|
||||||
|
*/
|
||||||
fun readQueue(): List<QueueItem> {
|
fun readQueue(): List<QueueItem> {
|
||||||
val database = readableDatabase
|
val database = readableDatabase
|
||||||
|
|
||||||
|
|
|
@ -120,6 +120,5 @@ data class Genre(
|
||||||
*/
|
*/
|
||||||
data class Header(
|
data class Header(
|
||||||
override val id: Long = -1,
|
override val id: Long = -1,
|
||||||
override var name: String = "",
|
override var name: String = ""
|
||||||
val isAction: Boolean = false
|
|
||||||
) : BaseModel()
|
) : BaseModel()
|
||||||
|
|
|
@ -159,7 +159,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove a queue OR user queue item, given a QueueAdapter index.
|
// Remove a queue OR user queue item, given a QueueAdapter index.
|
||||||
fun removeQueueItem(adapterIndex: Int, queueAdapter: QueueAdapter) {
|
fun removeQueueAdapterItem(adapterIndex: Int, queueAdapter: QueueAdapter) {
|
||||||
var index = adapterIndex.dec()
|
var index = adapterIndex.dec()
|
||||||
|
|
||||||
// If the item is in the user queue, then remove it from there after accounting for the header.
|
// If the item is in the user queue, then remove it from there after accounting for the header.
|
||||||
|
@ -182,7 +182,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move queue OR user queue items, given QueueAdapter indices.
|
// Move queue OR user queue items, given QueueAdapter indices.
|
||||||
fun moveQueueItems(adapterFrom: Int, adapterTo: Int, queueAdapter: QueueAdapter): Boolean {
|
fun moveQueueAdapterItems(adapterFrom: Int, adapterTo: Int, queueAdapter: QueueAdapter): Boolean {
|
||||||
var from = adapterFrom.dec()
|
var from = adapterFrom.dec()
|
||||||
var to = adapterTo.dec()
|
var to = adapterTo.dec()
|
||||||
|
|
||||||
|
@ -230,6 +230,10 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
playbackManager.addToUserQueue(songs)
|
playbackManager.addToUserQueue(songs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun clearUserQueue() {
|
||||||
|
playbackManager.clearUserQueue()
|
||||||
|
}
|
||||||
|
|
||||||
// --- STATUS FUNCTIONS ---
|
// --- STATUS FUNCTIONS ---
|
||||||
|
|
||||||
// Flip the playing status.
|
// Flip the playing status.
|
||||||
|
|
|
@ -98,6 +98,14 @@ class QueueAdapter(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun clearUserQueue() {
|
||||||
|
val nextQueueHeaderIndex = data.indexOfLast { it is Header }
|
||||||
|
val slice = data.slice(0 until nextQueueHeaderIndex)
|
||||||
|
|
||||||
|
data.removeAll(slice)
|
||||||
|
notifyItemRangeRemoved(0, slice.size)
|
||||||
|
}
|
||||||
|
|
||||||
// Generic ViewHolder for a queue item
|
// Generic ViewHolder for a queue item
|
||||||
inner class QueueSongViewHolder(
|
inner class QueueSongViewHolder(
|
||||||
private val binding: ItemQueueSongBinding,
|
private val binding: ItemQueueSongBinding,
|
||||||
|
|
|
@ -53,7 +53,7 @@ class QueueDragCallback(private val playbackModel: PlaybackViewModel) : ItemTouc
|
||||||
viewHolder: RecyclerView.ViewHolder,
|
viewHolder: RecyclerView.ViewHolder,
|
||||||
target: RecyclerView.ViewHolder
|
target: RecyclerView.ViewHolder
|
||||||
): Boolean {
|
): Boolean {
|
||||||
return playbackModel.moveQueueItems(
|
return playbackModel.moveQueueAdapterItems(
|
||||||
viewHolder.adapterPosition,
|
viewHolder.adapterPosition,
|
||||||
target.adapterPosition,
|
target.adapterPosition,
|
||||||
queueAdapter
|
queueAdapter
|
||||||
|
@ -61,7 +61,7 @@ class QueueDragCallback(private val playbackModel: PlaybackViewModel) : ItemTouc
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
||||||
playbackModel.removeQueueItem(viewHolder.adapterPosition, queueAdapter)
|
playbackModel.removeQueueAdapterItem(viewHolder.adapterPosition, queueAdapter)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addQueueAdapter(adapter: QueueAdapter) {
|
fun addQueueAdapter(adapter: QueueAdapter) {
|
||||||
|
|
|
@ -37,10 +37,22 @@ class QueueFragment : Fragment() {
|
||||||
val queueAdapter = QueueAdapter(helper)
|
val queueAdapter = QueueAdapter(helper)
|
||||||
callback.addQueueAdapter(queueAdapter)
|
callback.addQueueAdapter(queueAdapter)
|
||||||
|
|
||||||
|
val queueClearItem = binding.queueToolbar.menu.findItem(R.id.action_clear_user_queue)
|
||||||
|
|
||||||
// --- UI SETUP ---
|
// --- UI SETUP ---
|
||||||
|
|
||||||
binding.queueToolbar.setNavigationOnClickListener {
|
binding.queueToolbar.apply {
|
||||||
findNavController().navigateUp()
|
setNavigationOnClickListener {
|
||||||
|
findNavController().navigateUp()
|
||||||
|
}
|
||||||
|
|
||||||
|
setOnMenuItemClickListener {
|
||||||
|
if (it.itemId == R.id.action_clear_user_queue) {
|
||||||
|
queueAdapter.clearUserQueue()
|
||||||
|
playbackModel.clearUserQueue()
|
||||||
|
true
|
||||||
|
} else false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.queueRecycler.apply {
|
binding.queueRecycler.apply {
|
||||||
|
@ -53,8 +65,14 @@ class QueueFragment : Fragment() {
|
||||||
// --- VIEWMODEL SETUP ---
|
// --- VIEWMODEL SETUP ---
|
||||||
|
|
||||||
playbackModel.userQueue.observe(viewLifecycleOwner) {
|
playbackModel.userQueue.observe(viewLifecycleOwner) {
|
||||||
if (it.isEmpty() && playbackModel.nextItemsInQueue.value!!.isEmpty()) {
|
if (it.isEmpty()) {
|
||||||
findNavController().navigateUp()
|
queueClearItem.isEnabled = false
|
||||||
|
|
||||||
|
if (playbackModel.nextItemsInQueue.value!!.isEmpty()) {
|
||||||
|
findNavController().navigateUp()
|
||||||
|
|
||||||
|
return@observe
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
queueAdapter.submitList(createQueueData())
|
queueAdapter.submitList(createQueueData())
|
||||||
|
@ -76,10 +94,7 @@ class QueueFragment : Fragment() {
|
||||||
|
|
||||||
if (playbackModel.userQueue.value!!.isNotEmpty()) {
|
if (playbackModel.userQueue.value!!.isNotEmpty()) {
|
||||||
queue.add(
|
queue.add(
|
||||||
Header(
|
Header(name = getString(R.string.label_next_user_queue))
|
||||||
name = getString(R.string.label_next_user_queue),
|
|
||||||
isAction = true
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
queue.addAll(playbackModel.userQueue.value!!)
|
queue.addAll(playbackModel.userQueue.value!!)
|
||||||
}
|
}
|
||||||
|
@ -93,8 +108,7 @@ class QueueFragment : Fragment() {
|
||||||
getString(R.string.label_all_songs)
|
getString(R.string.label_all_songs)
|
||||||
else
|
else
|
||||||
playbackModel.parent.value!!.name
|
playbackModel.parent.value!!.name
|
||||||
),
|
)
|
||||||
isAction = false
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
queue.addAll(playbackModel.nextItemsInQueue.value!!)
|
queue.addAll(playbackModel.nextItemsInQueue.value!!)
|
||||||
|
|
|
@ -337,6 +337,12 @@ class PlaybackStateManager private constructor() {
|
||||||
forceUserQueueUpdate()
|
forceUserQueueUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun clearUserQueue() {
|
||||||
|
mUserQueue.clear()
|
||||||
|
|
||||||
|
forceUserQueueUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
// Force any callbacks to update when the queue is changed.
|
// Force any callbacks to update when the queue is changed.
|
||||||
private fun forceQueueUpdate() {
|
private fun forceQueueUpdate() {
|
||||||
mQueue = mQueue
|
mQueue = mQueue
|
||||||
|
|
|
@ -20,6 +20,8 @@
|
||||||
android:clickable="true"
|
android:clickable="true"
|
||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
android:elevation="@dimen/elevation_normal"
|
android:elevation="@dimen/elevation_normal"
|
||||||
|
app:popupTheme="@style/Widget.CustomPopup"
|
||||||
|
app:menu="@menu/menu_queue"
|
||||||
app:navigationIcon="@drawable/ic_down"
|
app:navigationIcon="@drawable/ic_down"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
|
|
@ -1,52 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
tools:context=".recycler.viewholders.HeaderViewHolder">
|
|
||||||
|
|
||||||
<data>
|
|
||||||
|
|
||||||
<variable
|
|
||||||
name="header"
|
|
||||||
type="org.oxycblt.auxio.music.Header" />
|
|
||||||
</data>
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="horizontal">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/header_text"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:fontFamily="@font/inter_semibold"
|
|
||||||
android:paddingStart="@dimen/padding_medium"
|
|
||||||
android:paddingTop="@dimen/padding_small"
|
|
||||||
android:paddingEnd="@dimen/padding_small"
|
|
||||||
android:paddingBottom="@dimen/padding_small"
|
|
||||||
android:text="@{header.name}"
|
|
||||||
android:textColor="?android:attr/textColorPrimary"
|
|
||||||
android:textSize="19sp"
|
|
||||||
app:layout_constraintEnd_toStartOf="@+id/header_action_button"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
tools:text="Songs" />
|
|
||||||
|
|
||||||
<ImageButton
|
|
||||||
android:id="@+id/header_action_button"
|
|
||||||
style="@style/Widget.AppCompat.Button.Borderless"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:visibility="gone"
|
|
||||||
android:src="@drawable/ic_clear"
|
|
||||||
tools:visibility="visible"
|
|
||||||
android:background="@drawable/ui_header_dividers"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@+id/header_text"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintDimensionRatio="1:1"
|
|
||||||
app:layout_constraintTop_toTopOf="@+id/header_text"
|
|
||||||
tools:ignore="contentDescription" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
</layout>
|
|
8
app/src/main/res/menu/menu_queue.xml
Normal file
8
app/src/main/res/menu/menu_queue.xml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_clear_user_queue"
|
||||||
|
android:title="@string/label_clear_user_queue"
|
||||||
|
app:showAsAction="never" />
|
||||||
|
</menu>
|
|
@ -25,6 +25,7 @@
|
||||||
<string name="label_queue_add">Add to queue</string>
|
<string name="label_queue_add">Add to queue</string>
|
||||||
<string name="label_queue_added">Added to queue</string>
|
<string name="label_queue_added">Added to queue</string>
|
||||||
<string name="label_next_user_queue">Next in Queue</string>
|
<string name="label_next_user_queue">Next in Queue</string>
|
||||||
|
<string name="label_clear_user_queue">Clear queue</string>
|
||||||
<string name="label_channel">Music Playback</string>
|
<string name="label_channel">Music Playback</string>
|
||||||
<string name="label_service_playback">The music playback service for Auxio.</string>
|
<string name="label_service_playback">The music playback service for Auxio.</string>
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue