Add ability to play from user queue

Add the ability to play from the user queue, also append some extra song actions.
This commit is contained in:
OxygenCobalt 2020-11-08 10:10:36 -07:00
parent 2be7d34601
commit 4fb4120342
14 changed files with 160 additions and 136 deletions

View file

@ -7,7 +7,7 @@ import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import org.oxycblt.auxio.playback.PlaybackService
import org.oxycblt.auxio.theme.accent
import org.oxycblt.auxio.ui.accent
// FIXME: Fix bug where fast navigation will break the animations and
// lead to nothing being displayed [Possibly Un-fixable]

View file

@ -17,10 +17,10 @@ import org.oxycblt.auxio.library.LibraryFragment
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.songs.SongsFragment
import org.oxycblt.auxio.theme.accent
import org.oxycblt.auxio.theme.getInactiveAlpha
import org.oxycblt.auxio.theme.getTransparentAccent
import org.oxycblt.auxio.theme.toColor
import org.oxycblt.auxio.ui.accent
import org.oxycblt.auxio.ui.getInactiveAlpha
import org.oxycblt.auxio.ui.getTransparentAccent
import org.oxycblt.auxio.ui.toColor
class MainFragment : Fragment() {
private val playbackModel: PlaybackViewModel by activityViewModels()

View file

@ -15,8 +15,8 @@ import org.oxycblt.auxio.detail.adapters.DetailSongAdapter
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.playback.state.PlaybackMode
import org.oxycblt.auxio.theme.applyDivider
import org.oxycblt.auxio.theme.disable
import org.oxycblt.auxio.ui.applyDivider
import org.oxycblt.auxio.ui.disable
class AlbumDetailFragment : Fragment() {

View file

@ -14,8 +14,8 @@ import org.oxycblt.auxio.databinding.FragmentArtistDetailBinding
import org.oxycblt.auxio.detail.adapters.DetailAlbumAdapter
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.theme.applyDivider
import org.oxycblt.auxio.theme.disable
import org.oxycblt.auxio.ui.applyDivider
import org.oxycblt.auxio.ui.disable
class ArtistDetailFragment : Fragment() {
private val args: ArtistDetailFragmentArgs by navArgs()

View file

@ -14,8 +14,8 @@ import org.oxycblt.auxio.databinding.FragmentGenreDetailBinding
import org.oxycblt.auxio.detail.adapters.DetailArtistAdapter
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.theme.applyDivider
import org.oxycblt.auxio.theme.disable
import org.oxycblt.auxio.ui.applyDivider
import org.oxycblt.auxio.ui.disable
class GenreDetailFragment : Fragment() {

View file

@ -27,9 +27,10 @@ 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.applyColor
import org.oxycblt.auxio.theme.applyDivider
import org.oxycblt.auxio.theme.resolveAttr
import org.oxycblt.auxio.ui.applyColor
import org.oxycblt.auxio.ui.applyDivider
import org.oxycblt.auxio.ui.resolveAttr
import org.oxycblt.auxio.ui.showActionMenuForSong
// A Fragment to show all the music in the Library.
class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
@ -54,7 +55,11 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
{
navToItem(it)
},
{ data, view -> }
{ data, view ->
if (data is Song) {
showActionMenuForSong(requireContext(), data, view, playbackModel)
}
}
)
// --- UI SETUP ---

View file

@ -16,10 +16,8 @@ import androidx.navigation.fragment.findNavController
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentPlaybackBinding
import org.oxycblt.auxio.playback.state.LoopMode
import org.oxycblt.auxio.theme.accent
import org.oxycblt.auxio.theme.disable
import org.oxycblt.auxio.theme.enable
import org.oxycblt.auxio.theme.toColor
import org.oxycblt.auxio.ui.accent
import org.oxycblt.auxio.ui.toColor
// TODO: Add a swipe-to-next-track function using a ViewPager
class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
@ -99,20 +97,6 @@ class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
}
}
playbackModel.index.observe(viewLifecycleOwner) {
if (it > 0) {
binding.playbackSkipPrev.enable(requireContext())
} else {
binding.playbackSkipPrev.disable(requireContext())
}
if (it < playbackModel.queue.value!!.lastIndex) {
binding.playbackSkipNext.enable(requireContext())
} else {
binding.playbackSkipNext.disable(requireContext())
}
}
playbackModel.isPlaying.observe(viewLifecycleOwner) {
if (it) {
// Animate the playing status and switch the button to the accent color
@ -186,14 +170,6 @@ class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
queueMenuItem.isEnabled = true
queueMenuItem.icon = iconQueueActive
}
// If someone edits the queue to make it have no songs left, then disable the
// skip next button.
if (playbackModel.index.value!! == it.size) {
binding.playbackSkipNext.disable(requireContext())
} else {
binding.playbackSkipNext.enable(requireContext())
}
}
playbackModel.userQueue.observe(viewLifecycleOwner) {

View file

@ -14,7 +14,7 @@ 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
import org.oxycblt.auxio.ui.applyDivider
class QueueListFragment(private val type: Int) : Fragment() {
private val playbackModel: PlaybackViewModel by activityViewModels()
@ -53,14 +53,12 @@ class QueueListFragment(private val type: Int) : Fragment() {
}
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
R.string.format_next_from,
if (it == PlaybackMode.ALL_SONGS) getString(R.string.title_all_songs)
else playbackModel.parent.value!!.name
)
}
}
playbackModel.nextItemsInQueue.observe(viewLifecycleOwner) {
if (it.isEmpty()) {

View file

@ -216,6 +216,12 @@ class PlaybackStateManager private constructor() {
fun next() {
resetLoopMode()
if (mUserQueue.isNotEmpty()) {
updatePlayback(mUserQueue[0])
mUserQueue.removeAt(0)
forceUserQueueUpdate()
} else {
if (mIndex < mQueue.lastIndex) {
mIndex = mIndex.inc()
} else {
@ -230,6 +236,7 @@ class PlaybackStateManager private constructor() {
forceQueueUpdate()
}
}
fun prev() {
if (mIndex > 0) {

View file

@ -5,16 +5,15 @@ 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
import org.oxycblt.auxio.ui.applyDivider
import org.oxycblt.auxio.ui.showActionMenuForSong
class SongsFragment : Fragment() {
private val playbackModel: PlaybackViewModel by activityViewModels()
@ -47,7 +46,7 @@ class SongsFragment : Fragment() {
playbackModel.playSong(it, PlaybackMode.ALL_SONGS)
},
{ data, view ->
showActionMenuForSong(data, view)
showActionMenuForSong(requireContext(), data, view, playbackModel)
}
)
applyDivider()
@ -58,21 +57,4 @@ 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

@ -0,0 +1,98 @@
package org.oxycblt.auxio.ui
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.drawable.ColorDrawable
import android.text.SpannableString
import android.text.style.ForegroundColorSpan
import android.view.MenuItem
import android.view.View
import android.widget.ImageButton
import android.widget.PopupMenu
import android.widget.Toast
import androidx.annotation.ColorInt
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.playback.state.PlaybackMode
// Functions for managing UI elements [Not Colors]
fun showActionMenuForSong(
context: Context,
song: Song,
view: View,
playbackModel: PlaybackViewModel
) {
// TODO: Replace this with a BottomSheet dialog?
PopupMenu(context, view).apply {
inflate(R.menu.menu_song_actions)
setOnMenuItemClickListener {
return@setOnMenuItemClickListener when (it.itemId) {
R.id.action_queue_add -> {
playbackModel.addToUserQueue(song)
Toast.makeText(
context,
context.getString(R.string.label_queue_added),
Toast.LENGTH_SHORT
).show()
true
}
R.id.action_play_artist -> {
playbackModel.playSong(song, PlaybackMode.IN_ARTIST)
true
}
R.id.action_play_album -> {
playbackModel.playSong(song, PlaybackMode.IN_ALBUM)
true
}
else -> false
}
}
show()
}
}
// Apply a color to a Menu Item
fun MenuItem.applyColor(@ColorInt color: Int) {
SpannableString(title).apply {
setSpan(ForegroundColorSpan(color), 0, length, SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE)
title = this
}
}
// Disable an ImageButton
fun ImageButton.disable(context: Context) {
if (isEnabled) {
imageTintList = ColorStateList.valueOf(
R.color.inactive_color.toColor(context)
)
isEnabled = false
}
}
// Apply a custom vertical divider
fun RecyclerView.applyDivider() {
val div = DividerItemDecoration(
context,
DividerItemDecoration.VERTICAL
)
div.setDrawable(
ColorDrawable(
R.color.divider_color.toColor(context)
)
)
addItemDecoration(div)
}

View file

@ -1,22 +1,16 @@
package org.oxycblt.auxio.theme
package org.oxycblt.auxio.ui
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.drawable.ColorDrawable
import android.text.SpannableString
import android.text.style.ForegroundColorSpan
import android.util.TypedValue
import android.view.MenuItem
import android.widget.ImageButton
import androidx.annotation.AttrRes
import androidx.annotation.ColorInt
import androidx.annotation.ColorRes
import androidx.core.content.ContextCompat
import androidx.core.graphics.ColorUtils
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.R
// Functions for managing colors/accents/whatever.
// Pairs of the base accent and its theme
private val ACCENTS = listOf(
Pair(R.color.red, R.style.Theme_Red), // 0
@ -85,49 +79,3 @@ fun resolveAttr(context: Context, @AttrRes attr: Int): Int {
return color.toColor(context)
}
// Apply a color to a Menu Item
fun MenuItem.applyColor(@ColorInt color: Int) {
SpannableString(title).apply {
setSpan(ForegroundColorSpan(color), 0, length, SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE)
title = this
}
}
// Disable an ImageButton
fun ImageButton.disable(context: Context) {
if (isEnabled) {
imageTintList = ColorStateList.valueOf(
R.color.inactive_color.toColor(context)
)
isEnabled = false
}
}
// Enable an ImageButton
fun ImageButton.enable(context: Context) {
if (!isEnabled) {
imageTintList = ColorStateList.valueOf(
R.color.control_color.toColor(context)
)
isEnabled = true
}
}
// Apply a custom vertical divider
fun RecyclerView.applyDivider() {
val div = DividerItemDecoration(
context,
DividerItemDecoration.VERTICAL
)
div.setDrawable(
ColorDrawable(
R.color.divider_color.toColor(context)
)
)
addItemDecoration(div)
}

View file

@ -4,4 +4,12 @@
android:id="@+id/action_queue_add"
android:title="@string/label_queue_add"
android:icon="@drawable/ic_user_queue" />
<item
android:id="@+id/action_play_artist"
android:title="@string/label_play_artist"
android:icon="@drawable/ic_artist" />
<item
android:id="@+id/action_play_album"
android:title="@string/label_play_album"
android:icon="@drawable/ic_album" />
</menu>

View file

@ -25,10 +25,12 @@
<string name="label_sort_alpha_up">Z-A</string>
<string name="label_shuffle">Shuffle</string>
<string name="label_play">Play</string>
<string name="label_play_artist">Play from artist</string>
<string name="label_play_album">Play from album</string>
<string name="label_queue">Queue</string>
<string name="label_queue_add">Add to queue</string>
<string name="label_queue_added">Added 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>