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:
parent
2be7d34601
commit
4fb4120342
14 changed files with 160 additions and 136 deletions
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
||||
|
|
|
|||
|
|
@ -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 ---
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,13 +53,11 @@ 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
|
||||
)
|
||||
}
|
||||
binding.queueHeader.text = getString(
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -216,19 +216,26 @@ class PlaybackStateManager private constructor() {
|
|||
fun next() {
|
||||
resetLoopMode()
|
||||
|
||||
if (mIndex < mQueue.lastIndex) {
|
||||
mIndex = mIndex.inc()
|
||||
if (mUserQueue.isNotEmpty()) {
|
||||
updatePlayback(mUserQueue[0])
|
||||
mUserQueue.removeAt(0)
|
||||
|
||||
forceUserQueueUpdate()
|
||||
} else {
|
||||
// TODO: Implement option to make the playlist loop instead of stop
|
||||
mQueue = mutableListOf()
|
||||
mSong = null
|
||||
if (mIndex < mQueue.lastIndex) {
|
||||
mIndex = mIndex.inc()
|
||||
} 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() {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
98
app/src/main/java/org/oxycblt/auxio/ui/InterfaceUtils.kt
Normal file
98
app/src/main/java/org/oxycblt/auxio/ui/InterfaceUtils.kt
Normal 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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue