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:
OxygenCobalt 2020-11-09 15:39:13 -07:00
parent b5552411b6
commit 67d10009d4
17 changed files with 189 additions and 302 deletions

View file

@ -120,8 +120,6 @@ class AlbumDetailFragment : Fragment() {
binding.albumArtist.setBackgroundResource(R.drawable.ui_ripple) binding.albumArtist.setBackgroundResource(R.drawable.ui_ripple)
} }
// TODO: Make DetailFragment scroll to song if navigated from CompactPlaybackFragment
Log.d(this::class.simpleName, "Fragment created.") Log.d(this::class.simpleName, "Fragment created.")
return binding.root return binding.root

View file

@ -85,7 +85,6 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
true true
} }
// TODO: Add icons to overflow menu items?
menu.apply { menu.apply {
val item = findItem(R.id.action_search) val item = findItem(R.id.action_search)
val searchView = item.actionView as SearchView val searchView = item.actionView as SearchView
@ -100,11 +99,6 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
item.setOnActionExpandListener(object : MenuItem.OnActionExpandListener { item.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(item: MenuItem): Boolean { 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 binding.libraryRecycler.adapter = searchAdapter
setGroupVisible(R.id.group_sorting, false) setGroupVisible(R.id.group_sorting, false)
libraryModel.resetQuery() libraryModel.resetQuery()
@ -113,8 +107,6 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
} }
override fun onMenuItemActionCollapse(item: MenuItem): Boolean { 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 binding.libraryRecycler.adapter = libraryAdapter
setGroupVisible(R.id.group_sorting, true) setGroupVisible(R.id.group_sorting, true)
libraryModel.resetQuery() libraryModel.resetQuery()
@ -188,14 +180,12 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean = false override fun onQueryTextSubmit(query: String): Boolean = false
override fun onQueryTextChange(query: String): Boolean { override fun onQueryTextChange(query: String): Boolean {
libraryModel.updateSearchQuery(query) libraryModel.updateSearchQuery(query, requireContext())
return false return false
} }
private fun navToItem(baseModel: BaseModel) { 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 // If the item is a song [That was selected through search], then update the playback
// to that song instead of doing any navigation // to that song instead of doing any navigation
if (baseModel is Song) { if (baseModel is Song) {

View file

@ -1,5 +1,6 @@
package org.oxycblt.auxio.library package org.oxycblt.auxio.library
import android.content.Context
import android.view.MenuItem import android.view.MenuItem
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData 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. // Don't bother if the query is blank.
if (query == "") { if (query == "") {
resetQuery() resetQuery()
@ -65,7 +66,7 @@ class LibraryViewModel : ViewModel() {
val genres = musicStore.genres.filter { it.name.contains(query, true) } val genres = musicStore.genres.filter { it.name.contains(query, true) }
if (genres.isNotEmpty()) { if (genres.isNotEmpty()) {
combined.add(Header(id = ShowMode.SHOW_GENRES.constant)) combined.add(Header(name = context.getString(R.string.label_genres)))
combined.addAll(genres) combined.addAll(genres)
} }
} }
@ -74,7 +75,7 @@ class LibraryViewModel : ViewModel() {
val artists = musicStore.artists.filter { it.name.contains(query, true) } val artists = musicStore.artists.filter { it.name.contains(query, true) }
if (artists.isNotEmpty()) { if (artists.isNotEmpty()) {
combined.add(Header(id = ShowMode.SHOW_ARTISTS.constant)) combined.add(Header(name = context.getString(R.string.label_artists)))
combined.addAll(artists) combined.addAll(artists)
} }
} }
@ -83,14 +84,14 @@ class LibraryViewModel : ViewModel() {
val albums = musicStore.albums.filter { it.name.contains(query, true) } val albums = musicStore.albums.filter { it.name.contains(query, true) }
if (albums.isNotEmpty()) { if (albums.isNotEmpty()) {
combined.add(Header(id = ShowMode.SHOW_ALBUMS.constant)) combined.add(Header(name = context.getString(R.string.label_albums)))
combined.addAll(albums) combined.addAll(albums)
} }
val songs = musicStore.songs.filter { it.name.contains(query, true) } val songs = musicStore.songs.filter { it.name.contains(query, true) }
if (songs.isNotEmpty()) { if (songs.isNotEmpty()) {
combined.add(Header(id = ShowMode.SHOW_SONGS.constant)) combined.add(Header(name = context.getString(R.string.label_songs)))
combined.addAll(songs) combined.addAll(songs)
} }

View file

@ -8,7 +8,6 @@ import android.text.format.DateUtils
import android.widget.TextView import android.widget.TextView
import androidx.databinding.BindingAdapter import androidx.databinding.BindingAdapter
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.recycler.ShowMode
// List of ID3 genres + Winamp extensions, each index corresponds to their int value. // 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. // 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) { fun TextView.bindAlbumYear(album: Album) {
text = album.year.toYear(context) 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
}
)
}

View file

@ -9,7 +9,6 @@ import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.content.pm.ServiceInfo import android.content.pm.ServiceInfo
import android.media.AudioManager import android.media.AudioManager
import android.os.Binder
import android.os.Build import android.os.Build
import android.os.IBinder import android.os.IBinder
import android.os.Parcelable import android.os.Parcelable
@ -71,7 +70,7 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca
return START_NOT_STICKY return START_NOT_STICKY
} }
override fun onBind(intent: Intent): IBinder? = LocalBinder() override fun onBind(intent: Intent): IBinder? = null
override fun onCreate() { override fun onCreate() {
super.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 { private fun pollCurrentPosition() = flow {
while (player.isPlaying) { while (player.isPlaying) {
emit(player.currentPosition) emit(player.currentPosition)
@ -435,10 +431,6 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca
} }
} }
inner class LocalBinder : Binder() {
fun getService() = this@PlaybackService
}
companion object { companion object {
private const val DISCONNECTED = 0 private const val DISCONNECTED = 0
private const val CONNECTED = 1 private const val CONNECTED = 1

View file

@ -153,39 +153,61 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
playbackManager.prev() 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) { fun removeQueueItem(adapterIndex: Int) {
// Translate the adapter indices into the correct queue indices var index = adapterIndex.dec()
val delta = mQueue.value!!.size - nextItemsInQueue.value!!.size
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)
if (userQueue.value!!.isNotEmpty()) {
index -= mUserQueue.value!!.size.inc()
}
playbackManager.removeQueueItem(index) playbackManager.removeQueueItem(index)
} }
}
// 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()
if (from < mUserQueue.value!!.size) {
if (to >= mUserQueue.value!!.size || to < 0) return false
playbackManager.moveUserQueueItems(from, to)
} else {
if (to < 0) return false
// 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 val delta = mQueue.value!!.size - nextItemsInQueue.value!!.size
val from = adapterFrom + delta from += delta
val to = adapterTo + 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) playbackManager.moveQueueItems(from, to)
} }
return true
}
fun addToUserQueue(song: Song) { fun addToUserQueue(song: Song) {
playbackManager.addToUserQueue(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.

View file

@ -1,25 +1,53 @@
package org.oxycblt.auxio.playback.queue package org.oxycblt.auxio.playback.queue
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MotionEvent import android.view.MotionEvent
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.databinding.ItemQueueSongBinding 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.music.Song
import org.oxycblt.auxio.recycler.DiffCallback import org.oxycblt.auxio.recycler.DiffCallback
import org.oxycblt.auxio.recycler.viewholders.BaseViewHolder import org.oxycblt.auxio.recycler.viewholders.BaseViewHolder
import org.oxycblt.auxio.recycler.viewholders.HeaderViewHolder
class QueueAdapter( class QueueAdapter(
val touchHelper: ItemTouchHelper val touchHelper: ItemTouchHelper
) : ListAdapter<Song, QueueAdapter.ViewHolder>(DiffCallback<Song>()) { ) : ListAdapter<BaseModel, RecyclerView.ViewHolder>(DiffCallback<BaseModel>()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun getItemViewType(position: Int): Int {
return ViewHolder(ItemQueueSongBinding.inflate(LayoutInflater.from(parent.context))) 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) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
holder.bind(getItem(position)) 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 // Generic ViewHolder for a queue item
@ -46,4 +74,8 @@ class QueueAdapter(
} }
} }
} }
companion object {
const val QUEUE_ITEM_VIEW_TYPE = 0xA030
}
} }

View file

@ -9,13 +9,22 @@ import kotlin.math.min
import kotlin.math.sign 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) : ItemTouchHelper.Callback() {
private val playbackModel: PlaybackViewModel, override fun getMovementFlags(
private val isUserQueue: Boolean recyclerView: RecyclerView,
) : ItemTouchHelper.SimpleCallback( viewHolder: RecyclerView.ViewHolder
ItemTouchHelper.UP or ItemTouchHelper.DOWN, ): Int {
ItemTouchHelper.START // 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( override fun interpolateOutOfBoundsScroll(
recyclerView: RecyclerView, recyclerView: RecyclerView,
viewSize: Int, viewSize: Int,
@ -45,22 +54,12 @@ class QueueDragCallback(
viewHolder: RecyclerView.ViewHolder, viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder target: RecyclerView.ViewHolder
): Boolean { ): Boolean {
if (isUserQueue) { return playbackModel.moveQueueItems(viewHolder.adapterPosition, target.adapterPosition)
playbackModel.moveUserQueueItems(viewHolder.adapterPosition, target.adapterPosition)
} else {
playbackModel.moveQueueItems(viewHolder.adapterPosition, target.adapterPosition)
}
return true
} }
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { 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 { companion object {
const val MINIMUM_INITIAL_DRAG_VELOCITY = 10 const val MINIMUM_INITIAL_DRAG_VELOCITY = 10

View file

@ -7,11 +7,16 @@ import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController 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.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.PlaybackViewModel
import org.oxycblt.auxio.playback.state.PlaybackMode
import org.oxycblt.auxio.ui.applyDivider
// TODO: Make this better
class QueueFragment : Fragment() { class QueueFragment : Fragment() {
private val playbackModel: PlaybackViewModel by activityViewModels() private val playbackModel: PlaybackViewModel by activityViewModels()
@ -22,28 +27,69 @@ class QueueFragment : Fragment() {
): View? { ): View? {
val binding = FragmentQueueBinding.inflate(inflater) val binding = FragmentQueueBinding.inflate(inflater)
val helper = ItemTouchHelper(QueueDragCallback(playbackModel))
val queueAdapter = QueueAdapter(helper)
// --- UI SETUP ---
binding.queueToolbar.setNavigationOnClickListener { binding.queueToolbar.setNavigationOnClickListener {
findNavController().navigateUp() findNavController().navigateUp()
} }
binding.queueViewpager.adapter = PagerAdapter() binding.queueRecycler.apply {
setHasFixedSize(true)
applyDivider()
adapter = queueAdapter
helper.attachToRecyclerView(this)
}
// TODO: Add option for default queue screen playbackModel.userQueue.observe(viewLifecycleOwner) {
if (playbackModel.userQueue.value!!.isNotEmpty()) { queueAdapter.submitList(createQueueDisplay()) {
binding.queueViewpager.setCurrentItem(0, false) binding.queueRecycler.scrollToPosition(0)
} else { scrollRecyclerIfNeeded(binding)
binding.queueViewpager.setCurrentItem(1, false) }
}
playbackModel.nextItemsInQueue.observe(viewLifecycleOwner) {
queueAdapter.submitList(createQueueDisplay()) {
scrollRecyclerIfNeeded(binding)
}
} }
return binding.root return binding.root
} }
private inner class PagerAdapter : private fun createQueueDisplay(): MutableList<BaseModel> {
FragmentStateAdapter(childFragmentManager, viewLifecycleOwner.lifecycle) { val queue = mutableListOf<BaseModel>()
override fun getItemCount(): Int = 2
override fun createFragment(position: Int): Fragment { if (playbackModel.userQueue.value!!.isNotEmpty()) {
return QueueListFragment(position) 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)
} }
} }
} }

View file

@ -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
}
}

View file

@ -42,7 +42,6 @@ class PlaybackStateManager private constructor() {
} }
private var mUserQueue = mutableListOf<Song>() private var mUserQueue = mutableListOf<Song>()
set(value) { set(value) {
Log.d(this::class.simpleName, "retard.")
field = value field = value
callbacks.forEach { it.onUserQueueUpdate(value) } callbacks.forEach { it.onUserQueueUpdate(value) }
} }
@ -174,7 +173,8 @@ class PlaybackStateManager private constructor() {
mMode = PlaybackMode.IN_GENRE mMode = PlaybackMode.IN_GENRE
} }
else -> error("what") else -> {
}
} }
resetLoopMode() resetLoopMode()

View file

@ -27,7 +27,7 @@ class SongsFragment : Fragment() {
val musicStore = MusicStore.getInstance() 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? // TODO: Fast scrolling?
// --- UI SETUP --- // --- UI SETUP ---

View file

@ -26,7 +26,6 @@ fun showActionMenuForSong(
view: View, view: View,
playbackModel: PlaybackViewModel playbackModel: PlaybackViewModel
) { ) {
// TODO: Replace this with a BottomSheet dialog?
PopupMenu(context, view).apply { PopupMenu(context, view).apply {
inflate(R.menu.menu_song_actions) inflate(R.menu.menu_song_actions)
setOnMenuItemClickListener { setOnMenuItemClickListener {

View file

@ -37,6 +37,7 @@
app:title="@string/title_library_fragment" /> app:title="@string/title_library_fragment" />
<androidx.core.widget.NestedScrollView <androidx.core.widget.NestedScrollView
android:id="@+id/nested_scroll"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">

View file

@ -6,7 +6,9 @@
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"> android:orientation="vertical"
android:background="@color/background"
android:animateLayoutChanges="true">
<androidx.appcompat.widget.Toolbar <androidx.appcompat.widget.Toolbar
android:id="@+id/queue_toolbar" android:id="@+id/queue_toolbar"
@ -22,10 +24,24 @@
app:title="@string/label_queue" app:title="@string/label_queue"
app:titleTextAppearance="@style/TextAppearance.Toolbar.Header" /> app:titleTextAppearance="@style/TextAppearance.Toolbar.Header" />
<androidx.viewpager2.widget.ViewPager2 <androidx.recyclerview.widget.RecyclerView
android:id="@+id/queue_viewpager" android:id="@+id/queue_recycler"
android:layout_width="match_parent" 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> </LinearLayout>
</layout> </layout>

View file

@ -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>

View file

@ -27,7 +27,7 @@
android:paddingBottom="@dimen/padding_small" android:paddingBottom="@dimen/padding_small"
android:textAppearance="@style/TextAppearance.MaterialComponents.Overline" android:textAppearance="@style/TextAppearance.MaterialComponents.Overline"
android:textSize="16sp" android:textSize="16sp"
app:headerText="@{header}" android:text="@{header.name}"
tools:text="Songs" /> tools:text="Songs" />
</FrameLayout> </FrameLayout>