Make QueueAdapter Adapter instead of ListAdapter
Fix a stupid amount of bugs by changing QueueAdapter to a normal adapter with a differ and addition/movement functions instead of a ListAdapter.
This commit is contained in:
parent
2aa630948b
commit
2ebee41ed0
15 changed files with 85 additions and 61 deletions
|
@ -7,7 +7,6 @@
|
|||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package org.oxycblt.auxio.music.processing
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ContentResolver
|
||||
import android.provider.MediaStore
|
||||
import android.provider.MediaStore.Audio.Albums
|
||||
|
@ -20,6 +21,7 @@ enum class MusicLoaderResponse {
|
|||
|
||||
// Class that loads music from the FileSystem.
|
||||
// TODO: Add custom artist images from the filesystem
|
||||
// TODO: Move genre loading of songs [Loads would take longer though]
|
||||
class MusicLoader(
|
||||
private val resolver: ContentResolver,
|
||||
|
||||
|
@ -163,9 +165,9 @@ class MusicLoader(
|
|||
it.genres.add(genre)
|
||||
}
|
||||
}
|
||||
|
||||
cursor.close()
|
||||
}
|
||||
|
||||
artistGenreCursor?.close()
|
||||
}
|
||||
|
||||
Log.d(
|
||||
|
@ -174,6 +176,7 @@ class MusicLoader(
|
|||
)
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
private fun loadAlbums() {
|
||||
Log.d(this::class.simpleName, "Starting album search...")
|
||||
|
||||
|
@ -225,6 +228,7 @@ class MusicLoader(
|
|||
)
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
private fun loadSongs() {
|
||||
Log.d(this::class.simpleName, "Starting song search...")
|
||||
|
||||
|
|
|
@ -111,7 +111,7 @@ fun NotificationCompat.Builder.updateMode(context: Context) {
|
|||
if (!NotificationUtils.DO_COMPAT_SUBTEXT) {
|
||||
val playbackManager = PlaybackStateManager.getInstance()
|
||||
|
||||
// If the mode is ALL_SONGS, then just put a string, otherwise put the parent model's name.
|
||||
// If playing from all songs, set the subtext as that, otherwise the currently played parent.
|
||||
if (playbackManager.mode == PlaybackMode.ALL_SONGS) {
|
||||
setSubText(context.getString(R.string.title_all_songs))
|
||||
} else {
|
||||
|
|
|
@ -11,6 +11,7 @@ import org.oxycblt.auxio.music.BaseModel
|
|||
import org.oxycblt.auxio.music.Genre
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.music.toDuration
|
||||
import org.oxycblt.auxio.playback.queue.QueueAdapter
|
||||
import org.oxycblt.auxio.playback.state.LoopMode
|
||||
import org.oxycblt.auxio.playback.state.PlaybackMode
|
||||
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||
|
@ -156,11 +157,13 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
|||
}
|
||||
|
||||
// Remove a queue OR user queue item, given a QueueAdapter index.
|
||||
fun removeQueueItem(adapterIndex: Int) {
|
||||
fun removeQueueItem(adapterIndex: Int, queueAdapter: QueueAdapter) {
|
||||
var index = adapterIndex.dec()
|
||||
|
||||
// If the item is in the user queue, then remove it from there after accounting for the header.
|
||||
if (index < mUserQueue.value!!.size) {
|
||||
queueAdapter.removeItem(adapterIndex)
|
||||
|
||||
playbackManager.removeUserQueueItem(index)
|
||||
} else {
|
||||
// Translate the indices into proper queue indices if removing an item from there.
|
||||
|
@ -170,12 +173,14 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
|||
index -= mUserQueue.value!!.size.inc()
|
||||
}
|
||||
|
||||
queueAdapter.removeItem(adapterIndex)
|
||||
|
||||
playbackManager.removeQueueItem(index)
|
||||
}
|
||||
}
|
||||
|
||||
// Move queue OR user queue items, given QueueAdapter indices.
|
||||
fun moveQueueItems(adapterFrom: Int, adapterTo: Int): Boolean {
|
||||
fun moveQueueItems(adapterFrom: Int, adapterTo: Int, queueAdapter: QueueAdapter): Boolean {
|
||||
var from = adapterFrom.dec()
|
||||
var to = adapterTo.dec()
|
||||
|
||||
|
@ -183,6 +188,8 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
|||
// Ignore invalid movements to out of bounds, header, or queue positions
|
||||
if (to >= mUserQueue.value!!.size || to < 0) return false
|
||||
|
||||
queueAdapter.moveItems(adapterFrom, adapterTo)
|
||||
|
||||
playbackManager.moveUserQueueItems(from, to)
|
||||
} else {
|
||||
// Ignore invalid movements to out of bounds or header positions
|
||||
|
@ -205,6 +212,8 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
|||
if (to <= mIndex.value!!) return false
|
||||
}
|
||||
|
||||
queueAdapter.moveItems(adapterFrom, adapterTo)
|
||||
|
||||
playbackManager.moveQueueItems(from, to)
|
||||
}
|
||||
|
||||
|
|
|
@ -5,8 +5,8 @@ import android.util.Log
|
|||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.AsyncListDiffer
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.oxycblt.auxio.databinding.ItemQueueSongBinding
|
||||
import org.oxycblt.auxio.music.BaseModel
|
||||
|
@ -16,11 +16,24 @@ import org.oxycblt.auxio.recycler.DiffCallback
|
|||
import org.oxycblt.auxio.recycler.viewholders.BaseViewHolder
|
||||
import org.oxycblt.auxio.recycler.viewholders.HeaderViewHolder
|
||||
|
||||
/**
|
||||
* The single adapter for both the Next Queue and the User Queue.
|
||||
* - [submitList] is for the plain async diff calculations, use this if you
|
||||
* have no idea what the differences are between the old data & the new data
|
||||
* - [removeItem] and [moveItems] are used by [org.oxycblt.auxio.playback.PlaybackViewModel]
|
||||
* so that this adapter doesn't flip-out when items are moved (Which happens with [AsyncListDiffer])
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class QueueAdapter(
|
||||
val touchHelper: ItemTouchHelper
|
||||
) : ListAdapter<BaseModel, RecyclerView.ViewHolder>(DiffCallback<BaseModel>()) {
|
||||
private val touchHelper: ItemTouchHelper
|
||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
private var data = mutableListOf<BaseModel>()
|
||||
private var listDiffer = AsyncListDiffer(this, DiffCallback())
|
||||
|
||||
override fun getItemCount(): Int = data.size
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
val item = getItem(position)
|
||||
val item = data[position]
|
||||
|
||||
return if (item is Header)
|
||||
HeaderViewHolder.ITEM_TYPE
|
||||
|
@ -39,7 +52,7 @@ class QueueAdapter(
|
|||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
when (val item = getItem(position)) {
|
||||
when (val item = data[position]) {
|
||||
is Song -> (holder as ViewHolder).bind(item)
|
||||
is Header -> (holder as HeaderViewHolder).bind(item)
|
||||
|
||||
|
@ -49,6 +62,27 @@ class QueueAdapter(
|
|||
}
|
||||
}
|
||||
|
||||
fun submitList(newData: MutableList<BaseModel>) {
|
||||
if (data != newData) {
|
||||
data = newData
|
||||
|
||||
listDiffer.submitList(newData)
|
||||
}
|
||||
}
|
||||
|
||||
fun moveItems(adapterFrom: Int, adapterTo: Int) {
|
||||
val item = data.removeAt(adapterFrom)
|
||||
data.add(adapterTo, item)
|
||||
|
||||
notifyItemMoved(adapterFrom, adapterTo)
|
||||
}
|
||||
|
||||
fun removeItem(adapterIndex: Int) {
|
||||
data.removeAt(adapterIndex)
|
||||
|
||||
notifyItemRemoved(adapterIndex)
|
||||
}
|
||||
|
||||
// Generic ViewHolder for a queue item
|
||||
inner class ViewHolder(
|
||||
private val binding: ItemQueueSongBinding,
|
||||
|
|
|
@ -10,6 +10,8 @@ import kotlin.math.sign
|
|||
|
||||
// The drag callback used for the Queue RecyclerView.
|
||||
class QueueDragCallback(private val playbackModel: PlaybackViewModel) : ItemTouchHelper.Callback() {
|
||||
private lateinit var queueAdapter: QueueAdapter
|
||||
|
||||
override fun getMovementFlags(
|
||||
recyclerView: RecyclerView,
|
||||
viewHolder: RecyclerView.ViewHolder
|
||||
|
@ -51,11 +53,15 @@ class QueueDragCallback(private val playbackModel: PlaybackViewModel) : ItemTouc
|
|||
viewHolder: RecyclerView.ViewHolder,
|
||||
target: RecyclerView.ViewHolder
|
||||
): Boolean {
|
||||
return playbackModel.moveQueueItems(viewHolder.adapterPosition, target.adapterPosition)
|
||||
return playbackModel.moveQueueItems(viewHolder.adapterPosition, target.adapterPosition, queueAdapter)
|
||||
}
|
||||
|
||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
||||
playbackModel.removeQueueItem(viewHolder.adapterPosition)
|
||||
playbackModel.removeQueueItem(viewHolder.adapterPosition, queueAdapter)
|
||||
}
|
||||
|
||||
fun addQueueAdapter(adapter: QueueAdapter) {
|
||||
queueAdapter = adapter
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -8,7 +8,6 @@ import androidx.fragment.app.Fragment
|
|||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.FragmentQueueBinding
|
||||
import org.oxycblt.auxio.music.BaseModel
|
||||
|
@ -27,8 +26,10 @@ class QueueFragment : Fragment() {
|
|||
): View? {
|
||||
val binding = FragmentQueueBinding.inflate(inflater)
|
||||
|
||||
val helper = ItemTouchHelper(QueueDragCallback(playbackModel))
|
||||
val callback = QueueDragCallback(playbackModel)
|
||||
val helper = ItemTouchHelper(callback)
|
||||
val queueAdapter = QueueAdapter(helper)
|
||||
callback.addQueueAdapter(queueAdapter)
|
||||
|
||||
// --- UI SETUP ---
|
||||
|
||||
|
@ -50,9 +51,7 @@ class QueueFragment : Fragment() {
|
|||
findNavController().navigateUp()
|
||||
}
|
||||
|
||||
queueAdapter.submitList(createQueueDisplay()) {
|
||||
scrollRecyclerIfNeeded(binding)
|
||||
}
|
||||
queueAdapter.submitList(createQueueData())
|
||||
}
|
||||
|
||||
playbackModel.nextItemsInQueue.observe(viewLifecycleOwner) {
|
||||
|
@ -60,15 +59,13 @@ class QueueFragment : Fragment() {
|
|||
findNavController().navigateUp()
|
||||
}
|
||||
|
||||
queueAdapter.submitList(createQueueDisplay()) {
|
||||
scrollRecyclerIfNeeded(binding)
|
||||
}
|
||||
queueAdapter.submitList(createQueueData())
|
||||
}
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
private fun createQueueDisplay(): MutableList<BaseModel> {
|
||||
private fun createQueueData(): MutableList<BaseModel> {
|
||||
val queue = mutableListOf<BaseModel>()
|
||||
|
||||
if (playbackModel.userQueue.value!!.isNotEmpty()) {
|
||||
|
@ -93,12 +90,4 @@ class QueueFragment : Fragment() {
|
|||
|
||||
return queue
|
||||
}
|
||||
|
||||
private fun scrollRecyclerIfNeeded(binding: FragmentQueueBinding) {
|
||||
if ((binding.queueRecycler.layoutManager as LinearLayoutManager)
|
||||
.findFirstVisibleItemPosition() < 1
|
||||
) {
|
||||
binding.queueRecycler.scrollToPosition(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,7 +13,8 @@ import kotlin.random.Random
|
|||
/**
|
||||
* Master class for the playback state. This should ***not*** be used outside of the playback module.
|
||||
* - If you want to use the playback state in the UI, use [org.oxycblt.auxio.playback.PlaybackViewModel].
|
||||
* - If you want to add to the system aspects or the exoplayer instance, use [org.oxycblt.auxio.playback.PlaybackService].
|
||||
* - If you want to use the playback state with the ExoPlayer instance or system-side things,
|
||||
* use [org.oxycblt.auxio.playback.PlaybackService].
|
||||
*
|
||||
* All instantiation should be done with [PlaybackStateManager.from()].
|
||||
* @author OxygenCobalt
|
||||
|
@ -120,7 +121,7 @@ class PlaybackStateManager private constructor() {
|
|||
PlaybackMode.ALL_SONGS -> null
|
||||
PlaybackMode.IN_ARTIST -> song.album.artist
|
||||
PlaybackMode.IN_ALBUM -> song.album
|
||||
PlaybackMode.IN_GENRE -> error("what")
|
||||
PlaybackMode.IN_GENRE -> song.album.artist.genres[0]
|
||||
}
|
||||
|
||||
mMode = mode
|
||||
|
@ -129,7 +130,7 @@ class PlaybackStateManager private constructor() {
|
|||
PlaybackMode.ALL_SONGS -> musicStore.songs.toMutableList()
|
||||
PlaybackMode.IN_ARTIST -> song.album.artist.songs
|
||||
PlaybackMode.IN_ALBUM -> song.album.songs
|
||||
PlaybackMode.IN_GENRE -> error("what")
|
||||
PlaybackMode.IN_GENRE -> song.album.artist.genres[0].songs
|
||||
}
|
||||
|
||||
resetLoopMode()
|
||||
|
|
|
@ -6,10 +6,10 @@ import org.oxycblt.auxio.music.BaseModel
|
|||
// Base Diff callback
|
||||
class DiffCallback<T : BaseModel> : DiffUtil.ItemCallback<T>() {
|
||||
override fun areContentsTheSame(oldItem: T, newItem: T): Boolean {
|
||||
return oldItem.id == newItem.id
|
||||
return oldItem == newItem
|
||||
}
|
||||
|
||||
override fun areItemsTheSame(oldItem: T, newItem: T): Boolean {
|
||||
return oldItem == newItem
|
||||
return oldItem.id == newItem.id
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,6 +39,10 @@ fun ImageButton.disable(context: Context) {
|
|||
}
|
||||
}
|
||||
|
||||
fun String.createToast(context: Context) {
|
||||
Toast.makeText(context, this, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
// Apply a custom vertical divider
|
||||
fun RecyclerView.applyDivider() {
|
||||
val div = DividerItemDecoration(
|
||||
|
@ -117,19 +121,7 @@ private fun doUserQueueAdd(context: Context, song: Song, playbackModel: Playback
|
|||
// This is just to prevent a bug with DiffCallback that creates strange
|
||||
// behavior when duplicate user queue items are added.
|
||||
// FIXME: Fix the duplicate item DiffCallback issue
|
||||
if (!playbackModel.userQueue.value!!.contains(song)) {
|
||||
playbackModel.addToUserQueue(song)
|
||||
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.label_queue_added),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
} else {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.label_queue_already_added),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
context.getString(R.string.label_queue_added).createToast(context)
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ private val ACCENTS = listOf(
|
|||
Pair(R.color.deep_purple, R.style.Theme_DeepPurple), // 3
|
||||
Pair(R.color.indigo, R.style.Theme_Indigo), // 4
|
||||
Pair(R.color.blue, R.style.Theme_Blue), // 5
|
||||
Pair(R.color.light_blue, R.style.Theme_Blue), // 6
|
||||
Pair(R.color.light_blue, R.style.Theme_LightBlue), // 6
|
||||
Pair(R.color.cyan, R.style.Theme_Cyan), // 7
|
||||
Pair(R.color.teal, R.style.Theme_Teal), // 8
|
||||
Pair(R.color.green, R.style.Theme_Green), // 9
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape android:shape="oval">
|
||||
<solid android:color="?attr/colorPrimary" />
|
||||
</shape>
|
||||
</item>
|
||||
</selector>
|
|
@ -1,7 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
tools:context=".recycler.viewholders.HeaderViewHolder">
|
||||
|
||||
<data>
|
||||
|
|
|
@ -61,7 +61,6 @@
|
|||
<string name="placeholder_genre">Unknown Genre</string>
|
||||
<string name="placeholder_artist">Unknown Artist</string>
|
||||
<string name="placeholder_album">Unknown Album</string>
|
||||
<string name="placeholder_song">Unknown Song</string>
|
||||
<string name="placeholder_no_date">No Date</string>
|
||||
|
||||
<!-- Format Namespace | Value formatting/plurals -->
|
||||
|
|
|
@ -7,7 +7,7 @@ buildscript {
|
|||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:4.1.0'
|
||||
classpath 'com.android.tools.build:gradle:4.1.1'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath "android.arch.navigation:navigation-safe-args-gradle-plugin:1.0.0"
|
||||
|
||||
|
|
Loading…
Reference in a new issue