Add user queue

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<BaseModel, RecyclerView.ViewHolder>(DiffCallback<BaseModel>()) {
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)

View file

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

View file

@ -33,6 +33,9 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
private val mQueue = MutableLiveData(mutableListOf<Song>())
val queue: LiveData<MutableList<Song>> get() = mQueue
private val mUserQueue = MutableLiveData(mutableListOf<Song>())
val userQueue: LiveData<MutableList<Song>> get() = mUserQueue
private val mIndex = MutableLiveData(0)
val index: LiveData<Int> 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<Song>) {
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

View file

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

View file

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

View file

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

View file

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

View file

@ -40,6 +40,12 @@ class PlaybackStateManager private constructor() {
field = value
callbacks.forEach { it.onQueueUpdate(value) }
}
private var mUserQueue = mutableListOf<Song>()
set(value) {
Log.d(this::class.simpleName, "retard.")
field = value
callbacks.forEach { it.onUserQueueUpdate(value) }
}
private var mIndex = 0
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<Song> get() = mQueue
val userQueue: MutableList<Song> 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<Song>) {}
fun onUserQueueUpdate(userQueue: MutableList<Song>) {}
fun onModeUpdate(mode: PlaybackMode) {}
fun onIndexUpdate(index: Int) {}
fun onPlayingUpdate(isPlaying: Boolean) {}

View file

@ -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<T : BaseModel>(
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<T : BaseModel>(
}
}
doOnLongClick?.let { onLongClick ->
baseBinding.root.setOnLongClickListener {
onLongClick(data, baseBinding.root)
true
}
}
onBind(data)
baseBinding.executePendingBindings()

View file

@ -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<Genre>(binding, doOnClick) {
) : BaseViewHolder<Genre>(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<Artist>(binding, doOnClick) {
) : BaseViewHolder<Artist>(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<Album>(binding, doOnClick) {
) : BaseViewHolder<Album>(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<Song>(binding, doOnClick) {
doOnLongClick: (data: Song, view: View) -> Unit
) : BaseViewHolder<Song>(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<Header>(binding, null) {
) : BaseViewHolder<Header>(binding, null, null) {
override fun onBind(data: Header) {
binding.header = data

View file

@ -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<Song>,
private val doOnClick: (data: Song) -> Unit
private val doOnClick: (data: Song) -> Unit,
private val doOnLongClick: (data: Song, view: View) -> Unit
) : RecyclerView.Adapter<SongViewHolder>() {
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) {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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">
<action
android:id="@+id/action_show_queue"
app:destination="@id/queue_fragment" />
app:destination="@id/queue_fragment"
app:enterAnim="@anim/anim_nav_slide_up"
app:exitAnim="@anim/anim_stationary"
app:popEnterAnim="@anim/anim_stationary"
app:popExitAnim="@anim/anim_nav_slide_down" />
</fragment>
<dialog
android:id="@+id/queue_fragment"
android:name="org.oxycblt.auxio.playback.queue.QueueFragment"
android:label="QueueFragment"
tools:layout="@layout/fragment_queue"/>
tools:layout="@layout/fragment_queue" />
</navigation>

View file

@ -27,7 +27,9 @@
<string name="label_play">Play</string>
<string name="label_queue">Queue</string>
<string name="label_queue_add">Add to queue</string>
<string name="label_next_user_queue">Next in Queue</string>
<string name="label_next_songs">Next from: All Songs</string>
<string name="label_empty_queue">Nothing here.</string>
<string name="label_notification_playback">Music Playback</string>
<string name="label_service_playback">The music playback service for Auxio.</string>

View file

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