Unify Queue
Make QueueFragment contain both the user queue and the next queue, instead of having viewpager between the two.
This commit is contained in:
parent
b5552411b6
commit
67d10009d4
17 changed files with 189 additions and 302 deletions
|
@ -120,8 +120,6 @@ class AlbumDetailFragment : Fragment() {
|
|||
binding.albumArtist.setBackgroundResource(R.drawable.ui_ripple)
|
||||
}
|
||||
|
||||
// TODO: Make DetailFragment scroll to song if navigated from CompactPlaybackFragment
|
||||
|
||||
Log.d(this::class.simpleName, "Fragment created.")
|
||||
|
||||
return binding.root
|
||||
|
|
|
@ -85,7 +85,6 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
|
|||
true
|
||||
}
|
||||
|
||||
// TODO: Add icons to overflow menu items?
|
||||
menu.apply {
|
||||
val item = findItem(R.id.action_search)
|
||||
val searchView = item.actionView as SearchView
|
||||
|
@ -100,11 +99,6 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
|
|||
|
||||
item.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
|
||||
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
|
||||
// When opened, update the adapter to the SearchAdapter, and make the
|
||||
// sorting group invisible. The query is also reset, as if the Auxio process
|
||||
// is killed in the background while still on the search adapter, then the
|
||||
// search query will stick around if its opened again
|
||||
// TODO: Couldn't you just try to restore the search state on restart?
|
||||
binding.libraryRecycler.adapter = searchAdapter
|
||||
setGroupVisible(R.id.group_sorting, false)
|
||||
libraryModel.resetQuery()
|
||||
|
@ -113,8 +107,6 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
|
|||
}
|
||||
|
||||
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
|
||||
// When closed, make the sorting icon visible again, change back to
|
||||
// LibraryAdapter, and reset the query.
|
||||
binding.libraryRecycler.adapter = libraryAdapter
|
||||
setGroupVisible(R.id.group_sorting, true)
|
||||
libraryModel.resetQuery()
|
||||
|
@ -188,14 +180,12 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
|
|||
override fun onQueryTextSubmit(query: String): Boolean = false
|
||||
|
||||
override fun onQueryTextChange(query: String): Boolean {
|
||||
libraryModel.updateSearchQuery(query)
|
||||
libraryModel.updateSearchQuery(query, requireContext())
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private fun navToItem(baseModel: BaseModel) {
|
||||
// TODO: Implement shared element transitions to the DetailFragments [If possible]
|
||||
|
||||
// If the item is a song [That was selected through search], then update the playback
|
||||
// to that song instead of doing any navigation
|
||||
if (baseModel is Song) {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package org.oxycblt.auxio.library
|
||||
|
||||
import android.content.Context
|
||||
import android.view.MenuItem
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
|
@ -44,7 +45,7 @@ class LibraryViewModel : ViewModel() {
|
|||
}
|
||||
}
|
||||
|
||||
fun updateSearchQuery(query: String) {
|
||||
fun updateSearchQuery(query: String, context: Context) {
|
||||
// Don't bother if the query is blank.
|
||||
if (query == "") {
|
||||
resetQuery()
|
||||
|
@ -65,7 +66,7 @@ class LibraryViewModel : ViewModel() {
|
|||
val genres = musicStore.genres.filter { it.name.contains(query, true) }
|
||||
|
||||
if (genres.isNotEmpty()) {
|
||||
combined.add(Header(id = ShowMode.SHOW_GENRES.constant))
|
||||
combined.add(Header(name = context.getString(R.string.label_genres)))
|
||||
combined.addAll(genres)
|
||||
}
|
||||
}
|
||||
|
@ -74,7 +75,7 @@ class LibraryViewModel : ViewModel() {
|
|||
val artists = musicStore.artists.filter { it.name.contains(query, true) }
|
||||
|
||||
if (artists.isNotEmpty()) {
|
||||
combined.add(Header(id = ShowMode.SHOW_ARTISTS.constant))
|
||||
combined.add(Header(name = context.getString(R.string.label_artists)))
|
||||
combined.addAll(artists)
|
||||
}
|
||||
}
|
||||
|
@ -83,14 +84,14 @@ class LibraryViewModel : ViewModel() {
|
|||
val albums = musicStore.albums.filter { it.name.contains(query, true) }
|
||||
|
||||
if (albums.isNotEmpty()) {
|
||||
combined.add(Header(id = ShowMode.SHOW_ALBUMS.constant))
|
||||
combined.add(Header(name = context.getString(R.string.label_albums)))
|
||||
combined.addAll(albums)
|
||||
}
|
||||
|
||||
val songs = musicStore.songs.filter { it.name.contains(query, true) }
|
||||
|
||||
if (songs.isNotEmpty()) {
|
||||
combined.add(Header(id = ShowMode.SHOW_SONGS.constant))
|
||||
combined.add(Header(name = context.getString(R.string.label_songs)))
|
||||
combined.addAll(songs)
|
||||
}
|
||||
|
||||
|
|
|
@ -8,7 +8,6 @@ import android.text.format.DateUtils
|
|||
import android.widget.TextView
|
||||
import androidx.databinding.BindingAdapter
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.recycler.ShowMode
|
||||
|
||||
// List of ID3 genres + Winamp extensions, each index corresponds to their int value.
|
||||
// There are a lot more int-genre extensions as far as Im aware, but this works for most cases.
|
||||
|
@ -158,18 +157,3 @@ fun TextView.bindAlbumInfo(album: Album) {
|
|||
fun TextView.bindAlbumYear(album: Album) {
|
||||
text = album.year.toYear(context)
|
||||
}
|
||||
|
||||
// Bind the text used by the header item
|
||||
@BindingAdapter("headerText")
|
||||
fun TextView.bindHeaderText(header: Header) {
|
||||
text = context.getString(
|
||||
when (header.id) {
|
||||
ShowMode.SHOW_GENRES.constant -> R.string.label_genres
|
||||
ShowMode.SHOW_ARTISTS.constant -> R.string.label_artists
|
||||
ShowMode.SHOW_ALBUMS.constant -> R.string.label_albums
|
||||
ShowMode.SHOW_SONGS.constant -> R.string.label_songs
|
||||
|
||||
else -> R.string.label_artists
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -9,7 +9,6 @@ import android.content.Intent
|
|||
import android.content.IntentFilter
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.media.AudioManager
|
||||
import android.os.Binder
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.os.Parcelable
|
||||
|
@ -71,7 +70,7 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca
|
|||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent): IBinder? = LocalBinder()
|
||||
override fun onBind(intent: Intent): IBinder? = null
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
@ -295,9 +294,6 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca
|
|||
}
|
||||
}
|
||||
|
||||
// Awful Hack to get position polling to work, as exoplayer does not provide any
|
||||
// onPositionChanged callback for some inane reason.
|
||||
// TODO: MediaSession might have a callback for positions. Idk.
|
||||
private fun pollCurrentPosition() = flow {
|
||||
while (player.isPlaying) {
|
||||
emit(player.currentPosition)
|
||||
|
@ -435,10 +431,6 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca
|
|||
}
|
||||
}
|
||||
|
||||
inner class LocalBinder : Binder() {
|
||||
fun getService() = this@PlaybackService
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val DISCONNECTED = 0
|
||||
private const val CONNECTED = 1
|
||||
|
|
|
@ -153,39 +153,61 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
|||
playbackManager.prev()
|
||||
}
|
||||
|
||||
// Remove a queue item, given a QueueAdapter index.
|
||||
// Remove a queue OR user queue item, given a QueueAdapter index.
|
||||
fun removeQueueItem(adapterIndex: Int) {
|
||||
// Translate the adapter indices into the correct queue indices
|
||||
val delta = mQueue.value!!.size - nextItemsInQueue.value!!.size
|
||||
var index = adapterIndex.dec()
|
||||
|
||||
val index = adapterIndex + delta
|
||||
// If the item is in the user queue, then remove it from there after accounting for the header.
|
||||
if (index < mUserQueue.value!!.size) {
|
||||
playbackManager.removeUserQueueItem(index)
|
||||
} else {
|
||||
// Translate the indices into proper queue indices if removing an item from there.
|
||||
index += (mQueue.value!!.size - nextItemsInQueue.value!!.size)
|
||||
|
||||
playbackManager.removeQueueItem(index)
|
||||
if (userQueue.value!!.isNotEmpty()) {
|
||||
index -= mUserQueue.value!!.size.inc()
|
||||
}
|
||||
|
||||
playbackManager.removeQueueItem(index)
|
||||
}
|
||||
}
|
||||
|
||||
// Move queue items, given QueueAdapter indices.
|
||||
fun moveQueueItems(adapterFrom: Int, adapterTo: Int) {
|
||||
// Translate the adapter indices into the correct queue indices
|
||||
val delta = mQueue.value!!.size - nextItemsInQueue.value!!.size
|
||||
// Move queue OR user queue items, given QueueAdapter indices.
|
||||
// I have no idea what is going on in this function, but it works, so
|
||||
fun moveQueueItems(adapterFrom: Int, adapterTo: Int): Boolean {
|
||||
var from = adapterFrom.dec()
|
||||
var to = adapterTo.dec()
|
||||
|
||||
val from = adapterFrom + delta
|
||||
val to = adapterTo + delta
|
||||
if (from < mUserQueue.value!!.size) {
|
||||
|
||||
playbackManager.moveQueueItems(from, to)
|
||||
if (to >= mUserQueue.value!!.size || to < 0) return false
|
||||
|
||||
playbackManager.moveUserQueueItems(from, to)
|
||||
} else {
|
||||
if (to < 0) return false
|
||||
|
||||
val delta = mQueue.value!!.size - nextItemsInQueue.value!!.size
|
||||
|
||||
from += delta
|
||||
to += delta
|
||||
|
||||
if (userQueue.value!!.isNotEmpty()) {
|
||||
if (to <= mUserQueue.value!!.size.inc()) return false
|
||||
|
||||
from -= mUserQueue.value!!.size.inc()
|
||||
to -= mUserQueue.value!!.size.inc()
|
||||
}
|
||||
|
||||
playbackManager.moveQueueItems(from, to)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
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 ---
|
||||
|
||||
// Flip the playing status.
|
||||
|
|
|
@ -1,25 +1,53 @@
|
|||
package org.oxycblt.auxio.playback.queue
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.ViewGroup
|
||||
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
|
||||
import org.oxycblt.auxio.music.Header
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.recycler.DiffCallback
|
||||
import org.oxycblt.auxio.recycler.viewholders.BaseViewHolder
|
||||
import org.oxycblt.auxio.recycler.viewholders.HeaderViewHolder
|
||||
|
||||
class QueueAdapter(
|
||||
val touchHelper: ItemTouchHelper
|
||||
) : ListAdapter<Song, QueueAdapter.ViewHolder>(DiffCallback<Song>()) {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
return ViewHolder(ItemQueueSongBinding.inflate(LayoutInflater.from(parent.context)))
|
||||
) : ListAdapter<BaseModel, RecyclerView.ViewHolder>(DiffCallback<BaseModel>()) {
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
val item = getItem(position)
|
||||
|
||||
if (item is Header) {
|
||||
return HeaderViewHolder.ITEM_TYPE
|
||||
} else {
|
||||
return QUEUE_ITEM_VIEW_TYPE
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
holder.bind(getItem(position))
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return when (viewType) {
|
||||
HeaderViewHolder.ITEM_TYPE -> HeaderViewHolder.from(parent.context)
|
||||
QUEUE_ITEM_VIEW_TYPE -> ViewHolder(
|
||||
ItemQueueSongBinding.inflate(LayoutInflater.from(parent.context))
|
||||
)
|
||||
else -> error("Someone messed with the ViewHolder item types. Tell OxygenCobalt.")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
when (val item = getItem(position)) {
|
||||
is Song -> (holder as ViewHolder).bind(item)
|
||||
is Header -> (holder as HeaderViewHolder).bind(item)
|
||||
|
||||
else -> {
|
||||
Log.d(this::class.simpleName, "Bad data fed to QueueAdapter.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generic ViewHolder for a queue item
|
||||
|
@ -46,4 +74,8 @@ class QueueAdapter(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val QUEUE_ITEM_VIEW_TYPE = 0xA030
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,13 +9,22 @@ import kotlin.math.min
|
|||
import kotlin.math.sign
|
||||
|
||||
// The drag callback used for the Queue RecyclerView.
|
||||
class QueueDragCallback(
|
||||
private val playbackModel: PlaybackViewModel,
|
||||
private val isUserQueue: Boolean
|
||||
) : ItemTouchHelper.SimpleCallback(
|
||||
ItemTouchHelper.UP or ItemTouchHelper.DOWN,
|
||||
ItemTouchHelper.START
|
||||
) {
|
||||
class QueueDragCallback(private val playbackModel: PlaybackViewModel) : ItemTouchHelper.Callback() {
|
||||
override fun getMovementFlags(
|
||||
recyclerView: RecyclerView,
|
||||
viewHolder: RecyclerView.ViewHolder
|
||||
): Int {
|
||||
// Make header objects unswipable by only returning the swipe flags if the ViewHolder
|
||||
// is for a queue item.
|
||||
return if (viewHolder is QueueAdapter.ViewHolder) {
|
||||
makeFlag(
|
||||
ItemTouchHelper.ACTION_STATE_DRAG, ItemTouchHelper.UP or ItemTouchHelper.DOWN
|
||||
) or makeFlag(ItemTouchHelper.ACTION_STATE_SWIPE, ItemTouchHelper.START)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
override fun interpolateOutOfBoundsScroll(
|
||||
recyclerView: RecyclerView,
|
||||
viewSize: Int,
|
||||
|
@ -45,21 +54,11 @@ class QueueDragCallback(
|
|||
viewHolder: RecyclerView.ViewHolder,
|
||||
target: RecyclerView.ViewHolder
|
||||
): Boolean {
|
||||
if (isUserQueue) {
|
||||
playbackModel.moveUserQueueItems(viewHolder.adapterPosition, target.adapterPosition)
|
||||
} else {
|
||||
playbackModel.moveQueueItems(viewHolder.adapterPosition, target.adapterPosition)
|
||||
}
|
||||
|
||||
return true
|
||||
return playbackModel.moveQueueItems(viewHolder.adapterPosition, target.adapterPosition)
|
||||
}
|
||||
|
||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
||||
if (isUserQueue) {
|
||||
playbackModel.removeUserQueueItem(viewHolder.adapterPosition)
|
||||
} else {
|
||||
playbackModel.removeQueueItem(viewHolder.adapterPosition)
|
||||
}
|
||||
playbackModel.removeQueueItem(viewHolder.adapterPosition)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -7,11 +7,16 @@ import android.view.ViewGroup
|
|||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
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
|
||||
import org.oxycblt.auxio.music.Header
|
||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||
import org.oxycblt.auxio.playback.state.PlaybackMode
|
||||
import org.oxycblt.auxio.ui.applyDivider
|
||||
|
||||
// TODO: Make this better
|
||||
class QueueFragment : Fragment() {
|
||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||
|
||||
|
@ -22,28 +27,69 @@ class QueueFragment : Fragment() {
|
|||
): View? {
|
||||
val binding = FragmentQueueBinding.inflate(inflater)
|
||||
|
||||
val helper = ItemTouchHelper(QueueDragCallback(playbackModel))
|
||||
val queueAdapter = QueueAdapter(helper)
|
||||
|
||||
// --- UI SETUP ---
|
||||
|
||||
binding.queueToolbar.setNavigationOnClickListener {
|
||||
findNavController().navigateUp()
|
||||
}
|
||||
|
||||
binding.queueViewpager.adapter = PagerAdapter()
|
||||
binding.queueRecycler.apply {
|
||||
setHasFixedSize(true)
|
||||
applyDivider()
|
||||
adapter = queueAdapter
|
||||
helper.attachToRecyclerView(this)
|
||||
}
|
||||
|
||||
// TODO: Add option for default queue screen
|
||||
if (playbackModel.userQueue.value!!.isNotEmpty()) {
|
||||
binding.queueViewpager.setCurrentItem(0, false)
|
||||
} else {
|
||||
binding.queueViewpager.setCurrentItem(1, false)
|
||||
playbackModel.userQueue.observe(viewLifecycleOwner) {
|
||||
queueAdapter.submitList(createQueueDisplay()) {
|
||||
binding.queueRecycler.scrollToPosition(0)
|
||||
scrollRecyclerIfNeeded(binding)
|
||||
}
|
||||
}
|
||||
|
||||
playbackModel.nextItemsInQueue.observe(viewLifecycleOwner) {
|
||||
queueAdapter.submitList(createQueueDisplay()) {
|
||||
scrollRecyclerIfNeeded(binding)
|
||||
}
|
||||
}
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
private inner class PagerAdapter :
|
||||
FragmentStateAdapter(childFragmentManager, viewLifecycleOwner.lifecycle) {
|
||||
override fun getItemCount(): Int = 2
|
||||
private fun createQueueDisplay(): MutableList<BaseModel> {
|
||||
val queue = mutableListOf<BaseModel>()
|
||||
|
||||
override fun createFragment(position: Int): Fragment {
|
||||
return QueueListFragment(position)
|
||||
if (playbackModel.userQueue.value!!.isNotEmpty()) {
|
||||
queue.add(Header(name = getString(R.string.label_next_user_queue)))
|
||||
queue.addAll(playbackModel.userQueue.value!!)
|
||||
}
|
||||
|
||||
if (playbackModel.nextItemsInQueue.value!!.isNotEmpty()) {
|
||||
queue.add(
|
||||
Header(
|
||||
name = getString(
|
||||
R.string.format_next_from,
|
||||
if (playbackModel.mode.value == PlaybackMode.ALL_SONGS)
|
||||
getString(R.string.title_all_songs)
|
||||
else
|
||||
playbackModel.parent.value!!.name
|
||||
)
|
||||
)
|
||||
)
|
||||
queue.addAll(playbackModel.nextItemsInQueue.value!!)
|
||||
}
|
||||
|
||||
return queue
|
||||
}
|
||||
|
||||
private fun scrollRecyclerIfNeeded(binding: FragmentQueueBinding) {
|
||||
if ((binding.queueRecycler.layoutManager as LinearLayoutManager)
|
||||
.findFirstVisibleItemPosition() < 1
|
||||
) {
|
||||
binding.queueRecycler.scrollToPosition(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,145 +0,0 @@
|
|||
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.ui.applyDivider
|
||||
|
||||
// TODO: Unify the user/next queues into a single fragment
|
||||
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) {
|
||||
binding.queueHeader.text = getString(
|
||||
R.string.format_next_from,
|
||||
if (it == PlaybackMode.ALL_SONGS) getString(R.string.title_all_songs)
|
||||
else 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
|
||||
}
|
||||
}
|
|
@ -42,7 +42,6 @@ class PlaybackStateManager private constructor() {
|
|||
}
|
||||
private var mUserQueue = mutableListOf<Song>()
|
||||
set(value) {
|
||||
Log.d(this::class.simpleName, "retard.")
|
||||
field = value
|
||||
callbacks.forEach { it.onUserQueueUpdate(value) }
|
||||
}
|
||||
|
@ -174,7 +173,8 @@ class PlaybackStateManager private constructor() {
|
|||
mMode = PlaybackMode.IN_GENRE
|
||||
}
|
||||
|
||||
else -> error("what")
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
|
||||
resetLoopMode()
|
||||
|
|
|
@ -27,7 +27,7 @@ class SongsFragment : Fragment() {
|
|||
|
||||
val musicStore = MusicStore.getInstance()
|
||||
|
||||
// TODO: Add option to search songs if LibraryFragment isn't enabled
|
||||
// TODO: Add option to search songs [Or just make a dedicated tab]
|
||||
// TODO: Fast scrolling?
|
||||
|
||||
// --- UI SETUP ---
|
||||
|
|
|
@ -26,7 +26,6 @@ fun showActionMenuForSong(
|
|||
view: View,
|
||||
playbackModel: PlaybackViewModel
|
||||
) {
|
||||
// TODO: Replace this with a BottomSheet dialog?
|
||||
PopupMenu(context, view).apply {
|
||||
inflate(R.menu.menu_song_actions)
|
||||
setOnMenuItemClickListener {
|
||||
|
|
|
@ -37,6 +37,7 @@
|
|||
app:title="@string/title_library_fragment" />
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:id="@+id/nested_scroll"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
|
|
|
@ -6,7 +6,9 @@
|
|||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
android:orientation="vertical"
|
||||
android:background="@color/background"
|
||||
android:animateLayoutChanges="true">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/queue_toolbar"
|
||||
|
@ -22,10 +24,24 @@
|
|||
app:title="@string/label_queue"
|
||||
app:titleTextAppearance="@style/TextAppearance.Toolbar.Header" />
|
||||
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/queue_viewpager"
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/queue_recycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="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>
|
|
@ -1,48 +0,0 @@
|
|||
<?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"
|
||||
android:animateLayoutChanges="true">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/queue_header"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/ui_header_dividers"
|
||||
android:fontFamily="@font/inter_bold"
|
||||
android:paddingStart="@dimen/padding_medium"
|
||||
android:paddingTop="@dimen/padding_small"
|
||||
android:paddingEnd="@dimen/padding_small"
|
||||
android:paddingBottom="@dimen/padding_small"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Overline"
|
||||
android:textSize="16sp"
|
||||
tools:text="Next in Queue"
|
||||
app:layout_constraintTop_toBottomOf="@+id/album_details" />
|
||||
|
||||
<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>
|
|
@ -27,7 +27,7 @@
|
|||
android:paddingBottom="@dimen/padding_small"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Overline"
|
||||
android:textSize="16sp"
|
||||
app:headerText="@{header}"
|
||||
android:text="@{header.name}"
|
||||
tools:text="Songs" />
|
||||
|
||||
</FrameLayout>
|
||||
|
|
Loading…
Reference in a new issue