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.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import org.oxycblt.auxio.playback.PlaybackService 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 // FIXME: Fix bug where fast navigation will break the animations and
// lead to nothing being displayed [Possibly Un-fixable] // 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.music.MusicStore
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.songs.SongsFragment import org.oxycblt.auxio.songs.SongsFragment
import org.oxycblt.auxio.theme.accent import org.oxycblt.auxio.ui.accent
import org.oxycblt.auxio.theme.getInactiveAlpha import org.oxycblt.auxio.ui.getInactiveAlpha
import org.oxycblt.auxio.theme.getTransparentAccent import org.oxycblt.auxio.ui.getTransparentAccent
import org.oxycblt.auxio.theme.toColor import org.oxycblt.auxio.ui.toColor
class MainFragment : Fragment() { class MainFragment : Fragment() {
private val playbackModel: PlaybackViewModel by activityViewModels() 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.music.MusicStore
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.playback.state.PlaybackMode import org.oxycblt.auxio.playback.state.PlaybackMode
import org.oxycblt.auxio.theme.applyDivider import org.oxycblt.auxio.ui.applyDivider
import org.oxycblt.auxio.theme.disable import org.oxycblt.auxio.ui.disable
class AlbumDetailFragment : Fragment() { 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.detail.adapters.DetailAlbumAdapter
import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.theme.applyDivider import org.oxycblt.auxio.ui.applyDivider
import org.oxycblt.auxio.theme.disable import org.oxycblt.auxio.ui.disable
class ArtistDetailFragment : Fragment() { class ArtistDetailFragment : Fragment() {
private val args: ArtistDetailFragmentArgs by navArgs() 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.detail.adapters.DetailArtistAdapter
import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.theme.applyDivider import org.oxycblt.auxio.ui.applyDivider
import org.oxycblt.auxio.theme.disable import org.oxycblt.auxio.ui.disable
class GenreDetailFragment : Fragment() { 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.music.Song
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.playback.state.PlaybackMode import org.oxycblt.auxio.playback.state.PlaybackMode
import org.oxycblt.auxio.theme.applyColor import org.oxycblt.auxio.ui.applyColor
import org.oxycblt.auxio.theme.applyDivider import org.oxycblt.auxio.ui.applyDivider
import org.oxycblt.auxio.theme.resolveAttr import org.oxycblt.auxio.ui.resolveAttr
import org.oxycblt.auxio.ui.showActionMenuForSong
// A Fragment to show all the music in the Library. // A Fragment to show all the music in the Library.
class LibraryFragment : Fragment(), SearchView.OnQueryTextListener { class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
@ -54,7 +55,11 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
{ {
navToItem(it) navToItem(it)
}, },
{ data, view -> } { data, view ->
if (data is Song) {
showActionMenuForSong(requireContext(), data, view, playbackModel)
}
}
) )
// --- UI SETUP --- // --- UI SETUP ---

View file

@ -16,10 +16,8 @@ import androidx.navigation.fragment.findNavController
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentPlaybackBinding import org.oxycblt.auxio.databinding.FragmentPlaybackBinding
import org.oxycblt.auxio.playback.state.LoopMode import org.oxycblt.auxio.playback.state.LoopMode
import org.oxycblt.auxio.theme.accent import org.oxycblt.auxio.ui.accent
import org.oxycblt.auxio.theme.disable import org.oxycblt.auxio.ui.toColor
import org.oxycblt.auxio.theme.enable
import org.oxycblt.auxio.theme.toColor
// TODO: Add a swipe-to-next-track function using a ViewPager // TODO: Add a swipe-to-next-track function using a ViewPager
class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener { 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) { playbackModel.isPlaying.observe(viewLifecycleOwner) {
if (it) { if (it) {
// Animate the playing status and switch the button to the accent color // 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.isEnabled = true
queueMenuItem.icon = iconQueueActive 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) { 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.databinding.FragmentQueueListBinding
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.playback.state.PlaybackMode 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() { class QueueListFragment(private val type: Int) : Fragment() {
private val playbackModel: PlaybackViewModel by activityViewModels() private val playbackModel: PlaybackViewModel by activityViewModels()
@ -53,13 +53,11 @@ class QueueListFragment(private val type: Int) : Fragment() {
} }
playbackModel.mode.observe(viewLifecycleOwner) { playbackModel.mode.observe(viewLifecycleOwner) {
if (it == PlaybackMode.ALL_SONGS) { binding.queueHeader.text = getString(
binding.queueHeader.setText(R.string.label_next_songs) R.string.format_next_from,
} else { if (it == PlaybackMode.ALL_SONGS) getString(R.string.title_all_songs)
binding.queueHeader.text = getString( else playbackModel.parent.value!!.name
R.string.format_next_from, playbackModel.parent.value!!.name )
)
}
} }
playbackModel.nextItemsInQueue.observe(viewLifecycleOwner) { playbackModel.nextItemsInQueue.observe(viewLifecycleOwner) {

View file

@ -216,19 +216,26 @@ class PlaybackStateManager private constructor() {
fun next() { fun next() {
resetLoopMode() resetLoopMode()
if (mIndex < mQueue.lastIndex) { if (mUserQueue.isNotEmpty()) {
mIndex = mIndex.inc() updatePlayback(mUserQueue[0])
mUserQueue.removeAt(0)
forceUserQueueUpdate()
} else { } else {
// TODO: Implement option to make the playlist loop instead of stop if (mIndex < mQueue.lastIndex) {
mQueue = mutableListOf() mIndex = mIndex.inc()
mSong = null } else {
// TODO: Implement option to make the playlist loop instead of stop
mQueue = mutableListOf()
mSong = null
return return
}
updatePlayback(mQueue[mIndex])
forceQueueUpdate()
} }
updatePlayback(mQueue[mIndex])
forceQueueUpdate()
} }
fun prev() { fun prev() {

View file

@ -5,16 +5,15 @@ import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.PopupMenu
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentSongsBinding import org.oxycblt.auxio.databinding.FragmentSongsBinding
import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.playback.state.PlaybackMode 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() { class SongsFragment : Fragment() {
private val playbackModel: PlaybackViewModel by activityViewModels() private val playbackModel: PlaybackViewModel by activityViewModels()
@ -47,7 +46,7 @@ class SongsFragment : Fragment() {
playbackModel.playSong(it, PlaybackMode.ALL_SONGS) playbackModel.playSong(it, PlaybackMode.ALL_SONGS)
}, },
{ data, view -> { data, view ->
showActionMenuForSong(data, view) showActionMenuForSong(requireContext(), data, view, playbackModel)
} }
) )
applyDivider() applyDivider()
@ -58,21 +57,4 @@ class SongsFragment : Fragment() {
return binding.root 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.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.util.TypedValue
import android.view.MenuItem
import android.widget.ImageButton
import androidx.annotation.AttrRes import androidx.annotation.AttrRes
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.annotation.ColorRes import androidx.annotation.ColorRes
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.graphics.ColorUtils import androidx.core.graphics.ColorUtils
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
// Functions for managing colors/accents/whatever.
// Pairs of the base accent and its theme // Pairs of the base accent and its theme
private val ACCENTS = listOf( private val ACCENTS = listOf(
Pair(R.color.red, R.style.Theme_Red), // 0 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) 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:id="@+id/action_queue_add"
android:title="@string/label_queue_add" android:title="@string/label_queue_add"
android:icon="@drawable/ic_user_queue" /> 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> </menu>

View file

@ -25,10 +25,12 @@
<string name="label_sort_alpha_up">Z-A</string> <string name="label_sort_alpha_up">Z-A</string>
<string name="label_shuffle">Shuffle</string> <string name="label_shuffle">Shuffle</string>
<string name="label_play">Play</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">Queue</string>
<string name="label_queue_add">Add to 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_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_empty_queue">Nothing here.</string>
<string name="label_notification_playback">Music Playback</string> <string name="label_notification_playback">Music Playback</string>
<string name="label_service_playback">The music playback service for Auxio.</string> <string name="label_service_playback">The music playback service for Auxio.</string>