From 2be7d3460167a6c2996d391bc5ab3bf6b778137b Mon Sep 17 00:00:00 2001 From: OxygenCobalt Date: Sat, 7 Nov 2020 16:06:47 -0700 Subject: [PATCH] Add user queue Add a user-generated queue, currently it isnt played from. --- .../detail/adapters/DetailAlbumAdapter.kt | 2 +- .../detail/adapters/DetailArtistAdapter.kt | 2 +- .../detail/adapters/DetailSongAdapter.kt | 2 +- .../oxycblt/auxio/library/LibraryFragment.kt | 9 +- .../auxio/library/adapters/SearchAdapter.kt | 10 +- .../auxio/playback/PlaybackFragment.kt | 13 +- .../auxio/playback/PlaybackViewModel.kt | 20 +++ .../auxio/playback/queue/QueueAdapter.kt | 2 +- .../auxio/playback/queue/QueueDragCallback.kt | 16 +- .../auxio/playback/queue/QueueFragment.kt | 78 +++------- .../auxio/playback/queue/QueueListFragment.kt | 146 ++++++++++++++++++ .../playback/state/PlaybackStateManager.kt | 51 +++++- .../recycler/viewholders/BaseViewHolder.kt | 12 +- .../recycler/viewholders/ModelHolders.kt | 15 +- .../org/oxycblt/auxio/songs/SongAdapter.kt | 6 +- .../org/oxycblt/auxio/songs/SongsFragment.kt | 31 +++- .../drawable-v24/ic_launcher_foreground.xml | 14 +- app/src/main/res/drawable/ic_exit.xml | 6 +- .../res/drawable/ic_launcher_foreground.xml | 14 +- app/src/main/res/drawable/ic_loop.xml | 6 +- .../main/res/drawable/ic_loop_disabled.xml | 6 +- app/src/main/res/drawable/ic_loop_large.xml | 6 +- app/src/main/res/drawable/ic_loop_one.xml | 6 +- .../main/res/drawable/ic_loop_one_large.xml | 6 +- app/src/main/res/drawable/ic_pause.xml | 5 +- app/src/main/res/drawable/ic_play.xml | 5 +- app/src/main/res/drawable/ic_user_queue.xml | 11 ++ app/src/main/res/layout/fragment_playback.xml | 3 +- app/src/main/res/layout/fragment_queue.xml | 27 +--- .../main/res/layout/fragment_queue_list.xml | 49 ++++++ app/src/main/res/menu/menu_song_actions.xml | 7 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 4 +- .../mipmap-anydpi-v26/ic_launcher_round.xml | 4 +- app/src/main/res/navigation/nav_main.xml | 10 +- app/src/main/res/values/strings.xml | 2 + app/src/main/res/values/styles.xml | 6 + 36 files changed, 464 insertions(+), 148 deletions(-) create mode 100644 app/src/main/java/org/oxycblt/auxio/playback/queue/QueueListFragment.kt create mode 100644 app/src/main/res/drawable/ic_user_queue.xml create mode 100644 app/src/main/res/layout/fragment_queue_list.xml create mode 100644 app/src/main/res/menu/menu_song_actions.xml diff --git a/app/src/main/java/org/oxycblt/auxio/detail/adapters/DetailAlbumAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/adapters/DetailAlbumAdapter.kt index 6ffb8f4ae..a080be676 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/adapters/DetailAlbumAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/adapters/DetailAlbumAdapter.kt @@ -25,7 +25,7 @@ class DetailAlbumAdapter( // Generic ViewHolder for a detail album inner class ViewHolder( private val binding: ItemArtistAlbumBinding, - ) : BaseViewHolder(binding, doOnClick) { + ) : BaseViewHolder(binding, doOnClick, null) { override fun onBind(data: Album) { binding.album = data diff --git a/app/src/main/java/org/oxycblt/auxio/detail/adapters/DetailArtistAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/adapters/DetailArtistAdapter.kt index 06c792c4a..161e92486 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/adapters/DetailArtistAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/adapters/DetailArtistAdapter.kt @@ -25,7 +25,7 @@ class DetailArtistAdapter( // Generic ViewHolder for an album inner class ViewHolder( private val binding: ItemGenreArtistBinding - ) : BaseViewHolder(binding, doOnClick) { + ) : BaseViewHolder(binding, doOnClick, null) { override fun onBind(data: Artist) { binding.artist = data diff --git a/app/src/main/java/org/oxycblt/auxio/detail/adapters/DetailSongAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/adapters/DetailSongAdapter.kt index 48a5bb08f..b0fbce423 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/adapters/DetailSongAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/adapters/DetailSongAdapter.kt @@ -24,7 +24,7 @@ class DetailSongAdapter( // Generic ViewHolder for a song inner class ViewHolder( private val binding: ItemAlbumSongBinding, - ) : BaseViewHolder(binding, doOnClick) { + ) : BaseViewHolder(binding, doOnClick, null) { override fun onBind(data: Song) { binding.song = data diff --git a/app/src/main/java/org/oxycblt/auxio/library/LibraryFragment.kt b/app/src/main/java/org/oxycblt/auxio/library/LibraryFragment.kt index 9efda2282..e6b3f4188 100644 --- a/app/src/main/java/org/oxycblt/auxio/library/LibraryFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/library/LibraryFragment.kt @@ -50,9 +50,12 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener { navToItem(it) } - val searchAdapter = SearchAdapter { - navToItem(it) - } + val searchAdapter = SearchAdapter( + { + navToItem(it) + }, + { data, view -> } + ) // --- UI SETUP --- diff --git a/app/src/main/java/org/oxycblt/auxio/library/adapters/SearchAdapter.kt b/app/src/main/java/org/oxycblt/auxio/library/adapters/SearchAdapter.kt index 985e01a85..266aec854 100644 --- a/app/src/main/java/org/oxycblt/auxio/library/adapters/SearchAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/library/adapters/SearchAdapter.kt @@ -1,5 +1,6 @@ package org.oxycblt.auxio.library.adapters +import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView @@ -17,7 +18,8 @@ import org.oxycblt.auxio.recycler.viewholders.HeaderViewHolder import org.oxycblt.auxio.recycler.viewholders.SongViewHolder class SearchAdapter( - private val doOnClick: (data: BaseModel) -> Unit + private val doOnClick: (data: BaseModel) -> Unit, + private val doOnLongClick: (data: BaseModel, view: View) -> Unit ) : ListAdapter(DiffCallback()) { override fun getItemViewType(position: Int): Int { @@ -35,7 +37,11 @@ class SearchAdapter( GenreViewHolder.ITEM_TYPE -> GenreViewHolder.from(parent.context, doOnClick) ArtistViewHolder.ITEM_TYPE -> ArtistViewHolder.from(parent.context, doOnClick) AlbumViewHolder.ITEM_TYPE -> AlbumViewHolder.from(parent.context, doOnClick) - SongViewHolder.ITEM_TYPE -> SongViewHolder.from(parent.context, doOnClick) + SongViewHolder.ITEM_TYPE -> SongViewHolder.from( + parent.context, + doOnClick, + doOnLongClick + ) HeaderViewHolder.ITEM_TYPE -> HeaderViewHolder.from(parent.context) else -> HeaderViewHolder.from(parent.context) diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt index c7fe578fc..05d0250a6 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt @@ -81,6 +81,7 @@ class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener { // Make marquee scroll work binding.playbackSong.isSelected = true + binding.playbackSeekBar.setOnSeekBarChangeListener(this) // --- VIEWMODEL SETUP -- @@ -178,7 +179,7 @@ class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener { playbackModel.nextItemsInQueue.observe(viewLifecycleOwner) { // Disable the option to open the queue if there's nothing in it. - if (it.isEmpty()) { + if (it.isEmpty() && playbackModel.userQueue.value!!.isEmpty()) { queueMenuItem.isEnabled = false queueMenuItem.icon = iconQueueInactive } else { @@ -195,6 +196,16 @@ class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener { } } + playbackModel.userQueue.observe(viewLifecycleOwner) { + if (it.isEmpty() && playbackModel.queue.value!!.isEmpty()) { + queueMenuItem.isEnabled = false + queueMenuItem.icon = iconQueueInactive + } else { + queueMenuItem.isEnabled = true + queueMenuItem.icon = iconQueueActive + } + } + Log.d(this::class.simpleName, "Fragment Created.") return binding.root diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt index 103a57ff4..6087284e6 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt @@ -33,6 +33,9 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback { private val mQueue = MutableLiveData(mutableListOf()) val queue: LiveData> get() = mQueue + private val mUserQueue = MutableLiveData(mutableListOf()) + val userQueue: LiveData> get() = mUserQueue + private val mIndex = MutableLiveData(0) val index: LiveData get() = mIndex @@ -169,6 +172,18 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback { playbackManager.moveQueueItems(from, to) } + fun addToUserQueue(song: Song) { + playbackManager.addToUserQueue(song) + } + + fun moveUserQueueItems(from: Int, to: Int) { + playbackManager.moveUserQueueItems(from, to) + } + + fun removeUserQueueItem(index: Int) { + playbackManager.removeUserQueueItem(index) + } + // --- STATUS FUNCTIONS --- // Flip the playing status. @@ -215,6 +230,10 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback { mQueue.value = queue } + override fun onUserQueueUpdate(userQueue: MutableList) { + mUserQueue.value = userQueue + } + override fun onIndexUpdate(index: Int) { mIndex.value = index } @@ -241,6 +260,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback { mSong.value = playbackManager.song mPosition.value = playbackManager.position / 1000 mQueue.value = playbackManager.queue + mUserQueue.value = playbackManager.userQueue mIndex.value = playbackManager.index mIsPlaying.value = playbackManager.isPlaying mIsShuffling.value = playbackManager.isShuffling diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt index 99f2f5902..95fa61611 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt @@ -25,7 +25,7 @@ class QueueAdapter( // Generic ViewHolder for a queue item inner class ViewHolder( private val binding: ItemQueueSongBinding, - ) : BaseViewHolder(binding, null) { + ) : BaseViewHolder(binding, null, null) { @SuppressLint("ClickableViewAccessibility") override fun onBind(data: Song) { diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueDragCallback.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueDragCallback.kt index bae506f83..93ad1ab5d 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueDragCallback.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueDragCallback.kt @@ -10,7 +10,8 @@ import kotlin.math.sign // The drag callback used for the Queue RecyclerView. class QueueDragCallback( - private val playbackModel: PlaybackViewModel + private val playbackModel: PlaybackViewModel, + private val isUserQueue: Boolean ) : ItemTouchHelper.SimpleCallback( ItemTouchHelper.UP or ItemTouchHelper.DOWN, ItemTouchHelper.START @@ -24,7 +25,6 @@ class QueueDragCallback( ): Int { // Fix to make QueueFragment scroll when an item is scrolled out of bounds. // Adapted from NewPipe: https://github.com/TeamNewPipe/NewPipe - val standardSpeed = super.interpolateOutOfBoundsScroll( recyclerView, viewSize, viewSizeOutOfBounds, totalSize, msSinceStartScroll ) @@ -45,13 +45,21 @@ class QueueDragCallback( viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder ): Boolean { - playbackModel.moveQueueItems(viewHolder.adapterPosition, target.adapterPosition) + if (isUserQueue) { + playbackModel.moveUserQueueItems(viewHolder.adapterPosition, target.adapterPosition) + } else { + playbackModel.moveQueueItems(viewHolder.adapterPosition, target.adapterPosition) + } return true } override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { - playbackModel.removeQueueItem(viewHolder.adapterPosition) + if (isUserQueue) { + playbackModel.removeUserQueueItem(viewHolder.adapterPosition) + } else { + playbackModel.removeQueueItem(viewHolder.adapterPosition) + } } companion object { diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt index afe3b10ad..65e85ec74 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt @@ -4,25 +4,17 @@ 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 androidx.viewpager2.adapter.FragmentStateAdapter import com.google.android.material.bottomsheet.BottomSheetDialogFragment -import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentQueueBinding import org.oxycblt.auxio.playback.PlaybackViewModel -import org.oxycblt.auxio.playback.state.PlaybackMode -import org.oxycblt.auxio.theme.accent -import org.oxycblt.auxio.theme.applyDivider -import org.oxycblt.auxio.theme.toColor +// TODO: Make this better class QueueFragment : BottomSheetDialogFragment() { private val playbackModel: PlaybackViewModel by activityViewModels() - override fun getTheme(): Int = R.style.Theme_BottomSheetFix - override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -30,58 +22,24 @@ class QueueFragment : BottomSheetDialogFragment() { ): View? { val binding = FragmentQueueBinding.inflate(inflater) - val helper = ItemTouchHelper(QueueDragCallback(playbackModel)) - val queueAdapter = QueueAdapter(helper) + binding.queueViewpager.adapter = PagerAdapter() - // --- UI SETUP --- - - binding.queueHeader.setTextColor(accent.first.toColor(requireContext())) - binding.queueRecycler.apply { - adapter = queueAdapter - itemAnimator = DefaultItemAnimator() - applyDivider() - setHasFixedSize(true) - - helper.attachToRecyclerView(this) - } - - // --- VIEWMODEL SETUP --- - - playbackModel.mode.observe(viewLifecycleOwner) { - if (it == PlaybackMode.ALL_SONGS) { - binding.queueHeader.setText(R.string.label_next_songs) - } else { - binding.queueHeader.text = getString( - R.string.format_next_from, playbackModel.parent.value!!.name - ) - } - } - - playbackModel.nextItemsInQueue.observe(viewLifecycleOwner) { - if (it.isEmpty()) { - findNavController().navigateUp() - - return@observe - } - - // If the first item is being moved, then scroll to the top position on completion - // to prevent ListAdapter from scrolling uncontrollably. - if (queueAdapter.currentList.isNotEmpty() && it[0].id != queueAdapter.currentList[0].id) { - queueAdapter.submitList(it.toMutableList()) { - // Make sure that the RecyclerView doesn't scroll to the top if the first item - // changed, but is not visible. - val firstItem = (binding.queueRecycler.layoutManager as LinearLayoutManager) - .findFirstVisibleItemPosition() - - if (firstItem == -1 || firstItem == 0) { - binding.queueRecycler.scrollToPosition(0) - } - } - } else { - queueAdapter.submitList(it.toMutableList()) - } + // TODO: Add option for default queue screen + if (playbackModel.userQueue.value!!.isEmpty()) { + binding.queueViewpager.setCurrentItem(1, false) + } else { + binding.queueViewpager.setCurrentItem(0, false) } return binding.root } + + private inner class PagerAdapter : + FragmentStateAdapter(childFragmentManager, viewLifecycleOwner.lifecycle) { + override fun getItemCount(): Int = 2 + + override fun createFragment(position: Int): Fragment { + return QueueListFragment(position) + } + } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueListFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueListFragment.kt new file mode 100644 index 000000000..36a7adb71 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueListFragment.kt @@ -0,0 +1,146 @@ +package org.oxycblt.auxio.playback.queue + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.DefaultItemAnimator +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.LinearLayoutManager +import org.oxycblt.auxio.R +import org.oxycblt.auxio.databinding.FragmentQueueListBinding +import org.oxycblt.auxio.playback.PlaybackViewModel +import org.oxycblt.auxio.playback.state.PlaybackMode +import org.oxycblt.auxio.theme.applyDivider + +class QueueListFragment(private val type: Int) : Fragment() { + private val playbackModel: PlaybackViewModel by activityViewModels() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + val binding = FragmentQueueListBinding.inflate(inflater) + + // --- UI SETUP --- + + binding.queueRecycler.apply { + itemAnimator = DefaultItemAnimator() + applyDivider() + setHasFixedSize(true) + } + + // Continue setup with different values depending on the type + when (type) { + TYPE_NEXT_QUEUE -> setupForNextQueue(binding) + TYPE_USER_QUEUE -> setupForUserQueue(binding) + } + + return binding.root + } + + private fun setupForNextQueue(binding: FragmentQueueListBinding) { + val helper = ItemTouchHelper(QueueDragCallback(playbackModel, false)) + val queueNextAdapter = QueueAdapter(helper) + + binding.queueRecycler.apply { + adapter = queueNextAdapter + helper.attachToRecyclerView(this) + } + + playbackModel.mode.observe(viewLifecycleOwner) { + if (it == PlaybackMode.ALL_SONGS) { + binding.queueHeader.setText(R.string.label_next_songs) + } else { + binding.queueHeader.text = getString( + R.string.format_next_from, playbackModel.parent.value!!.name + ) + } + } + + playbackModel.nextItemsInQueue.observe(viewLifecycleOwner) { + if (it.isEmpty()) { + if (playbackModel.userQueue.value!!.isEmpty()) { + findNavController().navigateUp() + } else { + binding.queueNothingIndicator.visibility = View.VISIBLE + binding.queueRecycler.visibility = View.GONE + } + + return@observe + } + + binding.queueNothingIndicator.visibility = View.GONE + binding.queueRecycler.visibility = View.VISIBLE + + // If the first item is being moved, then scroll to the top position on completion + // to prevent ListAdapter from scrolling uncontrollably. + if (queueNextAdapter.currentList.isNotEmpty() && + it[0].id != queueNextAdapter.currentList[0].id + ) { + queueNextAdapter.submitList(it.toMutableList()) { + scrollRecyclerIfNeeded(binding) + } + } else { + queueNextAdapter.submitList(it.toMutableList()) + } + } + } + + private fun setupForUserQueue(binding: FragmentQueueListBinding) { + val helper = ItemTouchHelper(QueueDragCallback(playbackModel, true)) + val userQueueAdapter = QueueAdapter(helper) + + binding.queueHeader.setText(R.string.label_next_user_queue) + + binding.queueRecycler.apply { + adapter = userQueueAdapter + helper.attachToRecyclerView(this) + } + + playbackModel.userQueue.observe(viewLifecycleOwner) { + if (it.isEmpty()) { + if (playbackModel.queue.value!!.isEmpty()) { + findNavController().navigateUp() + } else { + binding.queueNothingIndicator.visibility = View.VISIBLE + binding.queueRecycler.visibility = View.GONE + } + + return@observe + } + + binding.queueNothingIndicator.visibility = View.GONE + binding.queueRecycler.visibility = View.VISIBLE + + // If the first item is being moved, then scroll to the top position on completion + // to prevent ListAdapter from scrolling uncontrollably. + if (userQueueAdapter.currentList.isNotEmpty() && + it[0].id != userQueueAdapter.currentList[0].id + ) { + userQueueAdapter.submitList(it.toMutableList()) { + scrollRecyclerIfNeeded(binding) + } + } else { + userQueueAdapter.submitList(it.toMutableList()) + } + } + } + + private fun scrollRecyclerIfNeeded(binding: FragmentQueueListBinding) { + if ((binding.queueRecycler.layoutManager as LinearLayoutManager) + .findFirstVisibleItemPosition() < 1 + ) { + binding.queueRecycler.scrollToPosition(0) + } + } + + companion object { + const val TYPE_USER_QUEUE = 0 + const val TYPE_NEXT_QUEUE = 1 + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt index a30266273..ab05e3389 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt @@ -40,6 +40,12 @@ class PlaybackStateManager private constructor() { field = value callbacks.forEach { it.onQueueUpdate(value) } } + private var mUserQueue = mutableListOf() + set(value) { + Log.d(this::class.simpleName, "retard.") + field = value + callbacks.forEach { it.onUserQueueUpdate(value) } + } private var mIndex = 0 set(value) { field = value @@ -74,6 +80,7 @@ class PlaybackStateManager private constructor() { val parent: BaseModel? get() = mParent val position: Long get() = mPosition val queue: MutableList get() = mQueue + val userQueue: MutableList get() = mUserQueue val index: Int get() = mIndex val mode: PlaybackMode get() = mMode val isPlaying: Boolean get() = mIsPlaying @@ -252,10 +259,8 @@ class PlaybackStateManager private constructor() { fun moveQueueItems(from: Int, to: Int) { try { - val currentItem = mQueue[from] - - mQueue.removeAt(from) - mQueue.add(to, currentItem) + val item = mQueue.removeAt(from) + mQueue.add(to, item) } catch (exception: IndexOutOfBoundsException) { Log.e(this::class.simpleName, "Indices were out of bounds, did not move queue item") @@ -265,11 +270,48 @@ class PlaybackStateManager private constructor() { forceQueueUpdate() } + fun addToUserQueue(song: Song) { + mUserQueue.add(song) + + forceUserQueueUpdate() + } + + fun removeUserQueueItem(index: Int) { + Log.d(this::class.simpleName, "Removing item ${mUserQueue[index].name}.") + + if (index > mUserQueue.size || index < 0) { + Log.e(this::class.simpleName, "Index is out of bounds, did not remove queue item.") + + return + } + + mUserQueue.removeAt(index) + + forceUserQueueUpdate() + } + + fun moveUserQueueItems(from: Int, to: Int) { + try { + val item = mUserQueue.removeAt(from) + mUserQueue.add(to, item) + } catch (exception: IndexOutOfBoundsException) { + Log.e(this::class.simpleName, "Indices were out of bounds, did not move queue item") + + return + } + + forceUserQueueUpdate() + } + // Force any callbacks to update when the queue is changed. private fun forceQueueUpdate() { mQueue = mQueue } + private fun forceUserQueueUpdate() { + mUserQueue = mUserQueue + } + // --- SHUFFLE FUNCTIONS --- fun shuffleAll() { @@ -408,6 +450,7 @@ class PlaybackStateManager private constructor() { fun onParentUpdate(parent: BaseModel?) {} fun onPositionUpdate(position: Long) {} fun onQueueUpdate(queue: MutableList) {} + fun onUserQueueUpdate(userQueue: MutableList) {} fun onModeUpdate(mode: PlaybackMode) {} fun onIndexUpdate(index: Int) {} fun onPlayingUpdate(isPlaying: Boolean) {} diff --git a/app/src/main/java/org/oxycblt/auxio/recycler/viewholders/BaseViewHolder.kt b/app/src/main/java/org/oxycblt/auxio/recycler/viewholders/BaseViewHolder.kt index 72a248fd7..b468d65bd 100644 --- a/app/src/main/java/org/oxycblt/auxio/recycler/viewholders/BaseViewHolder.kt +++ b/app/src/main/java/org/oxycblt/auxio/recycler/viewholders/BaseViewHolder.kt @@ -1,5 +1,6 @@ package org.oxycblt.auxio.recycler.viewholders +import android.view.View import androidx.databinding.ViewDataBinding import androidx.recyclerview.widget.RecyclerView import org.oxycblt.auxio.music.BaseModel @@ -7,7 +8,8 @@ import org.oxycblt.auxio.music.BaseModel // ViewHolder abstraction that automates some of the things that are common for all ViewHolders. abstract class BaseViewHolder( private val baseBinding: ViewDataBinding, - private val doOnClick: ((data: T) -> Unit)? + private val doOnClick: ((data: T) -> Unit)?, + private val doOnLongClick: ((data: T, view: View) -> Unit)? ) : RecyclerView.ViewHolder(baseBinding.root) { init { // Force the layout to *actually* be the screen width @@ -23,6 +25,14 @@ abstract class BaseViewHolder( } } + doOnLongClick?.let { onLongClick -> + baseBinding.root.setOnLongClickListener { + onLongClick(data, baseBinding.root) + + true + } + } + onBind(data) baseBinding.executePendingBindings() diff --git a/app/src/main/java/org/oxycblt/auxio/recycler/viewholders/ModelHolders.kt b/app/src/main/java/org/oxycblt/auxio/recycler/viewholders/ModelHolders.kt index 10901c70b..22b7853a4 100644 --- a/app/src/main/java/org/oxycblt/auxio/recycler/viewholders/ModelHolders.kt +++ b/app/src/main/java/org/oxycblt/auxio/recycler/viewholders/ModelHolders.kt @@ -2,6 +2,7 @@ package org.oxycblt.auxio.recycler.viewholders import android.content.Context import android.view.LayoutInflater +import android.view.View import org.oxycblt.auxio.databinding.ItemAlbumBinding import org.oxycblt.auxio.databinding.ItemArtistBinding import org.oxycblt.auxio.databinding.ItemGenreBinding @@ -19,7 +20,7 @@ import org.oxycblt.auxio.music.Song class GenreViewHolder private constructor( private val binding: ItemGenreBinding, doOnClick: (Genre) -> Unit -) : BaseViewHolder(binding, doOnClick) { +) : BaseViewHolder(binding, doOnClick, null) { override fun onBind(data: Genre) { binding.genre = data @@ -41,7 +42,7 @@ class GenreViewHolder private constructor( class ArtistViewHolder private constructor( private val binding: ItemArtistBinding, doOnClick: (Artist) -> Unit, -) : BaseViewHolder(binding, doOnClick) { +) : BaseViewHolder(binding, doOnClick, null) { override fun onBind(data: Artist) { binding.artist = data @@ -63,7 +64,7 @@ class ArtistViewHolder private constructor( class AlbumViewHolder private constructor( private val binding: ItemAlbumBinding, doOnClick: (data: Album) -> Unit -) : BaseViewHolder(binding, doOnClick) { +) : BaseViewHolder(binding, doOnClick, null) { override fun onBind(data: Album) { binding.album = data @@ -87,7 +88,8 @@ class AlbumViewHolder private constructor( class SongViewHolder private constructor( private val binding: ItemSongBinding, doOnClick: (data: Song) -> Unit, -) : BaseViewHolder(binding, doOnClick) { + doOnLongClick: (data: Song, view: View) -> Unit +) : BaseViewHolder(binding, doOnClick, doOnLongClick) { override fun onBind(data: Song) { binding.song = data @@ -102,10 +104,11 @@ class SongViewHolder private constructor( fun from( context: Context, doOnClick: (data: Song) -> Unit, + doOnLongClick: (data: Song, view: View) -> Unit ): SongViewHolder { return SongViewHolder( ItemSongBinding.inflate(LayoutInflater.from(context)), - doOnClick + doOnClick, doOnLongClick ) } } @@ -113,7 +116,7 @@ class SongViewHolder private constructor( class HeaderViewHolder( private val binding: ItemHeaderBinding -) : BaseViewHolder
(binding, null) { +) : BaseViewHolder
(binding, null, null) { override fun onBind(data: Header) { binding.header = data diff --git a/app/src/main/java/org/oxycblt/auxio/songs/SongAdapter.kt b/app/src/main/java/org/oxycblt/auxio/songs/SongAdapter.kt index e46738820..f19663ca5 100644 --- a/app/src/main/java/org/oxycblt/auxio/songs/SongAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/songs/SongAdapter.kt @@ -1,5 +1,6 @@ package org.oxycblt.auxio.songs +import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import org.oxycblt.auxio.music.Song @@ -7,13 +8,14 @@ import org.oxycblt.auxio.recycler.viewholders.SongViewHolder class SongAdapter( private val data: List, - private val doOnClick: (data: Song) -> Unit + private val doOnClick: (data: Song) -> Unit, + private val doOnLongClick: (data: Song, view: View) -> Unit ) : RecyclerView.Adapter() { override fun getItemCount(): Int = data.size override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SongViewHolder { - return SongViewHolder.from(parent.context, doOnClick) + return SongViewHolder.from(parent.context, doOnClick, doOnLongClick) } override fun onBindViewHolder(holder: SongViewHolder, position: Int) { diff --git a/app/src/main/java/org/oxycblt/auxio/songs/SongsFragment.kt b/app/src/main/java/org/oxycblt/auxio/songs/SongsFragment.kt index ca979eceb..ec574788b 100644 --- a/app/src/main/java/org/oxycblt/auxio/songs/SongsFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/songs/SongsFragment.kt @@ -5,11 +5,13 @@ import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.PopupMenu import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentSongsBinding import org.oxycblt.auxio.music.MusicStore +import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.state.PlaybackMode import org.oxycblt.auxio.theme.applyDivider @@ -39,9 +41,15 @@ class SongsFragment : Fragment() { } binding.songRecycler.apply { - adapter = SongAdapter(musicStore.songs) { - playbackModel.playSong(it, PlaybackMode.ALL_SONGS) - } + adapter = SongAdapter( + musicStore.songs, + { + playbackModel.playSong(it, PlaybackMode.ALL_SONGS) + }, + { data, view -> + showActionMenuForSong(data, view) + } + ) applyDivider() setHasFixedSize(true) } @@ -50,4 +58,21 @@ class SongsFragment : Fragment() { return binding.root } + + private fun showActionMenuForSong(song: Song, view: View) { + // TODO: Replace this with something nicer + PopupMenu(requireContext(), view).apply { + inflate(R.menu.menu_song_actions) + setOnMenuItemClickListener { + if (it.itemId == R.id.action_queue_add) { + playbackModel.addToUserQueue(song) + + return@setOnMenuItemClickListener true + } + + false + } + show() + } + } } diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml index 602dbf3a2..cc71a811b 100644 --- a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -11,10 +11,16 @@ - - + android:startX="60" + android:startY="61.5" + android:endX="64.75" + android:endY="28.5"> + + diff --git a/app/src/main/res/drawable/ic_exit.xml b/app/src/main/res/drawable/ic_exit.xml index 575ae7453..ad4d40ce5 100644 --- a/app/src/main/res/drawable/ic_exit.xml +++ b/app/src/main/res/drawable/ic_exit.xml @@ -4,7 +4,7 @@ android:height="24dp" android:viewportWidth="24" android:viewportHeight="24"> - + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml index 602dbf3a2..cc71a811b 100644 --- a/app/src/main/res/drawable/ic_launcher_foreground.xml +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -11,10 +11,16 @@ - - + android:startX="60" + android:startY="61.5" + android:endX="64.75" + android:endY="28.5"> + + diff --git a/app/src/main/res/drawable/ic_loop.xml b/app/src/main/res/drawable/ic_loop.xml index 08649b001..70bf9c913 100644 --- a/app/src/main/res/drawable/ic_loop.xml +++ b/app/src/main/res/drawable/ic_loop.xml @@ -5,7 +5,7 @@ android:viewportWidth="24" android:viewportHeight="24" android:tint="?attr/colorControlNormal"> - + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_loop_disabled.xml b/app/src/main/res/drawable/ic_loop_disabled.xml index fcf057385..d43dd4115 100644 --- a/app/src/main/res/drawable/ic_loop_disabled.xml +++ b/app/src/main/res/drawable/ic_loop_disabled.xml @@ -4,7 +4,7 @@ android:height="24dp" android:viewportWidth="24" android:viewportHeight="24"> - + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_loop_large.xml b/app/src/main/res/drawable/ic_loop_large.xml index 0916f8d99..f9210f969 100644 --- a/app/src/main/res/drawable/ic_loop_large.xml +++ b/app/src/main/res/drawable/ic_loop_large.xml @@ -5,7 +5,7 @@ android:viewportWidth="24" android:viewportHeight="24" android:tint="?attr/colorControlNormal"> - + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_loop_one.xml b/app/src/main/res/drawable/ic_loop_one.xml index 2226d7fbe..99ce9ffc7 100644 --- a/app/src/main/res/drawable/ic_loop_one.xml +++ b/app/src/main/res/drawable/ic_loop_one.xml @@ -5,7 +5,7 @@ android:viewportWidth="24" android:viewportHeight="24" android:tint="?attr/colorControlNormal"> - + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_loop_one_large.xml b/app/src/main/res/drawable/ic_loop_one_large.xml index 30747a1a9..d3d0bf894 100644 --- a/app/src/main/res/drawable/ic_loop_one_large.xml +++ b/app/src/main/res/drawable/ic_loop_one_large.xml @@ -5,7 +5,7 @@ android:viewportWidth="24" android:viewportHeight="24" android:tint="?attr/colorControlNormal"> - + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_pause.xml b/app/src/main/res/drawable/ic_pause.xml index f8e0203a3..4c8d64180 100644 --- a/app/src/main/res/drawable/ic_pause.xml +++ b/app/src/main/res/drawable/ic_pause.xml @@ -5,6 +5,7 @@ android:viewportWidth="24" android:viewportHeight="24" android:tint="?android:attr/colorControlNormal"> - + diff --git a/app/src/main/res/drawable/ic_play.xml b/app/src/main/res/drawable/ic_play.xml index d22f9c07c..c3fe7c7e8 100644 --- a/app/src/main/res/drawable/ic_play.xml +++ b/app/src/main/res/drawable/ic_play.xml @@ -5,6 +5,7 @@ android:viewportWidth="24" android:viewportHeight="24" android:tint="?attr/colorControlNormal"> - + diff --git a/app/src/main/res/drawable/ic_user_queue.xml b/app/src/main/res/drawable/ic_user_queue.xml new file mode 100644 index 000000000..856e29a62 --- /dev/null +++ b/app/src/main/res/drawable/ic_user_queue.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_playback.xml b/app/src/main/res/layout/fragment_playback.xml index 19821a80b..2e4c9becf 100644 --- a/app/src/main/res/layout/fragment_playback.xml +++ b/app/src/main/res/layout/fragment_playback.xml @@ -15,6 +15,7 @@ type="org.oxycblt.auxio.playback.PlaybackViewModel" /> + + android:contentDescription="@string/description_loop" /> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_queue.xml b/app/src/main/res/layout/fragment_queue.xml index fbc3bbf12..5ab6c8597 100644 --- a/app/src/main/res/layout/fragment_queue.xml +++ b/app/src/main/res/layout/fragment_queue.xml @@ -2,28 +2,15 @@ - + android:layout_height="match_parent"> - + android:layout_height="match_parent" /> - - - + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_queue_list.xml b/app/src/main/res/layout/fragment_queue_list.xml new file mode 100644 index 000000000..52e8f39f5 --- /dev/null +++ b/app/src/main/res/layout/fragment_queue_list.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_song_actions.xml b/app/src/main/res/menu/menu_song_actions.xml new file mode 100644 index 000000000..125309274 --- /dev/null +++ b/app/src/main/res/menu/menu_song_actions.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index bbd3e0212..eca70cfe5 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,5 +1,5 @@ - - + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index bbd3e0212..eca70cfe5 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -1,5 +1,5 @@ - - + + \ No newline at end of file diff --git a/app/src/main/res/navigation/nav_main.xml b/app/src/main/res/navigation/nav_main.xml index 7d6cc3b9b..6a3b633ec 100644 --- a/app/src/main/res/navigation/nav_main.xml +++ b/app/src/main/res/navigation/nav_main.xml @@ -115,14 +115,18 @@ android:id="@+id/playback_fragment" android:name="org.oxycblt.auxio.playback.PlaybackFragment" android:label="PlaybackFragment" - tools:layout="@layout/fragment_playback" > + tools:layout="@layout/fragment_playback"> + app:destination="@id/queue_fragment" + app:enterAnim="@anim/anim_nav_slide_up" + app:exitAnim="@anim/anim_stationary" + app:popEnterAnim="@anim/anim_stationary" + app:popExitAnim="@anim/anim_nav_slide_down" /> + tools:layout="@layout/fragment_queue" /> \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index aadd0c25d..ed599206e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -27,7 +27,9 @@ Play Queue Add to queue + Next in Queue Next from: All Songs + Nothing here. Music Playback The music playback service for Auxio. diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index fa604bdb3..bd5ff68b7 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -8,6 +8,7 @@ @drawable/ui_cursor true + @style/Widget.CustomPopup @color/control_color @@ -41,6 +42,11 @@ @color/selection_color + +