Add user queue

Add a user-generated queue, currently it isnt played from.
This commit is contained in:
OxygenCobalt 2020-11-07 16:06:47 -07:00
parent bc7950e7af
commit 2be7d34601
36 changed files with 464 additions and 148 deletions

View file

@ -25,7 +25,7 @@ class DetailAlbumAdapter(
// Generic ViewHolder for a detail album // Generic ViewHolder for a detail album
inner class ViewHolder( inner class ViewHolder(
private val binding: ItemArtistAlbumBinding, private val binding: ItemArtistAlbumBinding,
) : BaseViewHolder<Album>(binding, doOnClick) { ) : BaseViewHolder<Album>(binding, doOnClick, null) {
override fun onBind(data: Album) { override fun onBind(data: Album) {
binding.album = data binding.album = data

View file

@ -25,7 +25,7 @@ class DetailArtistAdapter(
// Generic ViewHolder for an album // Generic ViewHolder for an album
inner class ViewHolder( inner class ViewHolder(
private val binding: ItemGenreArtistBinding private val binding: ItemGenreArtistBinding
) : BaseViewHolder<Artist>(binding, doOnClick) { ) : BaseViewHolder<Artist>(binding, doOnClick, null) {
override fun onBind(data: Artist) { override fun onBind(data: Artist) {
binding.artist = data binding.artist = data

View file

@ -24,7 +24,7 @@ class DetailSongAdapter(
// Generic ViewHolder for a song // Generic ViewHolder for a song
inner class ViewHolder( inner class ViewHolder(
private val binding: ItemAlbumSongBinding, private val binding: ItemAlbumSongBinding,
) : BaseViewHolder<Song>(binding, doOnClick) { ) : BaseViewHolder<Song>(binding, doOnClick, null) {
override fun onBind(data: Song) { override fun onBind(data: Song) {
binding.song = data binding.song = data

View file

@ -50,9 +50,12 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
navToItem(it) navToItem(it)
} }
val searchAdapter = SearchAdapter { val searchAdapter = SearchAdapter(
navToItem(it) {
} navToItem(it)
},
{ data, view -> }
)
// --- UI SETUP --- // --- UI SETUP ---

View file

@ -1,5 +1,6 @@
package org.oxycblt.auxio.library.adapters package org.oxycblt.auxio.library.adapters
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@ -17,7 +18,8 @@ import org.oxycblt.auxio.recycler.viewholders.HeaderViewHolder
import org.oxycblt.auxio.recycler.viewholders.SongViewHolder import org.oxycblt.auxio.recycler.viewholders.SongViewHolder
class SearchAdapter( class SearchAdapter(
private val doOnClick: (data: BaseModel) -> Unit private val doOnClick: (data: BaseModel) -> Unit,
private val doOnLongClick: (data: BaseModel, view: View) -> Unit
) : ListAdapter<BaseModel, RecyclerView.ViewHolder>(DiffCallback<BaseModel>()) { ) : ListAdapter<BaseModel, RecyclerView.ViewHolder>(DiffCallback<BaseModel>()) {
override fun getItemViewType(position: Int): Int { override fun getItemViewType(position: Int): Int {
@ -35,7 +37,11 @@ class SearchAdapter(
GenreViewHolder.ITEM_TYPE -> GenreViewHolder.from(parent.context, doOnClick) GenreViewHolder.ITEM_TYPE -> GenreViewHolder.from(parent.context, doOnClick)
ArtistViewHolder.ITEM_TYPE -> ArtistViewHolder.from(parent.context, doOnClick) ArtistViewHolder.ITEM_TYPE -> ArtistViewHolder.from(parent.context, doOnClick)
AlbumViewHolder.ITEM_TYPE -> AlbumViewHolder.from(parent.context, doOnClick) AlbumViewHolder.ITEM_TYPE -> AlbumViewHolder.from(parent.context, doOnClick)
SongViewHolder.ITEM_TYPE -> SongViewHolder.from(parent.context, doOnClick) SongViewHolder.ITEM_TYPE -> SongViewHolder.from(
parent.context,
doOnClick,
doOnLongClick
)
HeaderViewHolder.ITEM_TYPE -> HeaderViewHolder.from(parent.context) HeaderViewHolder.ITEM_TYPE -> HeaderViewHolder.from(parent.context)
else -> HeaderViewHolder.from(parent.context) else -> HeaderViewHolder.from(parent.context)

View file

@ -81,6 +81,7 @@ class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
// Make marquee scroll work // Make marquee scroll work
binding.playbackSong.isSelected = true binding.playbackSong.isSelected = true
binding.playbackSeekBar.setOnSeekBarChangeListener(this) binding.playbackSeekBar.setOnSeekBarChangeListener(this)
// --- VIEWMODEL SETUP -- // --- VIEWMODEL SETUP --
@ -178,7 +179,7 @@ class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
playbackModel.nextItemsInQueue.observe(viewLifecycleOwner) { playbackModel.nextItemsInQueue.observe(viewLifecycleOwner) {
// Disable the option to open the queue if there's nothing in it. // Disable the option to open the queue if there's nothing in it.
if (it.isEmpty()) { if (it.isEmpty() && playbackModel.userQueue.value!!.isEmpty()) {
queueMenuItem.isEnabled = false queueMenuItem.isEnabled = false
queueMenuItem.icon = iconQueueInactive queueMenuItem.icon = iconQueueInactive
} else { } else {
@ -195,6 +196,16 @@ class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
} }
} }
playbackModel.userQueue.observe(viewLifecycleOwner) {
if (it.isEmpty() && playbackModel.queue.value!!.isEmpty()) {
queueMenuItem.isEnabled = false
queueMenuItem.icon = iconQueueInactive
} else {
queueMenuItem.isEnabled = true
queueMenuItem.icon = iconQueueActive
}
}
Log.d(this::class.simpleName, "Fragment Created.") Log.d(this::class.simpleName, "Fragment Created.")
return binding.root return binding.root

View file

@ -33,6 +33,9 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
private val mQueue = MutableLiveData(mutableListOf<Song>()) private val mQueue = MutableLiveData(mutableListOf<Song>())
val queue: LiveData<MutableList<Song>> get() = mQueue val queue: LiveData<MutableList<Song>> get() = mQueue
private val mUserQueue = MutableLiveData(mutableListOf<Song>())
val userQueue: LiveData<MutableList<Song>> get() = mUserQueue
private val mIndex = MutableLiveData(0) private val mIndex = MutableLiveData(0)
val index: LiveData<Int> get() = mIndex val index: LiveData<Int> get() = mIndex
@ -169,6 +172,18 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
playbackManager.moveQueueItems(from, to) playbackManager.moveQueueItems(from, to)
} }
fun addToUserQueue(song: Song) {
playbackManager.addToUserQueue(song)
}
fun moveUserQueueItems(from: Int, to: Int) {
playbackManager.moveUserQueueItems(from, to)
}
fun removeUserQueueItem(index: Int) {
playbackManager.removeUserQueueItem(index)
}
// --- STATUS FUNCTIONS --- // --- STATUS FUNCTIONS ---
// Flip the playing status. // Flip the playing status.
@ -215,6 +230,10 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
mQueue.value = queue mQueue.value = queue
} }
override fun onUserQueueUpdate(userQueue: MutableList<Song>) {
mUserQueue.value = userQueue
}
override fun onIndexUpdate(index: Int) { override fun onIndexUpdate(index: Int) {
mIndex.value = index mIndex.value = index
} }
@ -241,6 +260,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
mSong.value = playbackManager.song mSong.value = playbackManager.song
mPosition.value = playbackManager.position / 1000 mPosition.value = playbackManager.position / 1000
mQueue.value = playbackManager.queue mQueue.value = playbackManager.queue
mUserQueue.value = playbackManager.userQueue
mIndex.value = playbackManager.index mIndex.value = playbackManager.index
mIsPlaying.value = playbackManager.isPlaying mIsPlaying.value = playbackManager.isPlaying
mIsShuffling.value = playbackManager.isShuffling mIsShuffling.value = playbackManager.isShuffling

View file

@ -25,7 +25,7 @@ class QueueAdapter(
// Generic ViewHolder for a queue item // Generic ViewHolder for a queue item
inner class ViewHolder( inner class ViewHolder(
private val binding: ItemQueueSongBinding, private val binding: ItemQueueSongBinding,
) : BaseViewHolder<Song>(binding, null) { ) : BaseViewHolder<Song>(binding, null, null) {
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
override fun onBind(data: Song) { override fun onBind(data: Song) {

View file

@ -10,7 +10,8 @@ import kotlin.math.sign
// The drag callback used for the Queue RecyclerView. // The drag callback used for the Queue RecyclerView.
class QueueDragCallback( class QueueDragCallback(
private val playbackModel: PlaybackViewModel private val playbackModel: PlaybackViewModel,
private val isUserQueue: Boolean
) : ItemTouchHelper.SimpleCallback( ) : ItemTouchHelper.SimpleCallback(
ItemTouchHelper.UP or ItemTouchHelper.DOWN, ItemTouchHelper.UP or ItemTouchHelper.DOWN,
ItemTouchHelper.START ItemTouchHelper.START
@ -24,7 +25,6 @@ class QueueDragCallback(
): Int { ): Int {
// Fix to make QueueFragment scroll when an item is scrolled out of bounds. // Fix to make QueueFragment scroll when an item is scrolled out of bounds.
// Adapted from NewPipe: https://github.com/TeamNewPipe/NewPipe // Adapted from NewPipe: https://github.com/TeamNewPipe/NewPipe
val standardSpeed = super.interpolateOutOfBoundsScroll( val standardSpeed = super.interpolateOutOfBoundsScroll(
recyclerView, viewSize, viewSizeOutOfBounds, totalSize, msSinceStartScroll recyclerView, viewSize, viewSizeOutOfBounds, totalSize, msSinceStartScroll
) )
@ -45,13 +45,21 @@ class QueueDragCallback(
viewHolder: RecyclerView.ViewHolder, viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder target: RecyclerView.ViewHolder
): Boolean { ): Boolean {
playbackModel.moveQueueItems(viewHolder.adapterPosition, target.adapterPosition) if (isUserQueue) {
playbackModel.moveUserQueueItems(viewHolder.adapterPosition, target.adapterPosition)
} else {
playbackModel.moveQueueItems(viewHolder.adapterPosition, target.adapterPosition)
}
return true return true
} }
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
playbackModel.removeQueueItem(viewHolder.adapterPosition) if (isUserQueue) {
playbackModel.removeUserQueueItem(viewHolder.adapterPosition)
} else {
playbackModel.removeQueueItem(viewHolder.adapterPosition)
}
} }
companion object { companion object {

View file

@ -4,25 +4,17 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentQueueBinding import org.oxycblt.auxio.databinding.FragmentQueueBinding
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.playback.state.PlaybackMode
import org.oxycblt.auxio.theme.accent
import org.oxycblt.auxio.theme.applyDivider
import org.oxycblt.auxio.theme.toColor
// TODO: Make this better
class QueueFragment : BottomSheetDialogFragment() { class QueueFragment : BottomSheetDialogFragment() {
private val playbackModel: PlaybackViewModel by activityViewModels() private val playbackModel: PlaybackViewModel by activityViewModels()
override fun getTheme(): Int = R.style.Theme_BottomSheetFix
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
@ -30,58 +22,24 @@ class QueueFragment : BottomSheetDialogFragment() {
): View? { ): View? {
val binding = FragmentQueueBinding.inflate(inflater) val binding = FragmentQueueBinding.inflate(inflater)
val helper = ItemTouchHelper(QueueDragCallback(playbackModel)) binding.queueViewpager.adapter = PagerAdapter()
val queueAdapter = QueueAdapter(helper)
// --- UI SETUP --- // TODO: Add option for default queue screen
if (playbackModel.userQueue.value!!.isEmpty()) {
binding.queueHeader.setTextColor(accent.first.toColor(requireContext())) binding.queueViewpager.setCurrentItem(1, false)
binding.queueRecycler.apply { } else {
adapter = queueAdapter binding.queueViewpager.setCurrentItem(0, false)
itemAnimator = DefaultItemAnimator()
applyDivider()
setHasFixedSize(true)
helper.attachToRecyclerView(this)
}
// --- VIEWMODEL SETUP ---
playbackModel.mode.observe(viewLifecycleOwner) {
if (it == PlaybackMode.ALL_SONGS) {
binding.queueHeader.setText(R.string.label_next_songs)
} else {
binding.queueHeader.text = getString(
R.string.format_next_from, playbackModel.parent.value!!.name
)
}
}
playbackModel.nextItemsInQueue.observe(viewLifecycleOwner) {
if (it.isEmpty()) {
findNavController().navigateUp()
return@observe
}
// If the first item is being moved, then scroll to the top position on completion
// to prevent ListAdapter from scrolling uncontrollably.
if (queueAdapter.currentList.isNotEmpty() && it[0].id != queueAdapter.currentList[0].id) {
queueAdapter.submitList(it.toMutableList()) {
// Make sure that the RecyclerView doesn't scroll to the top if the first item
// changed, but is not visible.
val firstItem = (binding.queueRecycler.layoutManager as LinearLayoutManager)
.findFirstVisibleItemPosition()
if (firstItem == -1 || firstItem == 0) {
binding.queueRecycler.scrollToPosition(0)
}
}
} else {
queueAdapter.submitList(it.toMutableList())
}
} }
return binding.root return binding.root
} }
private inner class PagerAdapter :
FragmentStateAdapter(childFragmentManager, viewLifecycleOwner.lifecycle) {
override fun getItemCount(): Int = 2
override fun createFragment(position: Int): Fragment {
return QueueListFragment(position)
}
}
} }

View file

@ -0,0 +1,146 @@
package org.oxycblt.auxio.playback.queue
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentQueueListBinding
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.playback.state.PlaybackMode
import org.oxycblt.auxio.theme.applyDivider
class QueueListFragment(private val type: Int) : Fragment() {
private val playbackModel: PlaybackViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding = FragmentQueueListBinding.inflate(inflater)
// --- UI SETUP ---
binding.queueRecycler.apply {
itemAnimator = DefaultItemAnimator()
applyDivider()
setHasFixedSize(true)
}
// Continue setup with different values depending on the type
when (type) {
TYPE_NEXT_QUEUE -> setupForNextQueue(binding)
TYPE_USER_QUEUE -> setupForUserQueue(binding)
}
return binding.root
}
private fun setupForNextQueue(binding: FragmentQueueListBinding) {
val helper = ItemTouchHelper(QueueDragCallback(playbackModel, false))
val queueNextAdapter = QueueAdapter(helper)
binding.queueRecycler.apply {
adapter = queueNextAdapter
helper.attachToRecyclerView(this)
}
playbackModel.mode.observe(viewLifecycleOwner) {
if (it == PlaybackMode.ALL_SONGS) {
binding.queueHeader.setText(R.string.label_next_songs)
} else {
binding.queueHeader.text = getString(
R.string.format_next_from, playbackModel.parent.value!!.name
)
}
}
playbackModel.nextItemsInQueue.observe(viewLifecycleOwner) {
if (it.isEmpty()) {
if (playbackModel.userQueue.value!!.isEmpty()) {
findNavController().navigateUp()
} else {
binding.queueNothingIndicator.visibility = View.VISIBLE
binding.queueRecycler.visibility = View.GONE
}
return@observe
}
binding.queueNothingIndicator.visibility = View.GONE
binding.queueRecycler.visibility = View.VISIBLE
// If the first item is being moved, then scroll to the top position on completion
// to prevent ListAdapter from scrolling uncontrollably.
if (queueNextAdapter.currentList.isNotEmpty() &&
it[0].id != queueNextAdapter.currentList[0].id
) {
queueNextAdapter.submitList(it.toMutableList()) {
scrollRecyclerIfNeeded(binding)
}
} else {
queueNextAdapter.submitList(it.toMutableList())
}
}
}
private fun setupForUserQueue(binding: FragmentQueueListBinding) {
val helper = ItemTouchHelper(QueueDragCallback(playbackModel, true))
val userQueueAdapter = QueueAdapter(helper)
binding.queueHeader.setText(R.string.label_next_user_queue)
binding.queueRecycler.apply {
adapter = userQueueAdapter
helper.attachToRecyclerView(this)
}
playbackModel.userQueue.observe(viewLifecycleOwner) {
if (it.isEmpty()) {
if (playbackModel.queue.value!!.isEmpty()) {
findNavController().navigateUp()
} else {
binding.queueNothingIndicator.visibility = View.VISIBLE
binding.queueRecycler.visibility = View.GONE
}
return@observe
}
binding.queueNothingIndicator.visibility = View.GONE
binding.queueRecycler.visibility = View.VISIBLE
// If the first item is being moved, then scroll to the top position on completion
// to prevent ListAdapter from scrolling uncontrollably.
if (userQueueAdapter.currentList.isNotEmpty() &&
it[0].id != userQueueAdapter.currentList[0].id
) {
userQueueAdapter.submitList(it.toMutableList()) {
scrollRecyclerIfNeeded(binding)
}
} else {
userQueueAdapter.submitList(it.toMutableList())
}
}
}
private fun scrollRecyclerIfNeeded(binding: FragmentQueueListBinding) {
if ((binding.queueRecycler.layoutManager as LinearLayoutManager)
.findFirstVisibleItemPosition() < 1
) {
binding.queueRecycler.scrollToPosition(0)
}
}
companion object {
const val TYPE_USER_QUEUE = 0
const val TYPE_NEXT_QUEUE = 1
}
}

View file

@ -40,6 +40,12 @@ class PlaybackStateManager private constructor() {
field = value field = value
callbacks.forEach { it.onQueueUpdate(value) } callbacks.forEach { it.onQueueUpdate(value) }
} }
private var mUserQueue = mutableListOf<Song>()
set(value) {
Log.d(this::class.simpleName, "retard.")
field = value
callbacks.forEach { it.onUserQueueUpdate(value) }
}
private var mIndex = 0 private var mIndex = 0
set(value) { set(value) {
field = value field = value
@ -74,6 +80,7 @@ class PlaybackStateManager private constructor() {
val parent: BaseModel? get() = mParent val parent: BaseModel? get() = mParent
val position: Long get() = mPosition val position: Long get() = mPosition
val queue: MutableList<Song> get() = mQueue val queue: MutableList<Song> get() = mQueue
val userQueue: MutableList<Song> get() = mUserQueue
val index: Int get() = mIndex val index: Int get() = mIndex
val mode: PlaybackMode get() = mMode val mode: PlaybackMode get() = mMode
val isPlaying: Boolean get() = mIsPlaying val isPlaying: Boolean get() = mIsPlaying
@ -252,10 +259,8 @@ class PlaybackStateManager private constructor() {
fun moveQueueItems(from: Int, to: Int) { fun moveQueueItems(from: Int, to: Int) {
try { try {
val currentItem = mQueue[from] val item = mQueue.removeAt(from)
mQueue.add(to, item)
mQueue.removeAt(from)
mQueue.add(to, currentItem)
} catch (exception: IndexOutOfBoundsException) { } catch (exception: IndexOutOfBoundsException) {
Log.e(this::class.simpleName, "Indices were out of bounds, did not move queue item") Log.e(this::class.simpleName, "Indices were out of bounds, did not move queue item")
@ -265,11 +270,48 @@ class PlaybackStateManager private constructor() {
forceQueueUpdate() forceQueueUpdate()
} }
fun addToUserQueue(song: Song) {
mUserQueue.add(song)
forceUserQueueUpdate()
}
fun removeUserQueueItem(index: Int) {
Log.d(this::class.simpleName, "Removing item ${mUserQueue[index].name}.")
if (index > mUserQueue.size || index < 0) {
Log.e(this::class.simpleName, "Index is out of bounds, did not remove queue item.")
return
}
mUserQueue.removeAt(index)
forceUserQueueUpdate()
}
fun moveUserQueueItems(from: Int, to: Int) {
try {
val item = mUserQueue.removeAt(from)
mUserQueue.add(to, item)
} catch (exception: IndexOutOfBoundsException) {
Log.e(this::class.simpleName, "Indices were out of bounds, did not move queue item")
return
}
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
} }
private fun forceUserQueueUpdate() {
mUserQueue = mUserQueue
}
// --- SHUFFLE FUNCTIONS --- // --- SHUFFLE FUNCTIONS ---
fun shuffleAll() { fun shuffleAll() {
@ -408,6 +450,7 @@ class PlaybackStateManager private constructor() {
fun onParentUpdate(parent: BaseModel?) {} fun onParentUpdate(parent: BaseModel?) {}
fun onPositionUpdate(position: Long) {} fun onPositionUpdate(position: Long) {}
fun onQueueUpdate(queue: MutableList<Song>) {} fun onQueueUpdate(queue: MutableList<Song>) {}
fun onUserQueueUpdate(userQueue: MutableList<Song>) {}
fun onModeUpdate(mode: PlaybackMode) {} fun onModeUpdate(mode: PlaybackMode) {}
fun onIndexUpdate(index: Int) {} fun onIndexUpdate(index: Int) {}
fun onPlayingUpdate(isPlaying: Boolean) {} fun onPlayingUpdate(isPlaying: Boolean) {}

View file

@ -1,5 +1,6 @@
package org.oxycblt.auxio.recycler.viewholders package org.oxycblt.auxio.recycler.viewholders
import android.view.View
import androidx.databinding.ViewDataBinding import androidx.databinding.ViewDataBinding
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.music.BaseModel import org.oxycblt.auxio.music.BaseModel
@ -7,7 +8,8 @@ import org.oxycblt.auxio.music.BaseModel
// ViewHolder abstraction that automates some of the things that are common for all ViewHolders. // ViewHolder abstraction that automates some of the things that are common for all ViewHolders.
abstract class BaseViewHolder<T : BaseModel>( abstract class BaseViewHolder<T : BaseModel>(
private val baseBinding: ViewDataBinding, private val baseBinding: ViewDataBinding,
private val doOnClick: ((data: T) -> Unit)? private val doOnClick: ((data: T) -> Unit)?,
private val doOnLongClick: ((data: T, view: View) -> Unit)?
) : RecyclerView.ViewHolder(baseBinding.root) { ) : RecyclerView.ViewHolder(baseBinding.root) {
init { init {
// Force the layout to *actually* be the screen width // Force the layout to *actually* be the screen width
@ -23,6 +25,14 @@ abstract class BaseViewHolder<T : BaseModel>(
} }
} }
doOnLongClick?.let { onLongClick ->
baseBinding.root.setOnLongClickListener {
onLongClick(data, baseBinding.root)
true
}
}
onBind(data) onBind(data)
baseBinding.executePendingBindings() baseBinding.executePendingBindings()

View file

@ -2,6 +2,7 @@ package org.oxycblt.auxio.recycler.viewholders
import android.content.Context import android.content.Context
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import org.oxycblt.auxio.databinding.ItemAlbumBinding import org.oxycblt.auxio.databinding.ItemAlbumBinding
import org.oxycblt.auxio.databinding.ItemArtistBinding import org.oxycblt.auxio.databinding.ItemArtistBinding
import org.oxycblt.auxio.databinding.ItemGenreBinding import org.oxycblt.auxio.databinding.ItemGenreBinding
@ -19,7 +20,7 @@ import org.oxycblt.auxio.music.Song
class GenreViewHolder private constructor( class GenreViewHolder private constructor(
private val binding: ItemGenreBinding, private val binding: ItemGenreBinding,
doOnClick: (Genre) -> Unit doOnClick: (Genre) -> Unit
) : BaseViewHolder<Genre>(binding, doOnClick) { ) : BaseViewHolder<Genre>(binding, doOnClick, null) {
override fun onBind(data: Genre) { override fun onBind(data: Genre) {
binding.genre = data binding.genre = data
@ -41,7 +42,7 @@ class GenreViewHolder private constructor(
class ArtistViewHolder private constructor( class ArtistViewHolder private constructor(
private val binding: ItemArtistBinding, private val binding: ItemArtistBinding,
doOnClick: (Artist) -> Unit, doOnClick: (Artist) -> Unit,
) : BaseViewHolder<Artist>(binding, doOnClick) { ) : BaseViewHolder<Artist>(binding, doOnClick, null) {
override fun onBind(data: Artist) { override fun onBind(data: Artist) {
binding.artist = data binding.artist = data
@ -63,7 +64,7 @@ class ArtistViewHolder private constructor(
class AlbumViewHolder private constructor( class AlbumViewHolder private constructor(
private val binding: ItemAlbumBinding, private val binding: ItemAlbumBinding,
doOnClick: (data: Album) -> Unit doOnClick: (data: Album) -> Unit
) : BaseViewHolder<Album>(binding, doOnClick) { ) : BaseViewHolder<Album>(binding, doOnClick, null) {
override fun onBind(data: Album) { override fun onBind(data: Album) {
binding.album = data binding.album = data
@ -87,7 +88,8 @@ class AlbumViewHolder private constructor(
class SongViewHolder private constructor( class SongViewHolder private constructor(
private val binding: ItemSongBinding, private val binding: ItemSongBinding,
doOnClick: (data: Song) -> Unit, doOnClick: (data: Song) -> Unit,
) : BaseViewHolder<Song>(binding, doOnClick) { doOnLongClick: (data: Song, view: View) -> Unit
) : BaseViewHolder<Song>(binding, doOnClick, doOnLongClick) {
override fun onBind(data: Song) { override fun onBind(data: Song) {
binding.song = data binding.song = data
@ -102,10 +104,11 @@ class SongViewHolder private constructor(
fun from( fun from(
context: Context, context: Context,
doOnClick: (data: Song) -> Unit, doOnClick: (data: Song) -> Unit,
doOnLongClick: (data: Song, view: View) -> Unit
): SongViewHolder { ): SongViewHolder {
return SongViewHolder( return SongViewHolder(
ItemSongBinding.inflate(LayoutInflater.from(context)), ItemSongBinding.inflate(LayoutInflater.from(context)),
doOnClick doOnClick, doOnLongClick
) )
} }
} }
@ -113,7 +116,7 @@ class SongViewHolder private constructor(
class HeaderViewHolder( class HeaderViewHolder(
private val binding: ItemHeaderBinding private val binding: ItemHeaderBinding
) : BaseViewHolder<Header>(binding, null) { ) : BaseViewHolder<Header>(binding, null, null) {
override fun onBind(data: Header) { override fun onBind(data: Header) {
binding.header = data binding.header = data

View file

@ -1,5 +1,6 @@
package org.oxycblt.auxio.songs package org.oxycblt.auxio.songs
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
@ -7,13 +8,14 @@ import org.oxycblt.auxio.recycler.viewholders.SongViewHolder
class SongAdapter( class SongAdapter(
private val data: List<Song>, private val data: List<Song>,
private val doOnClick: (data: Song) -> Unit private val doOnClick: (data: Song) -> Unit,
private val doOnLongClick: (data: Song, view: View) -> Unit
) : RecyclerView.Adapter<SongViewHolder>() { ) : RecyclerView.Adapter<SongViewHolder>() {
override fun getItemCount(): Int = data.size override fun getItemCount(): Int = data.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SongViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SongViewHolder {
return SongViewHolder.from(parent.context, doOnClick) return SongViewHolder.from(parent.context, doOnClick, doOnLongClick)
} }
override fun onBindViewHolder(holder: SongViewHolder, position: Int) { override fun onBindViewHolder(holder: SongViewHolder, position: Int) {

View file

@ -5,11 +5,13 @@ import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.PopupMenu
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentSongsBinding import org.oxycblt.auxio.databinding.FragmentSongsBinding
import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.playback.state.PlaybackMode import org.oxycblt.auxio.playback.state.PlaybackMode
import org.oxycblt.auxio.theme.applyDivider import org.oxycblt.auxio.theme.applyDivider
@ -39,9 +41,15 @@ class SongsFragment : Fragment() {
} }
binding.songRecycler.apply { binding.songRecycler.apply {
adapter = SongAdapter(musicStore.songs) { adapter = SongAdapter(
playbackModel.playSong(it, PlaybackMode.ALL_SONGS) musicStore.songs,
} {
playbackModel.playSong(it, PlaybackMode.ALL_SONGS)
},
{ data, view ->
showActionMenuForSong(data, view)
}
)
applyDivider() applyDivider()
setHasFixedSize(true) setHasFixedSize(true)
} }
@ -50,4 +58,21 @@ class SongsFragment : Fragment() {
return binding.root return binding.root
} }
private fun showActionMenuForSong(song: Song, view: View) {
// TODO: Replace this with something nicer
PopupMenu(requireContext(), view).apply {
inflate(R.menu.menu_song_actions)
setOnMenuItemClickListener {
if (it.itemId == R.id.action_queue_add) {
playbackModel.addToUserQueue(song)
return@setOnMenuItemClickListener true
}
false
}
show()
}
}
} }

View file

@ -11,10 +11,16 @@
<aapt:attr name="android:fillColor"> <aapt:attr name="android:fillColor">
<gradient <gradient
android:type="linear" android:type="linear"
android:startX="60" android:startY="61.5" android:startX="60"
android:endX="64.75" android:endY="28.5"> android:startY="61.5"
<item android:color="#2196f3" android:offset="0.0"/> android:endX="64.75"
<item android:color="#90caf9" android:offset="1.0"/> android:endY="28.5">
<item
android:color="#2196f3"
android:offset="0.0" />
<item
android:color="#90caf9"
android:offset="1.0" />
</gradient> </gradient>
</aapt:attr> </aapt:attr>
</path> </path>

View file

@ -4,7 +4,7 @@
android:height="24dp" android:height="24dp"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24"> android:viewportHeight="24">
<path <path
android:fillColor="@android:color/white" android:fillColor="@android:color/white"
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/> android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z" />
</vector> </vector>

View file

@ -11,10 +11,16 @@
<aapt:attr name="android:fillColor"> <aapt:attr name="android:fillColor">
<gradient <gradient
android:type="linear" android:type="linear"
android:startX="60" android:startY="61.5" android:startX="60"
android:endX="64.75" android:endY="28.5"> android:startY="61.5"
<item android:color="#2196f3" android:offset="0.0"/> android:endX="64.75"
<item android:color="#90caf9" android:offset="1.0"/> android:endY="28.5">
<item
android:color="#2196f3"
android:offset="0.0" />
<item
android:color="#90caf9"
android:offset="1.0" />
</gradient> </gradient>
</aapt:attr> </aapt:attr>
</path> </path>

View file

@ -5,7 +5,7 @@
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24" android:viewportHeight="24"
android:tint="?attr/colorControlNormal"> android:tint="?attr/colorControlNormal">
<path <path
android:fillColor="@android:color/white" android:fillColor="@android:color/white"
android:pathData="M7,7h10v3l4,-4 -4,-4v3L5,5v6h2L7,7zM17,17L7,17v-3l-4,4 4,4v-3h12v-6h-2v4z"/> android:pathData="M7,7h10v3l4,-4 -4,-4v3L5,5v6h2L7,7zM17,17L7,17v-3l-4,4 4,4v-3h12v-6h-2v4z" />
</vector> </vector>

View file

@ -4,7 +4,7 @@
android:height="24dp" android:height="24dp"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24"> android:viewportHeight="24">
<path <path
android:fillColor="#80ffffff" android:fillColor="#80ffffff"
android:pathData="M7,7h10v3l4,-4 -4,-4v3L5,5v6h2L7,7zM17,17L7,17v-3l-4,4 4,4v-3h12v-6h-2v4z"/> android:pathData="M7,7h10v3l4,-4 -4,-4v3L5,5v6h2L7,7zM17,17L7,17v-3l-4,4 4,4v-3h12v-6h-2v4z" />
</vector> </vector>

View file

@ -5,7 +5,7 @@
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24" android:viewportHeight="24"
android:tint="?attr/colorControlNormal"> android:tint="?attr/colorControlNormal">
<path <path
android:fillColor="@android:color/white" android:fillColor="@android:color/white"
android:pathData="M7,7h10v3l4,-4 -4,-4v3L5,5v6h2L7,7zM17,17L7,17v-3l-4,4 4,4v-3h12v-6h-2v4z"/> android:pathData="M7,7h10v3l4,-4 -4,-4v3L5,5v6h2L7,7zM17,17L7,17v-3l-4,4 4,4v-3h12v-6h-2v4z" />
</vector> </vector>

View file

@ -5,7 +5,7 @@
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24" android:viewportHeight="24"
android:tint="?attr/colorControlNormal"> android:tint="?attr/colorControlNormal">
<path <path
android:fillColor="@android:color/white" android:fillColor="@android:color/white"
android:pathData="M7,7h10v3l4,-4 -4,-4v3L5,5v6h2L7,7zM17,17L7,17v-3l-4,4 4,4v-3h12v-6h-2v4zM13,15L13,9h-1l-2,1v1h1.5v4L13,15z"/> android:pathData="M7,7h10v3l4,-4 -4,-4v3L5,5v6h2L7,7zM17,17L7,17v-3l-4,4 4,4v-3h12v-6h-2v4zM13,15L13,9h-1l-2,1v1h1.5v4L13,15z" />
</vector> </vector>

View file

@ -5,7 +5,7 @@
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24" android:viewportHeight="24"
android:tint="?attr/colorControlNormal"> android:tint="?attr/colorControlNormal">
<path <path
android:fillColor="@android:color/white" android:fillColor="@android:color/white"
android:pathData="M7,7h10v3l4,-4 -4,-4v3L5,5v6h2L7,7zM17,17L7,17v-3l-4,4 4,4v-3h12v-6h-2v4zM13,15L13,9h-1l-2,1v1h1.5v4L13,15z"/> android:pathData="M7,7h10v3l4,-4 -4,-4v3L5,5v6h2L7,7zM17,17L7,17v-3l-4,4 4,4v-3h12v-6h-2v4zM13,15L13,9h-1l-2,1v1h1.5v4L13,15z" />
</vector> </vector>

View file

@ -5,6 +5,7 @@
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24" android:viewportHeight="24"
android:tint="?android:attr/colorControlNormal"> android:tint="?android:attr/colorControlNormal">
<path android:fillColor="@android:color/white" <path
android:pathData="M5.571 19.5h4.286v-15H5.571zm8.572-15v15h4.286v-15z"/> android:fillColor="@android:color/white"
android:pathData="M5.571 19.5h4.286v-15H5.571zm8.572-15v15h4.286v-15z" />
</vector> </vector>

View file

@ -5,6 +5,7 @@
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24" android:viewportHeight="24"
android:tint="?attr/colorControlNormal"> android:tint="?attr/colorControlNormal">
<path android:fillColor="@android:color/white" <path
android:pathData="M5.078 4.089V12h13.844zm0 15.822V12h13.844z"/> android:fillColor="@android:color/white"
android:pathData="M5.078 4.089V12h13.844zm0 15.822V12h13.844z" />
</vector> </vector>

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M4,6L2,6v14c0,1.1 0.9,2 2,2h14v-2L4,20L4,6zM20,2L8,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM19,11h-4v4h-2v-4L9,11L9,9h4L13,5h2v4h4v2z" />
</vector>

View file

@ -15,6 +15,7 @@
type="org.oxycblt.auxio.playback.PlaybackViewModel" /> type="org.oxycblt.auxio.playback.PlaybackViewModel" />
</data> </data>
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/playback_layout" android:id="@+id/playback_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -213,7 +214,7 @@
app:layout_constraintBottom_toBottomOf="@+id/playback_play_pause" app:layout_constraintBottom_toBottomOf="@+id/playback_play_pause"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/playback_play_pause" app:layout_constraintTop_toTopOf="@+id/playback_play_pause"
android:contentDescription="@string/description_loop"/> android:contentDescription="@string/description_loop" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</layout> </layout>

View file

@ -2,28 +2,15 @@
<layout xmlns:android="http://schemas.android.com/apk/res/android" <layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<LinearLayout
<FrameLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent">
android:orientation="vertical"
android:theme="@style/Theme.Base"
android:background="@color/background">
<TextView <androidx.viewpager2.widget.ViewPager2
android:id="@+id/queue_header" android:id="@+id/queue_viewpager"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent" />
android:padding="@dimen/padding_medium"
android:text="@string/label_queue"
android:fontFamily="@font/inter_black"
android:textAppearance="@style/TextAppearance.Toolbar.Header" />
<androidx.recyclerview.widget.RecyclerView </FrameLayout>
android:id="@+id/queue_recycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:listitem="@layout/item_song"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
</LinearLayout>
</layout> </layout>

View file

@ -0,0 +1,49 @@
<?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">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@color/background">
<TextView
android:id="@+id/queue_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:fontFamily="@font/inter_black"
android:marqueeRepeatLimit="marquee_forever"
android:padding="@dimen/padding_medium"
android:singleLine="true"
android:text="@string/label_queue"
android:textColor="?android:attr/colorPrimary"
android:textAppearance="@style/TextAppearance.Toolbar.Header"
app:layout_constraintEnd_toStartOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/queue_recycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintTop_toBottomOf="@+id/queue_header"
tools:layout_editor_absoluteX="0dp"
tools:listitem="@layout/item_song" />
<TextView
android:id="@+id/queue_nothing_indicator"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="@dimen/margin_medium"
android:textSize="15sp"
android:text="@string/label_empty_queue"
android:textAlignment="center"
android:visibility="gone" />
</LinearLayout>
</layout>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/action_queue_add"
android:title="@string/label_queue_add"
android:icon="@drawable/ic_user_queue" />
</menu>

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/> <background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground"/> <foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon> </adaptive-icon>

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/> <background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground"/> <foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon> </adaptive-icon>

View file

@ -115,14 +115,18 @@
android:id="@+id/playback_fragment" android:id="@+id/playback_fragment"
android:name="org.oxycblt.auxio.playback.PlaybackFragment" android:name="org.oxycblt.auxio.playback.PlaybackFragment"
android:label="PlaybackFragment" android:label="PlaybackFragment"
tools:layout="@layout/fragment_playback" > tools:layout="@layout/fragment_playback">
<action <action
android:id="@+id/action_show_queue" android:id="@+id/action_show_queue"
app:destination="@id/queue_fragment" /> app:destination="@id/queue_fragment"
app:enterAnim="@anim/anim_nav_slide_up"
app:exitAnim="@anim/anim_stationary"
app:popEnterAnim="@anim/anim_stationary"
app:popExitAnim="@anim/anim_nav_slide_down" />
</fragment> </fragment>
<dialog <dialog
android:id="@+id/queue_fragment" android:id="@+id/queue_fragment"
android:name="org.oxycblt.auxio.playback.queue.QueueFragment" android:name="org.oxycblt.auxio.playback.queue.QueueFragment"
android:label="QueueFragment" android:label="QueueFragment"
tools:layout="@layout/fragment_queue"/> tools:layout="@layout/fragment_queue" />
</navigation> </navigation>

View file

@ -27,7 +27,9 @@
<string name="label_play">Play</string> <string name="label_play">Play</string>
<string name="label_queue">Queue</string> <string name="label_queue">Queue</string>
<string name="label_queue_add">Add to queue</string> <string name="label_queue_add">Add to queue</string>
<string name="label_next_user_queue">Next in Queue</string>
<string name="label_next_songs">Next from: All Songs</string> <string name="label_next_songs">Next from: All Songs</string>
<string name="label_empty_queue">Nothing here.</string>
<string name="label_notification_playback">Music Playback</string> <string name="label_notification_playback">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>

View file

@ -8,6 +8,7 @@
<item name="android:textCursorDrawable">@drawable/ui_cursor</item> <item name="android:textCursorDrawable">@drawable/ui_cursor</item>
<item name="android:fitsSystemWindows">true</item> <item name="android:fitsSystemWindows">true</item>
<item name="android:popupMenuStyle">@style/Widget.CustomPopup</item>
<item name="colorControlNormal">@color/control_color</item> <item name="colorControlNormal">@color/control_color</item>
</style> </style>
@ -41,6 +42,11 @@
<item name="colorControlHighlight">@color/selection_color</item> <item name="colorControlHighlight">@color/selection_color</item>
</style> </style>
<style name="Widget.CustomPopup" parent="Widget.AppCompat.PopupMenu">
<item name="android:colorBackground">@color/background</item>
<item name="android:popupBackground">@color/background</item>
</style>
<!-- <!--
Fix to get QueueFragment to not overlap the Status Bar or Navigation Bar Fix to get QueueFragment to not overlap the Status Bar or Navigation Bar
https://stackoverflow.com/a/57790787/14143986 https://stackoverflow.com/a/57790787/14143986