queue: redocument

Redocument the queue module.
This commit is contained in:
Alexander Capehart 2022-12-25 19:27:40 -07:00
parent b086c44b59
commit 7394e87471
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
24 changed files with 258 additions and 148 deletions

View file

@ -48,10 +48,12 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat
* *
* TODO: Migrate to material animation system * TODO: Migrate to material animation system
* *
* TODO: Re-document project
*
* TODO: Unit testing * TODO: Unit testing
* *
* TODO: Standardize from/new usage
*
* TODO: Standardize companion object usage
*
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {

View file

@ -27,7 +27,7 @@ import org.oxycblt.auxio.databinding.ItemAlbumSongBinding
import org.oxycblt.auxio.databinding.ItemDetailBinding import org.oxycblt.auxio.databinding.ItemDetailBinding
import org.oxycblt.auxio.databinding.ItemDiscHeaderBinding import org.oxycblt.auxio.databinding.ItemDiscHeaderBinding
import org.oxycblt.auxio.detail.DiscHeader import org.oxycblt.auxio.detail.DiscHeader
import org.oxycblt.auxio.list.ExtendedListListener import org.oxycblt.auxio.list.SelectableListListener
import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter
import org.oxycblt.auxio.list.recycler.SimpleItemCallback import org.oxycblt.auxio.list.recycler.SimpleItemCallback
@ -224,10 +224,10 @@ private class AlbumSongViewHolder private constructor(private val binding: ItemA
/** /**
* Bind new data to this instance. * Bind new data to this instance.
* @param song The new [Song] to bind. * @param song The new [Song] to bind.
* @param listener A [ExtendedListListener] to bind interactions to. * @param listener A [SelectableListListener] to bind interactions to.
*/ */
fun bind(song: Song, listener: ExtendedListListener) { fun bind(song: Song, listener: SelectableListListener) {
listener.bind(song, binding.root, binding.songMenu) listener.bind(this, song, binding.songMenu)
binding.songTrack.apply { binding.songTrack.apply {
if (song.track != null) { if (song.track != null) {

View file

@ -26,7 +26,7 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.ItemDetailBinding import org.oxycblt.auxio.databinding.ItemDetailBinding
import org.oxycblt.auxio.databinding.ItemParentBinding import org.oxycblt.auxio.databinding.ItemParentBinding
import org.oxycblt.auxio.databinding.ItemSongBinding import org.oxycblt.auxio.databinding.ItemSongBinding
import org.oxycblt.auxio.list.ExtendedListListener import org.oxycblt.auxio.list.SelectableListListener
import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter
import org.oxycblt.auxio.list.recycler.SimpleItemCallback import org.oxycblt.auxio.list.recycler.SimpleItemCallback
@ -181,10 +181,10 @@ private class ArtistAlbumViewHolder private constructor(private val binding: Ite
/** /**
* Bind new data to this instance. * Bind new data to this instance.
* @param album The new [Album] to bind. * @param album The new [Album] to bind.
* @param listener An [ExtendedListListener] to bind interactions to. * @param listener An [SelectableListListener] to bind interactions to.
*/ */
fun bind(album: Album, listener: ExtendedListListener) { fun bind(album: Album, listener: SelectableListListener) {
listener.bind(album, binding.root, binding.parentMenu) listener.bind(this, album, binding.parentMenu)
binding.parentImage.bind(album) binding.parentImage.bind(album)
binding.parentName.text = album.resolveName(binding.context) binding.parentName.text = album.resolveName(binding.context)
binding.parentInfo.text = binding.parentInfo.text =
@ -232,10 +232,10 @@ private class ArtistSongViewHolder private constructor(private val binding: Item
/** /**
* Bind new data to this instance. * Bind new data to this instance.
* @param song The new [Song] to bind. * @param song The new [Song] to bind.
* @param listener An [ExtendedListListener] to bind interactions to. * @param listener An [SelectableListListener] to bind interactions to.
*/ */
fun bind(song: Song, listener: ExtendedListListener) { fun bind(song: Song, listener: SelectableListListener) {
listener.bind(song, binding.root, binding.songMenu) listener.bind(this, song, binding.songMenu)
binding.songAlbumCover.bind(song) binding.songAlbumCover.bind(song)
binding.songName.text = song.resolveName(binding.context) binding.songName.text = song.resolveName(binding.context)
binding.songInfo.text = song.album.resolveName(binding.context) binding.songInfo.text = song.album.resolveName(binding.context)

View file

@ -26,7 +26,7 @@ import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.IntegerTable
import org.oxycblt.auxio.databinding.ItemSortHeaderBinding import org.oxycblt.auxio.databinding.ItemSortHeaderBinding
import org.oxycblt.auxio.detail.SortHeader import org.oxycblt.auxio.detail.SortHeader
import org.oxycblt.auxio.list.ExtendedListListener import org.oxycblt.auxio.list.SelectableListListener
import org.oxycblt.auxio.list.Header import org.oxycblt.auxio.list.Header
import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.list.recycler.* import org.oxycblt.auxio.list.recycler.*
@ -87,8 +87,8 @@ abstract class DetailAdapter(
differ.submitList(newList) differ.submitList(newList)
} }
/** An extended [ExtendedListListener] for [DetailAdapter] implementations. */ /** An extended [SelectableListListener] for [DetailAdapter] implementations. */
interface Listener : ExtendedListListener { interface Listener : SelectableListListener {
// TODO: Split off into sub-listeners if a collapsing toolbar is implemented. // TODO: Split off into sub-listeners if a collapsing toolbar is implemented.
/** /**
* Called when the play button in a detail header is pressed, requesting that the current * Called when the play button in a detail header is pressed, requesting that the current

View file

@ -139,9 +139,9 @@ class AlbumListFragment : ListFragment<FragmentHomeListBinding>(), FastScrollRec
/** /**
* A [SelectionIndicatorAdapter] that shows a list of [Album]s using [AlbumViewHolder]. * A [SelectionIndicatorAdapter] that shows a list of [Album]s using [AlbumViewHolder].
* @param listener An [ExtendedListListener] to bind interactions to. * @param listener An [SelectableListListener] to bind interactions to.
*/ */
private class AlbumAdapter(private val listener: ExtendedListListener) : private class AlbumAdapter(private val listener: SelectableListListener) :
SelectionIndicatorAdapter<AlbumViewHolder>() { SelectionIndicatorAdapter<AlbumViewHolder>() {
private val differ = SyncListDiffer(this, AlbumViewHolder.DIFF_CALLBACK) private val differ = SyncListDiffer(this, AlbumViewHolder.DIFF_CALLBACK)

View file

@ -114,9 +114,9 @@ class ArtistListFragment : ListFragment<FragmentHomeListBinding>(), FastScrollRe
/** /**
* A [SelectionIndicatorAdapter] that shows a list of [Artist]s using [ArtistViewHolder]. * A [SelectionIndicatorAdapter] that shows a list of [Artist]s using [ArtistViewHolder].
* @param listener An [ExtendedListListener] to bind interactions to. * @param listener An [SelectableListListener] to bind interactions to.
*/ */
private class ArtistAdapter(private val listener: ExtendedListListener) : private class ArtistAdapter(private val listener: SelectableListListener) :
SelectionIndicatorAdapter<ArtistViewHolder>() { SelectionIndicatorAdapter<ArtistViewHolder>() {
private val differ = SyncListDiffer(this, ArtistViewHolder.DIFF_CALLBACK) private val differ = SyncListDiffer(this, ArtistViewHolder.DIFF_CALLBACK)

View file

@ -113,9 +113,9 @@ class GenreListFragment : ListFragment<FragmentHomeListBinding>(), FastScrollRec
/** /**
* A [SelectionIndicatorAdapter] that shows a list of [Genre]s using [GenreViewHolder]. * A [SelectionIndicatorAdapter] that shows a list of [Genre]s using [GenreViewHolder].
* @param listener An [ExtendedListListener] to bind interactions to. * @param listener An [SelectableListListener] to bind interactions to.
*/ */
private class GenreAdapter(private val listener: ExtendedListListener) : private class GenreAdapter(private val listener: SelectableListListener) :
SelectionIndicatorAdapter<GenreViewHolder>() { SelectionIndicatorAdapter<GenreViewHolder>() {
private val differ = SyncListDiffer(this, GenreViewHolder.DIFF_CALLBACK) private val differ = SyncListDiffer(this, GenreViewHolder.DIFF_CALLBACK)

View file

@ -153,9 +153,9 @@ class SongListFragment : ListFragment<FragmentHomeListBinding>(), FastScrollRecy
/** /**
* A [SelectionIndicatorAdapter] that shows a list of [Song]s using [SongViewHolder]. * A [SelectionIndicatorAdapter] that shows a list of [Song]s using [SongViewHolder].
* @param listener An [ExtendedListListener] to bind interactions to. * @param listener An [SelectableListListener] to bind interactions to.
*/ */
private class SongAdapter(private val listener: ExtendedListListener) : private class SongAdapter(private val listener: SelectableListListener) :
SelectionIndicatorAdapter<SongViewHolder>() { SelectionIndicatorAdapter<SongViewHolder>() {
private val differ = SyncListDiffer(this, SongViewHolder.DIFF_CALLBACK) private val differ = SyncListDiffer(this, SongViewHolder.DIFF_CALLBACK)

View file

@ -85,10 +85,11 @@ class TabAdapter(private val listener: Listener) : RecyclerView.Adapter<TabViewH
fun onToggleVisibility(tabMode: MusicMode) fun onToggleVisibility(tabMode: MusicMode)
/** /**
* Called when the drag handle is pressed, requesting that a drag should be started. * Called when the drag handle on a [RecyclerView.ViewHolder] is clicked, requesting that a
* drag should be started.
* @param viewHolder The [RecyclerView.ViewHolder] to start dragging. * @param viewHolder The [RecyclerView.ViewHolder] to start dragging.
*/ */
fun onPickUpTab(viewHolder: RecyclerView.ViewHolder) fun onPickUp(viewHolder: RecyclerView.ViewHolder)
} }
companion object { companion object {
@ -105,7 +106,7 @@ class TabViewHolder private constructor(private val binding: ItemTabBinding) :
/** /**
* Bind new data to this instance. * Bind new data to this instance.
* @param tab The new [Tab] to bind. * @param tab The new [Tab] to bind.
* @param listener An [TabAdapter.Listener] to bind interactions to. * @param listener A [TabAdapter.Listener] to bind interactions to.
*/ */
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
fun bind(tab: Tab, listener: TabAdapter.Listener) { fun bind(tab: Tab, listener: TabAdapter.Listener) {
@ -130,14 +131,13 @@ class TabViewHolder private constructor(private val binding: ItemTabBinding) :
binding.tabDragHandle.setOnTouchListener { _, motionEvent -> binding.tabDragHandle.setOnTouchListener { _, motionEvent ->
binding.tabDragHandle.performClick() binding.tabDragHandle.performClick()
if (motionEvent.actionMasked == MotionEvent.ACTION_DOWN) { if (motionEvent.actionMasked == MotionEvent.ACTION_DOWN) {
listener.onPickUpTab(this) listener.onPickUp(this)
true true
} else false } else false
} }
} }
companion object { companion object {
/** /**
* Create a new instance. * Create a new instance.
* @param parent The parent to inflate this instance from. * @param parent The parent to inflate this instance from.

View file

@ -104,7 +104,7 @@ class TabCustomizeDialog : ViewBindingDialogFragment<DialogTabsBinding>(), TabAd
tabAdapter.tabs.filterIsInstance<Tab.Visible>().isNotEmpty() tabAdapter.tabs.filterIsInstance<Tab.Visible>().isNotEmpty()
} }
override fun onPickUpTab(viewHolder: RecyclerView.ViewHolder) { override fun onPickUp(viewHolder: RecyclerView.ViewHolder) {
touchHelper.startDrag(viewHolder) touchHelper.startDrag(viewHolder)
} }

View file

@ -2,7 +2,6 @@ package org.oxycblt.auxio.list
import androidx.annotation.StringRes import androidx.annotation.StringRes
/** /**
* A marker for something that is a RecyclerView item. Has no functionality on it's own. * A marker for something that is a RecyclerView item. Has no functionality on it's own.
*/ */

View file

@ -36,7 +36,7 @@ import org.oxycblt.auxio.util.showToast
* A Fragment containing a selectable list. * A Fragment containing a selectable list.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
abstract class ListFragment<VB : ViewBinding> : SelectionFragment<VB>(), ExtendedListListener { abstract class ListFragment<VB : ViewBinding> : SelectionFragment<VB>(), SelectableListListener {
protected val navModel: NavigationViewModel by activityViewModels() protected val navModel: NavigationViewModel by activityViewModels()
private var currentMenu: PopupMenu? = null private var currentMenu: PopupMenu? = null

View file

@ -1,12 +1,17 @@
package org.oxycblt.auxio.list package org.oxycblt.auxio.list
import android.view.MotionEvent
import android.view.View import android.view.View
import android.widget.Button import android.widget.Button
import android.widget.ImageView
import androidx.recyclerview.widget.RecyclerView
/** /**
* A basic listener for list interactions. * A basic listener for list interactions.
* TODO: Supply a ViewHolder on clicks (allows editable lists to be standardized into a listener.)
* @author Alexander Capehart (OxygenCobalt)
*/ */
interface BasicListListener { interface ClickableListListener {
/** /**
* Called when an [Item] in the list is clicked. * Called when an [Item] in the list is clicked.
* @param item The [Item] that was clicked. * @param item The [Item] that was clicked.
@ -15,9 +20,10 @@ interface BasicListListener {
} }
/** /**
* An extension of [BasicListListener] that enables menu and selection functionality. * An extension of [ClickableListListener] that enables menu and selection functionality.
* @author Alexander Capehart (OxygenCobalt)
*/ */
interface ExtendedListListener : BasicListListener { interface SelectableListListener : ClickableListListener {
/** /**
* Called when an [Item] in the list requests that a menu related to it should be opened. * Called when an [Item] in the list requests that a menu related to it should be opened.
* @param item The [Item] to show a menu for. * @param item The [Item] to show a menu for.
@ -33,12 +39,12 @@ interface ExtendedListListener : BasicListListener {
/** /**
* Binds this instance to a list item. * Binds this instance to a list item.
* @param viewHolder The [RecyclerView.ViewHolder] to bind.
* @param item The [Item] that this list entry is bound to. * @param item The [Item] that this list entry is bound to.
* @param root The root of the list [View].
* @param menuButton A [Button] that opens a menu. * @param menuButton A [Button] that opens a menu.
*/ */
fun bind(item: Item, root: View, menuButton: Button) { fun bind(viewHolder: RecyclerView.ViewHolder, item: Item, menuButton: Button) {
root.apply { viewHolder.itemView.apply {
// Map clicks to the click callback. // Map clicks to the click callback.
setOnClickListener { onClick(item) } setOnClickListener { onClick(item) }
// Map long clicks to the selection callback. // Map long clicks to the selection callback.
@ -47,7 +53,6 @@ interface ExtendedListListener : BasicListListener {
true true
} }
} }
// Map the menu button to the menu opening callback. // Map the menu button to the menu opening callback.
menuButton.setOnClickListener { onOpenMenu(item, it) } menuButton.setOnClickListener { onOpenMenu(item, it) }
} }

View file

@ -24,7 +24,7 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.ItemHeaderBinding import org.oxycblt.auxio.databinding.ItemHeaderBinding
import org.oxycblt.auxio.databinding.ItemParentBinding import org.oxycblt.auxio.databinding.ItemParentBinding
import org.oxycblt.auxio.databinding.ItemSongBinding import org.oxycblt.auxio.databinding.ItemSongBinding
import org.oxycblt.auxio.list.ExtendedListListener import org.oxycblt.auxio.list.SelectableListListener
import org.oxycblt.auxio.list.Header import org.oxycblt.auxio.list.Header
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
@ -43,10 +43,10 @@ class SongViewHolder private constructor(private val binding: ItemSongBinding) :
/** /**
* Bind new data to this instance. * Bind new data to this instance.
* @param song The new [Song] to bind. * @param song The new [Song] to bind.
* @param listener An [ExtendedListListener] to bind interactions to. * @param listener An [SelectableListListener] to bind interactions to.
*/ */
fun bind(song: Song, listener: ExtendedListListener) { fun bind(song: Song, listener: SelectableListListener) {
listener.bind(song, binding.root, binding.songMenu) listener.bind(this, song, binding.songMenu)
binding.songAlbumCover.bind(song) binding.songAlbumCover.bind(song)
binding.songName.text = song.resolveName(binding.context) binding.songName.text = song.resolveName(binding.context)
binding.songInfo.text = song.resolveArtistContents(binding.context) binding.songInfo.text = song.resolveArtistContents(binding.context)
@ -90,10 +90,10 @@ class AlbumViewHolder private constructor(private val binding: ItemParentBinding
/** /**
* Bind new data to this instance. * Bind new data to this instance.
* @param album The new [Album] to bind. * @param album The new [Album] to bind.
* @param listener An [ExtendedListListener] to bind interactions to. * @param listener An [SelectableListListener] to bind interactions to.
*/ */
fun bind(album: Album, listener: ExtendedListListener) { fun bind(album: Album, listener: SelectableListListener) {
listener.bind(album, binding.root, binding.parentMenu) listener.bind(this, album, binding.parentMenu)
binding.parentImage.bind(album) binding.parentImage.bind(album)
binding.parentName.text = album.resolveName(binding.context) binding.parentName.text = album.resolveName(binding.context)
binding.parentInfo.text = album.resolveArtistContents(binding.context) binding.parentInfo.text = album.resolveArtistContents(binding.context)
@ -139,10 +139,10 @@ class ArtistViewHolder private constructor(private val binding: ItemParentBindin
/** /**
* Bind new data to this instance. * Bind new data to this instance.
* @param artist The new [Artist] to bind. * @param artist The new [Artist] to bind.
* @param listener An [ExtendedListListener] to bind interactions to. * @param listener An [SelectableListListener] to bind interactions to.
*/ */
fun bind(artist: Artist, listener: ExtendedListListener) { fun bind(artist: Artist, listener: SelectableListListener) {
listener.bind(artist, binding.root, binding.parentMenu) listener.bind(this, artist, binding.parentMenu)
binding.parentImage.bind(artist) binding.parentImage.bind(artist)
binding.parentName.text = artist.resolveName(binding.context) binding.parentName.text = artist.resolveName(binding.context)
binding.parentInfo.text = binding.parentInfo.text =
@ -197,10 +197,10 @@ class GenreViewHolder private constructor(private val binding: ItemParentBinding
/** /**
* Bind new data to this instance. * Bind new data to this instance.
* @param genre The new [Genre] to bind. * @param genre The new [Genre] to bind.
* @param listener An [ExtendedListListener] to bind interactions to. * @param listener An [SelectableListListener] to bind interactions to.
*/ */
fun bind(genre: Genre, listener: ExtendedListListener) { fun bind(genre: Genre, listener: SelectableListListener) {
listener.bind(genre, binding.root, binding.parentMenu) listener.bind(this, genre, binding.parentMenu)
binding.parentImage.bind(genre) binding.parentImage.bind(genre)
binding.parentName.text = genre.resolveName(binding.context) binding.parentName.text = genre.resolveName(binding.context)
binding.parentInfo.text = binding.parentInfo.text =

View file

@ -21,7 +21,7 @@ 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.databinding.ItemPickerChoiceBinding import org.oxycblt.auxio.databinding.ItemPickerChoiceBinding
import org.oxycblt.auxio.list.BasicListListener import org.oxycblt.auxio.list.ClickableListListener
import org.oxycblt.auxio.list.recycler.DialogRecyclerView import org.oxycblt.auxio.list.recycler.DialogRecyclerView
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.context
@ -29,10 +29,10 @@ import org.oxycblt.auxio.util.inflater
/** /**
* An adapter responsible for showing a list of [Artist] choices in [ArtistPickerDialog]. * An adapter responsible for showing a list of [Artist] choices in [ArtistPickerDialog].
* @param listener A [BasicListListener] to bind interactions to. * @param listener A [ClickableListListener] to bind interactions to.
* @author OxygenCobalt. * @author OxygenCobalt.
*/ */
class ArtistChoiceAdapter(private val listener: BasicListListener) : class ArtistChoiceAdapter(private val listener: ClickableListListener) :
RecyclerView.Adapter<ArtistChoiceViewHolder>() { RecyclerView.Adapter<ArtistChoiceViewHolder>() {
private var artists = listOf<Artist>() private var artists = listOf<Artist>()
@ -65,9 +65,9 @@ class ArtistChoiceViewHolder(private val binding: ItemPickerChoiceBinding) :
/** /**
* Bind new data to this instance. * Bind new data to this instance.
* @param artist The new [Artist] to bind. * @param artist The new [Artist] to bind.
* @param listener A [BasicListListener] to bind interactions to. * @param listener A [ClickableListListener] to bind interactions to.
*/ */
fun bind(artist: Artist, listener: BasicListListener) { fun bind(artist: Artist, listener: ClickableListListener) {
binding.root.setOnClickListener { listener.onClick(artist) } binding.root.setOnClickListener { listener.onClick(artist) }
binding.pickerImage.bind(artist) binding.pickerImage.bind(artist)
binding.pickerName.text = artist.resolveName(binding.context) binding.pickerName.text = artist.resolveName(binding.context)

View file

@ -24,7 +24,7 @@ import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogMusicPickerBinding import org.oxycblt.auxio.databinding.DialogMusicPickerBinding
import org.oxycblt.auxio.list.BasicListListener import org.oxycblt.auxio.list.ClickableListListener
import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.shared.ViewBindingDialogFragment import org.oxycblt.auxio.shared.ViewBindingDialogFragment
@ -36,7 +36,7 @@ import org.oxycblt.auxio.util.collectImmediately
* multiple [Artist]'s to choose from. * multiple [Artist]'s to choose from.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
abstract class ArtistPickerDialog : ViewBindingDialogFragment<DialogMusicPickerBinding>(), BasicListListener { abstract class ArtistPickerDialog : ViewBindingDialogFragment<DialogMusicPickerBinding>(), ClickableListListener {
protected val pickerModel: PickerViewModel by viewModels() protected val pickerModel: PickerViewModel by viewModels()
// Okay to leak this since the Listener will not be called until after initialization. // Okay to leak this since the Listener will not be called until after initialization.
private val artistAdapter = ArtistChoiceAdapter(@Suppress("LeakingThis") this) private val artistAdapter = ArtistChoiceAdapter(@Suppress("LeakingThis") this)

View file

@ -36,9 +36,17 @@ import org.oxycblt.auxio.util.getAttrColorCompat
import org.oxycblt.auxio.util.getDimen import org.oxycblt.auxio.util.getDimen
import org.oxycblt.auxio.util.inflater import org.oxycblt.auxio.util.inflater
class QueueAdapter(private val listener: QueueItemListener) : /**
* A [RecyclerView.Adapter] that shows an editable list of queue items.
* @param listener A [Listener] to bind interactions to.
* @author Alexander Capehart (OxygenCobalt)
*/
class QueueAdapter(private val listener: Listener) :
RecyclerView.Adapter<QueueSongViewHolder>() { RecyclerView.Adapter<QueueSongViewHolder>() {
private var differ = SyncListDiffer(this, QueueSongViewHolder.DIFF_CALLBACK) private var differ = SyncListDiffer(this, QueueSongViewHolder.DIFF_CALLBACK)
// Since PlayingIndicator adapter relies on an item value, we cannot use it for this
// adapter, as one item can appear at several points in the UI. Use a similar implementation
// with an index value instead.
private var currentIndex = 0 private var currentIndex = 0
private var isPlaying = false private var isPlaying = false
@ -59,63 +67,106 @@ class QueueAdapter(private val listener: QueueItemListener) :
viewHolder.bind(differ.currentList[position], listener) viewHolder.bind(differ.currentList[position], listener)
} }
viewHolder.isEnabled = position > currentIndex viewHolder.isFuture = position > currentIndex
viewHolder.updatePlayingIndicator(position == currentIndex, isPlaying) viewHolder.updatePlayingIndicator(position == currentIndex, isPlaying)
} }
/**
* Synchronously update the list with new items. This is exceedingly slow for large diffs,
* so only use it for trivial updates.
* @param newList The new [Song]s for the adapter to display.
*/
fun submitList(newList: List<Song>) { fun submitList(newList: List<Song>) {
differ.submitList(newList) differ.submitList(newList)
} }
/**
* Replace the list with a new list. This is exceedingly slow for large diffs,
* so only use it for trivial updates.
* @param newList The new [Song]s for the adapter to display.
*/
fun replaceList(newList: List<Song>) { fun replaceList(newList: List<Song>) {
differ.replaceList(newList) differ.replaceList(newList)
} }
fun updateIndicator(index: Int, isPlaying: Boolean) { /**
* Set the position of the currently playing item in the queue. This will mark the item
* as playing and any previous items as played.
* @param index The position of the currently playing item in the queue.
* @param isPlaying Whether playback is ongoing or paused.
*/
fun setPosition(index: Int, isPlaying: Boolean) {
var updatedIndex = false var updatedIndex = false
if (index != currentIndex) { if (index != currentIndex) {
when { val lastIndex = currentIndex
index < currentIndex -> { currentIndex = index
val lastIndex = currentIndex
currentIndex = index
notifyItemRangeChanged(0, lastIndex + 1, PAYLOAD_UPDATE_INDEX)
}
else -> {
currentIndex = index
notifyItemRangeChanged(0, currentIndex + 1, PAYLOAD_UPDATE_INDEX)
}
}
updatedIndex = true updatedIndex = true
// Have to update not only the currently playing item, but also all items marked
// as playing.
if (currentIndex < lastIndex) {
notifyItemRangeChanged(0, lastIndex + 1, PAYLOAD_UPDATE_POSITION)
} else {
notifyItemRangeChanged(0, currentIndex + 1, PAYLOAD_UPDATE_POSITION)
}
} }
if (this.isPlaying != isPlaying) { if (this.isPlaying != isPlaying) {
this.isPlaying = isPlaying this.isPlaying = isPlaying
// Don't need to do anything if we've already sent an update from changing the
// index.
if (!updatedIndex) { if (!updatedIndex) {
notifyItemChanged(index, PAYLOAD_UPDATE_INDEX) notifyItemChanged(index, PAYLOAD_UPDATE_POSITION)
} }
} }
} }
/**
* A listener for queue list events.
*/
interface Listener {
/**
* Called when a [RecyclerView.ViewHolder] in the list as clicked.
* @param viewHolder The [RecyclerView.ViewHolder] that was clicked.
*/
fun onClick(viewHolder: RecyclerView.ViewHolder)
/**
* Called when the drag handle on a [RecyclerView.ViewHolder] is clicked, requesting that a
* drag should be started.
* @param viewHolder The [RecyclerView.ViewHolder] to start dragging.
*/
fun onPickUp(viewHolder: RecyclerView.ViewHolder)
}
companion object { companion object {
val PAYLOAD_UPDATE_INDEX = Any() private val PAYLOAD_UPDATE_POSITION = Any()
} }
} }
interface QueueItemListener { /**
fun onClick(viewHolder: RecyclerView.ViewHolder) * A [PlayingIndicatorAdapter.ViewHolder] that displays a queue [Song]. Use [new] to create an
fun onPickUp(viewHolder: RecyclerView.ViewHolder) * instance.
} * @author Alexander Capehart (OxygenCobalt)
*/
class QueueSongViewHolder private constructor(private val binding: ItemQueueSongBinding) : class QueueSongViewHolder private constructor(private val binding: ItemQueueSongBinding) :
PlayingIndicatorAdapter.ViewHolder(binding.root) { PlayingIndicatorAdapter.ViewHolder(binding.root) {
/**
* The "body" view of this [QueueSongViewHolder] that shows the [Song] information.
*/
val bodyView: View val bodyView: View
get() = binding.body get() = binding.body
/**
* The background view of this [QueueSongViewHolder] that shows the delete icon.
*/
val backgroundView: View val backgroundView: View
get() = binding.background get() = binding.background
/**
* The actual background drawable of this [QueueSongViewHolder] that can be manipulated.
*/
val backgroundDrawable = val backgroundDrawable =
MaterialShapeDrawable.createWithElevationOverlay(binding.root.context).apply { MaterialShapeDrawable.createWithElevationOverlay(binding.root.context).apply {
fillColor = binding.context.getAttrColorCompat(R.attr.colorSurface) fillColor = binding.context.getAttrColorCompat(R.attr.colorSurface)
@ -123,6 +174,19 @@ class QueueSongViewHolder private constructor(private val binding: ItemQueueSong
alpha = 0 alpha = 0
} }
/**
* If this queue item is considered "in the future" (i.e has not played yet).
*/
var isFuture: Boolean
get() = binding.songAlbumCover.isEnabled
set(value) {
// Don't want to disable clicking, just indicate the body and handle is disabled
binding.songAlbumCover.isEnabled = value
binding.songName.isEnabled = value
binding.songInfo.isEnabled = value
binding.songDragHandle.isEnabled = value
}
init { init {
binding.body.background = binding.body.background =
LayerDrawable( LayerDrawable(
@ -134,17 +198,24 @@ class QueueSongViewHolder private constructor(private val binding: ItemQueueSong
backgroundDrawable)) backgroundDrawable))
} }
/**
* Bind new data to this instance.
* @param song The new [Song] to bind.
* @param listener A [QueueAdapter.Listener] to bind interactions to.
*/
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
fun bind(item: Song, listener: QueueItemListener) { fun bind(song: Song, listener: QueueAdapter.Listener) {
binding.songAlbumCover.bind(item) binding.body.setOnClickListener {
binding.songName.text = item.resolveName(binding.context) listener.onClick(this)
binding.songInfo.text = item.resolveArtistContents(binding.context) }
binding.songAlbumCover.bind(song)
binding.songName.text = song.resolveName(binding.context)
binding.songInfo.text = song.resolveArtistContents(binding.context)
// TODO: Why is this here?
binding.background.isInvisible = true binding.background.isInvisible = true
binding.body.setOnClickListener { listener.onClick(this) } // Set up the drag handle to start a drag whenever it is touched.
// Roll our own drag handlers as the default ones suck
binding.songDragHandle.setOnTouchListener { _, motionEvent -> binding.songDragHandle.setOnTouchListener { _, motionEvent ->
binding.songDragHandle.performClick() binding.songDragHandle.performClick()
if (motionEvent.actionMasked == MotionEvent.ACTION_DOWN) { if (motionEvent.actionMasked == MotionEvent.ACTION_DOWN) {
@ -154,25 +225,21 @@ class QueueSongViewHolder private constructor(private val binding: ItemQueueSong
} }
} }
var isEnabled: Boolean
get() = binding.songAlbumCover.isEnabled
set(value) {
// Don't want to disable clicking, just indicate the body and handle is disabled
binding.songAlbumCover.isEnabled = value
binding.songName.isEnabled = value
binding.songInfo.isEnabled = value
binding.songDragHandle.isEnabled = value
}
override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) { override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {
binding.interactBody.isSelected = isActive binding.interactBody.isSelected = isActive
binding.songAlbumCover.isPlaying = isPlaying binding.songAlbumCover.isPlaying = isPlaying
} }
companion object { companion object {
/**
* Create a new instance.
* @param parent The parent to inflate this instance from.
* @return A new instance.
*/
fun new(parent: View) = fun new(parent: View) =
QueueSongViewHolder(ItemQueueSongBinding.inflate(parent.context.inflater)) QueueSongViewHolder(ItemQueueSongBinding.inflate(parent.context.inflater))
/** A comparator that can be used with DiffUtil. */
val DIFF_CALLBACK = SongViewHolder.DIFF_CALLBACK val DIFF_CALLBACK = SongViewHolder.DIFF_CALLBACK
} }
} }

View file

@ -32,7 +32,8 @@ import org.oxycblt.auxio.util.replaceSystemBarInsetsCompat
import org.oxycblt.auxio.util.systemBarInsetsCompat import org.oxycblt.auxio.util.systemBarInsetsCompat
/** /**
* The bottom sheet behavior designed for the queue in particular. * The [BaseBottomSheetBehavior] for the queue bottom sheet. This is placed within the playback
* sheet and automatically arranges itself to show the playback bar at the top.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class QueueBottomSheetBehavior<V : View>(context: Context, attributeSet: AttributeSet?) : class QueueBottomSheetBehavior<V : View>(context: Context, attributeSet: AttributeSet?) :
@ -41,6 +42,7 @@ class QueueBottomSheetBehavior<V : View>(context: Context, attributeSet: Attribu
private var barSpacing = context.getDimenPixels(R.dimen.spacing_small) private var barSpacing = context.getDimenPixels(R.dimen.spacing_small)
init { init {
// Not hide-able (and not programmatically hide-able)
isHideable = false isHideable = false
} }
@ -53,18 +55,19 @@ class QueueBottomSheetBehavior<V : View>(context: Context, attributeSet: Attribu
dependency: View dependency: View
): Boolean { ): Boolean {
barHeight = dependency.height barHeight = dependency.height
return false // No change, just grabbed the height // No change, just grabbed the height
return false
} }
override fun createBackground(context: Context) = override fun createBackground(context: Context) =
MaterialShapeDrawable.createWithElevationOverlay(context).apply { MaterialShapeDrawable.createWithElevationOverlay(context).apply {
// The queue sheet's background is a static elevated background.
fillColor = context.getAttrColorCompat(R.attr.colorSurface) fillColor = context.getAttrColorCompat(R.attr.colorSurface)
elevation = context.getDimen(R.dimen.elevation_normal) elevation = context.getDimen(R.dimen.elevation_normal)
} }
override fun applyWindowInsets(child: View, insets: WindowInsets): WindowInsets { override fun applyWindowInsets(child: View, insets: WindowInsets): WindowInsets {
super.applyWindowInsets(child, insets) super.applyWindowInsets(child, insets)
// Offset our expanded panel by the size of the playback bar, as that is shown when // Offset our expanded panel by the size of the playback bar, as that is shown when
// we slide up the panel. // we slide up the panel.
val bars = insets.systemBarInsetsCompat val bars = insets.systemBarInsetsCompat

View file

@ -24,12 +24,12 @@ import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.util.getDimen import org.oxycblt.auxio.util.getDimen
import org.oxycblt.auxio.util.getInteger
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
/** /**
* A highly customized [ItemTouchHelper.Callback] that handles the queue system while basically * A highly customized [ItemTouchHelper.Callback] that enables some extra eye candy in the queue
* rebuilding most the "Material-y" aspects of an editable list because Google's implementations are * UI, such as an animation when lifting items.
* hot garbage. This shouldn't have *too many* UI bugs. I hope.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class QueueDragCallback(private val playbackModel: QueueViewModel) : ItemTouchHelper.Callback() { class QueueDragCallback(private val playbackModel: QueueViewModel) : ItemTouchHelper.Callback() {
@ -40,11 +40,13 @@ class QueueDragCallback(private val playbackModel: QueueViewModel) : ItemTouchHe
viewHolder: RecyclerView.ViewHolder viewHolder: RecyclerView.ViewHolder
): Int { ): Int {
val queueHolder = viewHolder as QueueSongViewHolder val queueHolder = viewHolder as QueueSongViewHolder
return if (queueHolder.isEnabled) { return if (queueHolder.isFuture) {
makeFlag( makeFlag(
ItemTouchHelper.ACTION_STATE_DRAG, ItemTouchHelper.UP or ItemTouchHelper.DOWN) or ItemTouchHelper.ACTION_STATE_DRAG, ItemTouchHelper.UP or ItemTouchHelper.DOWN) or
makeFlag(ItemTouchHelper.ACTION_STATE_SWIPE, ItemTouchHelper.START) makeFlag(ItemTouchHelper.ACTION_STATE_SWIPE, ItemTouchHelper.START)
} else { } else {
// Avoid allowing any touch actions for already-played queue items, as the playback
// system does not currently allow for this.
0 0
} }
} }
@ -58,12 +60,11 @@ class QueueDragCallback(private val playbackModel: QueueViewModel) : ItemTouchHe
actionState: Int, actionState: Int,
isCurrentlyActive: Boolean isCurrentlyActive: Boolean
) { ) {
// The material design page on elevation has a cool example of draggable items elevating
// themselves when being dragged. Too bad google's implementation of this doesn't even
// work! To emulate it on my own, I check if this child is in a drag state and then animate
// an elevation change.
val holder = viewHolder as QueueSongViewHolder val holder = viewHolder as QueueSongViewHolder
// Hook drag events to "lifting" the queue item (i.e raising it's elevation). Make sure
// this is only done once when the item is initially picked up.
// TODO: I think this is possible to improve with a raw ValueAnimator.
if (shouldLift && isCurrentlyActive && actionState == ItemTouchHelper.ACTION_STATE_DRAG) { if (shouldLift && isCurrentlyActive && actionState == ItemTouchHelper.ACTION_STATE_DRAG) {
logD("Lifting queue item") logD("Lifting queue item")
@ -72,7 +73,7 @@ class QueueDragCallback(private val playbackModel: QueueViewModel) : ItemTouchHe
holder.itemView holder.itemView
.animate() .animate()
.translationZ(elevation) .translationZ(elevation)
.setDuration(100) .setDuration(recyclerView.context.getInteger(R.integer.anim_fade_exit_duration).toLong())
.setUpdateListener { .setUpdateListener {
bg.alpha = ((holder.itemView.translationZ / elevation) * 255).toInt() bg.alpha = ((holder.itemView.translationZ / elevation) * 255).toInt()
} }
@ -82,20 +83,19 @@ class QueueDragCallback(private val playbackModel: QueueViewModel) : ItemTouchHe
shouldLift = false shouldLift = false
} }
// We show a background with a clear icon behind the queue song each time one is swiped // We show a background with a delete icon behind the queue song each time one is swiped
// away. To avoid any canvas shenanigans, we just place a custom background view behind the // away. To avoid working with canvas, this is simply placed behind the queue body.
// main "body" layout of the queue item and then translate that.
//
// That comes with a couple of problems, however. For one, the background view will always // That comes with a couple of problems, however. For one, the background view will always
// lag behind the body view, resulting in a noticeable pixel offset when dragging. To fix // lag behind the body view, resulting in a noticeable pixel offset when dragging. To fix
// this, we make this a separate view and make this view invisible whenever the item is // this, we make this a separate view and make this view invisible whenever the item is
// not being swiped. We cannot merge this view with the FrameLayout, as that will cause // not being swiped. This issue is also the reason why the background is not merged with
// another weird pixel desynchronization issue that is less visible but still incredibly // the FrameLayout within the queue item.
// annoying.
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) { if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
holder.backgroundView.isInvisible = dX == 0f holder.backgroundView.isInvisible = dX == 0f
} }
// Update other translations. We do not call the default implementation, so we must do
// this ourselves.
holder.bodyView.translationX = dX holder.bodyView.translationX = dX
holder.itemView.translationY = dY holder.itemView.translationY = dY
} }
@ -104,6 +104,8 @@ class QueueDragCallback(private val playbackModel: QueueViewModel) : ItemTouchHe
// When an elevated item is cleared, we reset the elevation using another animation. // When an elevated item is cleared, we reset the elevation using another animation.
val holder = viewHolder as QueueSongViewHolder val holder = viewHolder as QueueSongViewHolder
// This function can be called multiple times, so only start the animation when the view's
// translationZ is already non-zero.
if (holder.itemView.translationZ != 0f) { if (holder.itemView.translationZ != 0f) {
logD("Dropping queue item") logD("Dropping queue item")
@ -112,7 +114,7 @@ class QueueDragCallback(private val playbackModel: QueueViewModel) : ItemTouchHe
holder.itemView holder.itemView
.animate() .animate()
.translationZ(0f) .translationZ(0f)
.setDuration(100) .setDuration(recyclerView.context.getInteger(R.integer.anim_fade_exit_duration).toLong())
.setUpdateListener { .setUpdateListener {
bg.alpha = ((holder.itemView.translationZ / elevation) * 255).toInt() bg.alpha = ((holder.itemView.translationZ / elevation) * 255).toInt()
} }
@ -122,6 +124,8 @@ class QueueDragCallback(private val playbackModel: QueueViewModel) : ItemTouchHe
shouldLift = true shouldLift = true
// Reset translations. We do not call the default implementation, so we must do
// this ourselves.
holder.bodyView.translationX = 0f holder.bodyView.translationX = 0f
holder.itemView.translationY = 0f holder.itemView.translationY = 0f
} }
@ -138,5 +142,6 @@ class QueueDragCallback(private val playbackModel: QueueViewModel) : ItemTouchHe
playbackModel.removeQueueDataItem(viewHolder.bindingAdapterPosition) playbackModel.removeQueueDataItem(viewHolder.bindingAdapterPosition)
} }
// Long-press events are too buggy, only allow dragging with the handle.
override fun isLongPressDragEnabled() = false override fun isLongPressDragEnabled() = false
} }

View file

@ -19,7 +19,6 @@ package org.oxycblt.auxio.playback.queue
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@ -33,11 +32,10 @@ import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
/** /**
* A [Fragment] that shows the queue and enables editing as well. * A [ViewBindingFragment] that displays an editable queue.
*
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), QueueItemListener { class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), QueueAdapter.Listener {
private val queueModel: QueueViewModel by activityViewModels() private val queueModel: QueueViewModel by activityViewModels()
private val playbackModel: PlaybackViewModel by androidActivityViewModels() private val playbackModel: PlaybackViewModel by androidActivityViewModels()
private val queueAdapter = QueueAdapter(this) private val queueAdapter = QueueAdapter(this)
@ -48,13 +46,15 @@ class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), QueueItemList
override fun onCreateBinding(inflater: LayoutInflater) = FragmentQueueBinding.inflate(inflater) override fun onCreateBinding(inflater: LayoutInflater) = FragmentQueueBinding.inflate(inflater)
override fun onBindingCreated(binding: FragmentQueueBinding, savedInstanceState: Bundle?) { override fun onBindingCreated(binding: FragmentQueueBinding, savedInstanceState: Bundle?) {
super.onBindingCreated(binding, savedInstanceState)
// --- UI SETUP ---
binding.queueRecycler.apply { binding.queueRecycler.apply {
adapter = queueAdapter adapter = queueAdapter
touchHelper.attachToRecyclerView(this) touchHelper.attachToRecyclerView(this)
} }
// --- VIEWMODEL SETUP ---- // --- VIEWMODEL SETUP ----
collectImmediately( collectImmediately(
queueModel.queue, queueModel.index, playbackModel.isPlaying, ::updateQueue) queueModel.queue, queueModel.index, playbackModel.isPlaying, ::updateQueue)
} }
@ -65,6 +65,7 @@ class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), QueueItemList
} }
override fun onClick(viewHolder: RecyclerView.ViewHolder) { override fun onClick(viewHolder: RecyclerView.ViewHolder) {
// Clicking on a queue item should start playing it.
queueModel.goto(viewHolder.bindingAdapterPosition) queueModel.goto(viewHolder.bindingAdapterPosition)
} }
@ -75,31 +76,34 @@ class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), QueueItemList
private fun updateQueue(queue: List<Song>, index: Int, isPlaying: Boolean) { private fun updateQueue(queue: List<Song>, index: Int, isPlaying: Boolean) {
val binding = requireBinding() val binding = requireBinding()
val replaceQueue = queueModel.replaceQueue // Replace or diff the queue depending on the type of change it is.
if (replaceQueue == true) { // TODO: Extend this to the whole app.
if (queueModel.replaceQueue == true) {
logD("Replacing queue") logD("Replacing queue")
queueAdapter.replaceList(queue) queueAdapter.replaceList(queue)
} else { } else {
logD("Diffing queue") logD("Diffing queue")
queueAdapter.submitList(queue) queueAdapter.submitList(queue)
} }
queueModel.finishReplace() queueModel.finishReplace()
// If requested, scroll to a new item (occurs when the index moves)
val scrollTo = queueModel.scrollTo val scrollTo = queueModel.scrollTo
if (scrollTo != null) { if (scrollTo != null) {
// Do not scroll to indices that are not in the currently visible range.
// This prevents the queue from jumping around when the user is trying to
// navigate the queue.
val lmm = binding.queueRecycler.layoutManager as LinearLayoutManager val lmm = binding.queueRecycler.layoutManager as LinearLayoutManager
val start = lmm.findFirstCompletelyVisibleItemPosition() val start = lmm.findFirstCompletelyVisibleItemPosition()
val end = lmm.findLastCompletelyVisibleItemPosition() val end = lmm.findLastCompletelyVisibleItemPosition()
if (scrollTo !in start..end) { if (scrollTo !in start..end) {
logD("Scrolling to new position") logD("Scrolling to new position")
binding.queueRecycler.scrollToPosition(scrollTo) binding.queueRecycler.scrollToPosition(scrollTo)
} }
} }
queueModel.finishScrollTo() queueModel.finishScrollTo()
queueAdapter.updateIndicator(index, isPlaying) // Update currently playing item
queueAdapter.setPosition(index, isPlaying)
} }
} }

View file

@ -25,21 +25,25 @@ import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.PlaybackStateManager
/** /**
* Class enabling more advanced queue list functionality and queue editing. TODO: Allow editing * A [ViewModel] that manages the current queue state and allows navigation through the queue.
* previous parts of the queue *
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class QueueViewModel : ViewModel(), PlaybackStateManager.Callback { class QueueViewModel : ViewModel(), PlaybackStateManager.Callback {
private val playbackManager = PlaybackStateManager.getInstance() private val playbackManager = PlaybackStateManager.getInstance()
private val _queue = MutableStateFlow(listOf<Song>()) private val _queue = MutableStateFlow(listOf<Song>())
/** The current queue. */
val queue: StateFlow<List<Song>> = _queue val queue: StateFlow<List<Song>> = _queue
private val _index = MutableStateFlow(playbackManager.index) private val _index = MutableStateFlow(playbackManager.index)
/** The index of the currently playing song in the queue. */
val index: StateFlow<Int> val index: StateFlow<Int>
get() = _index get() = _index
/** Whether to replace or diff the queue list when updating it. Is null if not specified. */
var replaceQueue: Boolean? = null var replaceQueue: Boolean? = null
/** Flag to scroll to a particular queue item. Is null if no command has been specified. */
var scrollTo: Int? = null var scrollTo: Int? = null
init { init {
@ -47,58 +51,78 @@ class QueueViewModel : ViewModel(), PlaybackStateManager.Callback {
} }
/** /**
* Go to an item in the queue using it's recyclerview adapter index. No-ops if out of bounds. * Start playing the the queue item at the given index.
* @param adapterIndex The index of the queue item to play. Does nothing if the index is out
* of range.
*/ */
fun goto(adapterIndex: Int) { fun goto(adapterIndex: Int) {
if (adapterIndex !in playbackManager.queue.indices) { if (adapterIndex !in playbackManager.queue.indices) {
// Invalid input. Nothing to do.
return return
} }
playbackManager.goto(adapterIndex) playbackManager.goto(adapterIndex)
} }
/** Remove a queue item using it's recyclerview adapter index. */ /**
* Remove a queue item at the given index.
* @param adapterIndex The index of the queue item to play. Does nothing if the index is
* out of range.
*/
fun removeQueueDataItem(adapterIndex: Int) { fun removeQueueDataItem(adapterIndex: Int) {
if (adapterIndex <= playbackManager.index || if (adapterIndex <= playbackManager.index ||
adapterIndex !in playbackManager.queue.indices) { adapterIndex !in playbackManager.queue.indices) {
// Invalid input. Nothing to do.
// TODO: Allow editing played queue items.
return return
} }
playbackManager.removeQueueItem(adapterIndex) playbackManager.removeQueueItem(adapterIndex)
} }
/** Move queue items using their recyclerview adapter indices. */ /**
* Move a queue item from one index to another index.
* @param adapterFrom The index of the queue item to move.
* @param adapterTo The destination index for the queue item.
* @return true if the items were moved, false otherwise.
*/
fun moveQueueDataItems(adapterFrom: Int, adapterTo: Int): Boolean { fun moveQueueDataItems(adapterFrom: Int, adapterTo: Int): Boolean {
if (adapterFrom <= playbackManager.index || adapterTo <= playbackManager.index) { if (adapterFrom <= playbackManager.index || adapterTo <= playbackManager.index) {
// Invalid input. Nothing to do.
return false return false
} }
playbackManager.moveQueueItem(adapterFrom, adapterTo) playbackManager.moveQueueItem(adapterFrom, adapterTo)
return true return true
} }
/**
* Finish a replace flag specified by [replaceQueue].
*/
fun finishReplace() { fun finishReplace() {
replaceQueue = null replaceQueue = null
} }
/**
* Finish a scroll operation started by [scrollTo].
*/
fun finishScrollTo() { fun finishScrollTo() {
scrollTo = null scrollTo = null
} }
override fun onIndexMoved(index: Int) { override fun onIndexMoved(index: Int) {
// Index moved -> Scroll to new index
replaceQueue = null replaceQueue = null
scrollTo = index scrollTo = index
_index.value = index _index.value = index
} }
override fun onQueueChanged(queue: List<Song>) { override fun onQueueChanged(queue: List<Song>) {
// Queue changed trivially due to item move -> Diff queue, stay at current index.
replaceQueue = false replaceQueue = false
scrollTo = null scrollTo = null
_queue.value = playbackManager.queue.toMutableList() _queue.value = playbackManager.queue.toMutableList()
} }
override fun onQueueReworked(index: Int, queue: List<Song>) { override fun onQueueReworked(index: Int, queue: List<Song>) {
// Queue changed completely -> Replace queue, update index
replaceQueue = true replaceQueue = true
scrollTo = index scrollTo = index
_queue.value = playbackManager.queue.toMutableList() _queue.value = playbackManager.queue.toMutableList()
@ -106,6 +130,7 @@ class QueueViewModel : ViewModel(), PlaybackStateManager.Callback {
} }
override fun onNewPlayback(index: Int, queue: List<Song>, parent: MusicParent?) { override fun onNewPlayback(index: Int, queue: List<Song>, parent: MusicParent?) {
// Entirely new queue -> Replace queue, update index
replaceQueue = true replaceQueue = true
scrollTo = index scrollTo = index
_queue.value = playbackManager.queue.toMutableList() _queue.value = playbackManager.queue.toMutableList()

View file

@ -29,10 +29,10 @@ import org.oxycblt.auxio.music.Song
/** /**
* An adapter that displays search results. * An adapter that displays search results.
* @param listener An [ExtendedListListener] to bind interactions to. * @param listener An [SelectableListListener] to bind interactions to.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class SearchAdapter(private val listener: ExtendedListListener) : class SearchAdapter(private val listener: SelectableListListener) :
SelectionIndicatorAdapter<RecyclerView.ViewHolder>(), AuxioRecyclerView.SpanSizeLookup { SelectionIndicatorAdapter<RecyclerView.ViewHolder>(), AuxioRecyclerView.SpanSizeLookup {
private val differ = AsyncListDiffer(this, DIFF_CALLBACK) private val differ = AsyncListDiffer(this, DIFF_CALLBACK)

View file

@ -23,17 +23,17 @@ import androidx.appcompat.widget.TooltipCompat
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.ItemAccentBinding import org.oxycblt.auxio.databinding.ItemAccentBinding
import org.oxycblt.auxio.list.BasicListListener import org.oxycblt.auxio.list.ClickableListListener
import org.oxycblt.auxio.util.getAttrColorCompat import org.oxycblt.auxio.util.getAttrColorCompat
import org.oxycblt.auxio.util.getColorCompat import org.oxycblt.auxio.util.getColorCompat
import org.oxycblt.auxio.util.inflater import org.oxycblt.auxio.util.inflater
/** /**
* A [RecyclerView.Adapter] that displays [Accent] choices. * A [RecyclerView.Adapter] that displays [Accent] choices.
* @param listener A [BasicListListener] to bind interactions to. * @param listener A [ClickableListListener] to bind interactions to.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class AccentAdapter(private val listener: BasicListListener) : class AccentAdapter(private val listener: ClickableListListener) :
RecyclerView.Adapter<AccentViewHolder>() { RecyclerView.Adapter<AccentViewHolder>() {
/** The currently selected [Accent]. */ /** The currently selected [Accent]. */
var selectedAccent: Accent? = null var selectedAccent: Accent? = null
@ -90,9 +90,9 @@ class AccentViewHolder private constructor(private val binding: ItemAccentBindin
/** /**
* Bind new data to this instance. * Bind new data to this instance.
* @param accent The new [Accent] to bind. * @param accent The new [Accent] to bind.
* @param listener A [BasicListListener] to bind interactions to. * @param listener A [ClickableListListener] to bind interactions to.
*/ */
fun bind(accent: Accent, listener: BasicListListener) { fun bind(accent: Accent, listener: ClickableListListener) {
binding.accent.apply { binding.accent.apply {
setOnClickListener { listener.onClick(accent) } setOnClickListener { listener.onClick(accent) }
backgroundTintList = context.getColorCompat(accent.primary) backgroundTintList = context.getColorCompat(accent.primary)

View file

@ -23,7 +23,7 @@ import androidx.appcompat.app.AlertDialog
import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogAccentBinding import org.oxycblt.auxio.databinding.DialogAccentBinding
import org.oxycblt.auxio.list.BasicListListener import org.oxycblt.auxio.list.ClickableListListener
import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.shared.ViewBindingDialogFragment import org.oxycblt.auxio.shared.ViewBindingDialogFragment
@ -35,7 +35,7 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
* A [ViewBindingDialogFragment] that allows the user to configure the current [Accent]. * A [ViewBindingDialogFragment] that allows the user to configure the current [Accent].
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class AccentCustomizeDialog : ViewBindingDialogFragment<DialogAccentBinding>(), BasicListListener { class AccentCustomizeDialog : ViewBindingDialogFragment<DialogAccentBinding>(), ClickableListListener {
private var accentAdapter = AccentAdapter(this) private var accentAdapter = AccentAdapter(this)
private val settings: Settings by lifecycleObject { binding -> Settings(binding.context) } private val settings: Settings by lifecycleObject { binding -> Settings(binding.context) }