Extend Popup actions

Extend the action menus to every music model.
This commit is contained in:
OxygenCobalt 2020-11-21 16:10:16 -07:00
parent d2350de12a
commit 13b80585d2
24 changed files with 291 additions and 115 deletions

View file

@ -14,4 +14,7 @@ interface PlaybackStateDAO {
@Query("DELETE FROM playback_state_table") @Query("DELETE FROM playback_state_table")
fun clear() fun clear()
@Query("SELECT * FROM playback_state_table ORDER BY id DESC LIMIT 1")
fun getRecent(): PlaybackState?
} }

View file

@ -18,8 +18,11 @@ interface QueueDAO {
} }
} }
@Query("SELECT * FROM queue_table") @Query("SELECT * FROM queue_table WHERE is_user_queue == 1")
fun getAll(): List<QueueItem> fun getUserQueue(): List<QueueItem>
@Query("SELECT * FROM queue_table WHERE is_user_queue == 0")
fun getQueue(): List<QueueItem>
@Query("DELETE FROM queue_table") @Query("DELETE FROM queue_table")
fun clear() fun clear()

View file

@ -22,7 +22,6 @@ import org.oxycblt.auxio.ui.setupAlbumSongActions
class AlbumDetailFragment : DetailFragment() { class AlbumDetailFragment : DetailFragment() {
private val args: AlbumDetailFragmentArgs by navArgs() private val args: AlbumDetailFragmentArgs by navArgs()
private val detailModel: DetailViewModel by activityViewModels()
private val playbackModel: PlaybackViewModel by activityViewModels() private val playbackModel: PlaybackViewModel by activityViewModels()
override fun onCreateView( override fun onCreateView(
@ -45,10 +44,8 @@ class AlbumDetailFragment : DetailFragment() {
} }
val songAdapter = DetailSongAdapter( val songAdapter = DetailSongAdapter(
{ doOnClick = { playbackModel.playSong(it, PlaybackMode.IN_ALBUM) },
playbackModel.playSong(it, PlaybackMode.IN_ALBUM) doOnLongClick = { data, view ->
},
{ data, view ->
PopupMenu(requireContext(), view).setupAlbumSongActions( PopupMenu(requireContext(), view).setupAlbumSongActions(
data, requireContext(), detailModel, playbackModel data, requireContext(), detailModel, playbackModel
) )
@ -76,6 +73,10 @@ class AlbumDetailFragment : DetailFragment() {
R.id.action_play -> playbackModel.playAlbum( R.id.action_play -> playbackModel.playAlbum(
detailModel.currentAlbum.value!!, false detailModel.currentAlbum.value!!, false
) )
R.id.action_queue_add -> playbackModel.addToUserQueue(
detailModel.currentAlbum.value!!.songs
)
} }
true true

View file

@ -5,6 +5,7 @@ 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 androidx.appcompat.widget.PopupMenu
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
@ -15,10 +16,10 @@ import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.applyDivider import org.oxycblt.auxio.ui.applyDivider
import org.oxycblt.auxio.ui.disable import org.oxycblt.auxio.ui.disable
import org.oxycblt.auxio.ui.setupAlbumActions
class ArtistDetailFragment : DetailFragment() { class ArtistDetailFragment : DetailFragment() {
private val args: ArtistDetailFragmentArgs by navArgs() private val args: ArtistDetailFragmentArgs by navArgs()
private val detailModel: DetailViewModel by activityViewModels()
private val playbackModel: PlaybackViewModel by activityViewModels() private val playbackModel: PlaybackViewModel by activityViewModels()
override fun onCreateView( override fun onCreateView(
@ -40,7 +41,8 @@ class ArtistDetailFragment : DetailFragment() {
) )
} }
val albumAdapter = DetailAlbumAdapter { val albumAdapter = DetailAlbumAdapter(
doOnClick = {
if (!detailModel.isNavigating) { if (!detailModel.isNavigating) {
detailModel.updateNavigationStatus(true) detailModel.updateNavigationStatus(true)
@ -48,7 +50,13 @@ class ArtistDetailFragment : DetailFragment() {
ArtistDetailFragmentDirections.actionShowAlbum(it.id, false) ArtistDetailFragmentDirections.actionShowAlbum(it.id, false)
) )
} }
},
doOnLongClick = { data, view ->
PopupMenu(requireContext(), view).setupAlbumActions(
data, requireContext(), playbackModel
)
} }
)
// --- UI SETUP --- // --- UI SETUP ---
@ -106,10 +114,4 @@ class ArtistDetailFragment : DetailFragment() {
return binding.root return binding.root
} }
override fun onResume() {
super.onResume()
detailModel.updateNavigationStatus(false)
}
} }

View file

@ -4,14 +4,18 @@ import android.os.Bundle
import android.view.View import android.view.View
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
/** /**
* A Base [Fragment] implementing a [OnBackPressedCallback] so that Auxio will navigate upwards * A Base [Fragment] implementing a [OnBackPressedCallback] so that Auxio will navigate upwards
* instead of out of the app if a Detail Fragment is currently open. * instead of out of the app if a Detail Fragment is currently open. Also carries the
* multi-navigation fix.
* @author OxygenCobalt * @author OxygenCobalt
*/ */
abstract class DetailFragment : Fragment() { abstract class DetailFragment : Fragment() {
protected val detailModel: DetailViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, callback) requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, callback)
} }
@ -20,6 +24,7 @@ abstract class DetailFragment : Fragment() {
super.onResume() super.onResume()
callback.isEnabled = true callback.isEnabled = true
detailModel.updateNavigationStatus(false)
} }
override fun onPause() { override fun onPause() {

View file

@ -5,6 +5,7 @@ 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 androidx.appcompat.widget.PopupMenu
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
@ -15,11 +16,11 @@ import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.applyDivider import org.oxycblt.auxio.ui.applyDivider
import org.oxycblt.auxio.ui.disable import org.oxycblt.auxio.ui.disable
import org.oxycblt.auxio.ui.setupArtistActions
class GenreDetailFragment : DetailFragment() { class GenreDetailFragment : DetailFragment() {
private val args: GenreDetailFragmentArgs by navArgs() private val args: GenreDetailFragmentArgs by navArgs()
private val detailModel: DetailViewModel by activityViewModels()
private val playbackModel: PlaybackViewModel by activityViewModels() private val playbackModel: PlaybackViewModel by activityViewModels()
override fun onCreateView( override fun onCreateView(
@ -41,7 +42,8 @@ class GenreDetailFragment : DetailFragment() {
) )
} }
val artistAdapter = DetailArtistAdapter { val artistAdapter = DetailArtistAdapter(
doOnClick = {
if (!detailModel.isNavigating) { if (!detailModel.isNavigating) {
detailModel.updateNavigationStatus(true) detailModel.updateNavigationStatus(true)
@ -49,7 +51,13 @@ class GenreDetailFragment : DetailFragment() {
GenreDetailFragmentDirections.actionShowArtist(it.id) GenreDetailFragmentDirections.actionShowArtist(it.id)
) )
} }
},
doOnLongClick = { data, view ->
PopupMenu(requireContext(), view).setupArtistActions(
data, requireContext(), playbackModel
)
} }
)
// --- UI SETUP --- // --- UI SETUP ---
@ -106,10 +114,4 @@ class GenreDetailFragment : DetailFragment() {
return binding.root return binding.root
} }
override fun onResume() {
super.onResume()
detailModel.updateNavigationStatus(false)
}
} }

View file

@ -1,6 +1,7 @@
package org.oxycblt.auxio.detail.adapters package org.oxycblt.auxio.detail.adapters
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import org.oxycblt.auxio.databinding.ItemArtistAlbumBinding import org.oxycblt.auxio.databinding.ItemArtistAlbumBinding
@ -9,7 +10,8 @@ import org.oxycblt.auxio.recycler.DiffCallback
import org.oxycblt.auxio.recycler.viewholders.BaseViewHolder import org.oxycblt.auxio.recycler.viewholders.BaseViewHolder
class DetailAlbumAdapter( class DetailAlbumAdapter(
private val doOnClick: (data: Album) -> Unit private val doOnClick: (data: Album) -> Unit,
private val doOnLongClick: (data: Album, view: View) -> Unit
) : ListAdapter<Album, DetailAlbumAdapter.ViewHolder>(DiffCallback()) { ) : ListAdapter<Album, DetailAlbumAdapter.ViewHolder>(DiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
@ -25,7 +27,7 @@ class DetailAlbumAdapter(
// Generic ViewHolder for a detail album // Generic ViewHolder for a detail album
inner class ViewHolder( inner class ViewHolder(
private val binding: ItemArtistAlbumBinding, private val binding: ItemArtistAlbumBinding,
) : BaseViewHolder<Album>(binding, doOnClick, null) { ) : BaseViewHolder<Album>(binding, doOnClick, doOnLongClick) {
override fun onBind(data: Album) { override fun onBind(data: Album) {
binding.album = data binding.album = data

View file

@ -1,6 +1,7 @@
package org.oxycblt.auxio.detail.adapters package org.oxycblt.auxio.detail.adapters
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import org.oxycblt.auxio.databinding.ItemGenreArtistBinding import org.oxycblt.auxio.databinding.ItemGenreArtistBinding
@ -9,7 +10,8 @@ import org.oxycblt.auxio.recycler.DiffCallback
import org.oxycblt.auxio.recycler.viewholders.BaseViewHolder import org.oxycblt.auxio.recycler.viewholders.BaseViewHolder
class DetailArtistAdapter( class DetailArtistAdapter(
private val doOnClick: (data: Artist) -> Unit private val doOnClick: (data: Artist) -> Unit,
private val doOnLongClick: (data: Artist, view: View) -> Unit
) : ListAdapter<Artist, DetailArtistAdapter.ViewHolder>(DiffCallback()) { ) : ListAdapter<Artist, DetailArtistAdapter.ViewHolder>(DiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
@ -25,7 +27,7 @@ class DetailArtistAdapter(
// Generic ViewHolder for an album // Generic ViewHolder for an album
inner class ViewHolder( inner class ViewHolder(
private val binding: ItemGenreArtistBinding private val binding: ItemGenreArtistBinding
) : BaseViewHolder<Artist>(binding, doOnClick, null) { ) : BaseViewHolder<Artist>(binding, doOnClick, doOnLongClick) {
override fun onBind(data: Artist) { override fun onBind(data: Artist) {
binding.artist = data binding.artist = data

View file

@ -30,6 +30,9 @@ import org.oxycblt.auxio.playback.state.PlaybackMode
import org.oxycblt.auxio.ui.applyColor import org.oxycblt.auxio.ui.applyColor
import org.oxycblt.auxio.ui.applyDivider import org.oxycblt.auxio.ui.applyDivider
import org.oxycblt.auxio.ui.resolveAttr import org.oxycblt.auxio.ui.resolveAttr
import org.oxycblt.auxio.ui.setupAlbumActions
import org.oxycblt.auxio.ui.setupArtistActions
import org.oxycblt.auxio.ui.setupGenreActions
import org.oxycblt.auxio.ui.setupSongActions import org.oxycblt.auxio.ui.setupSongActions
// A Fragment to show all the music in the Library. // A Fragment to show all the music in the Library.
@ -47,21 +50,15 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
val musicStore = MusicStore.getInstance() val musicStore = MusicStore.getInstance()
val libraryAdapter = LibraryAdapter(libraryModel.showMode.value!!) { val libraryAdapter = LibraryAdapter(
navToItem(it) libraryModel.showMode.value!!,
} doOnClick = { navToItem(it) },
doOnLongClick = { data, view -> showActionsForItem(data, view) }
)
val searchAdapter = SearchAdapter( val searchAdapter = SearchAdapter(
{ doOnClick = { navToItem(it) },
navToItem(it) doOnLongClick = { data, view -> showActionsForItem(data, view) }
},
{ data, view ->
if (data is Song) {
PopupMenu(requireContext(), view).setupSongActions(
data, requireContext(), playbackModel
)
}
}
) )
// --- UI SETUP --- // --- UI SETUP ---
@ -175,6 +172,19 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
return false return false
} }
private fun showActionsForItem(data: BaseModel, view: View) {
val menu = PopupMenu(requireContext(), view)
when (data) {
is Song -> menu.setupSongActions(data, requireContext(), playbackModel)
is Album -> menu.setupAlbumActions(data, requireContext(), playbackModel)
is Artist -> menu.setupArtistActions(data, requireContext(), playbackModel)
is Genre -> menu.setupGenreActions(data, requireContext(), playbackModel)
else -> {
}
}
}
private fun navToItem(baseModel: BaseModel) { private fun navToItem(baseModel: BaseModel) {
// If the item is a song [That was selected through search], then update the playback // If the item is a song [That was selected through search], then update the playback
// to that song instead of doing any navigation // to that song instead of doing any navigation

View file

@ -22,7 +22,7 @@ class LibraryViewModel : ViewModel() {
val searchHasFocus: Boolean get() = mSearchHasFocus val searchHasFocus: Boolean get() = mSearchHasFocus
// TODO: Move these to prefs when they're added // TODO: Move these to prefs when they're added
private val mShowMode = MutableLiveData(ShowMode.SHOW_ARTISTS) private val mShowMode = MutableLiveData(ShowMode.SHOW_GENRES)
val showMode: LiveData<ShowMode> get() = mShowMode val showMode: LiveData<ShowMode> get() = mShowMode
private val mSortMode = MutableLiveData(SortMode.ALPHA_DOWN) private val mSortMode = MutableLiveData(SortMode.ALPHA_DOWN)

View file

@ -1,5 +1,6 @@
package org.oxycblt.auxio.library.adapters package org.oxycblt.auxio.library.adapters
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
@ -11,12 +12,11 @@ import org.oxycblt.auxio.recycler.viewholders.AlbumViewHolder
import org.oxycblt.auxio.recycler.viewholders.ArtistViewHolder import org.oxycblt.auxio.recycler.viewholders.ArtistViewHolder
import org.oxycblt.auxio.recycler.viewholders.GenreViewHolder import org.oxycblt.auxio.recycler.viewholders.GenreViewHolder
// A ListAdapter that can contain three different types of ViewHolders depending // The primary RecyclerView adapter for the library. Displays genres, artists, and albums.
// the ShowMode given.
// It cannot display multiple ViewHolders *at once* however. That's what SearchAdapter is for.
class LibraryAdapter( class LibraryAdapter(
private val showMode: ShowMode, private val showMode: ShowMode,
private val doOnClick: (data: BaseModel) -> Unit private val doOnClick: (data: BaseModel) -> Unit,
private val doOnLongClick: (data: BaseModel, view: View) -> Unit
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { ) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private var data: List<BaseModel> private var data: List<BaseModel>
@ -37,10 +37,11 @@ class LibraryAdapter(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
// Return a different View Holder depending on the show type // Return a different View Holder depending on the show type
return when (showMode) { return when (showMode) {
ShowMode.SHOW_GENRES -> GenreViewHolder.from(parent.context, doOnClick) ShowMode.SHOW_GENRES -> GenreViewHolder.from(parent.context, doOnClick, doOnLongClick)
ShowMode.SHOW_ARTISTS -> ArtistViewHolder.from(parent.context, doOnClick) ShowMode.SHOW_ARTISTS -> ArtistViewHolder.from(parent.context, doOnClick, doOnLongClick)
ShowMode.SHOW_ALBUMS -> AlbumViewHolder.from(parent.context, doOnClick) ShowMode.SHOW_ALBUMS -> AlbumViewHolder.from(parent.context, doOnClick, doOnLongClick)
else -> ArtistViewHolder.from(parent.context, doOnClick)
else -> error("Bad ShowMode given.")
} }
} }

View file

@ -34,14 +34,22 @@ class SearchAdapter(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) { return when (viewType) {
GenreViewHolder.ITEM_TYPE -> GenreViewHolder.from(parent.context, doOnClick) GenreViewHolder.ITEM_TYPE -> GenreViewHolder.from(
ArtistViewHolder.ITEM_TYPE -> ArtistViewHolder.from(parent.context, doOnClick) parent.context, doOnClick, doOnLongClick
AlbumViewHolder.ITEM_TYPE -> AlbumViewHolder.from(parent.context, doOnClick)
SongViewHolder.ITEM_TYPE -> SongViewHolder.from(
parent.context,
doOnClick,
doOnLongClick
) )
ArtistViewHolder.ITEM_TYPE -> ArtistViewHolder.from(
parent.context, doOnClick, doOnLongClick
)
AlbumViewHolder.ITEM_TYPE -> AlbumViewHolder.from(
parent.context, doOnClick, doOnLongClick
)
SongViewHolder.ITEM_TYPE -> SongViewHolder.from(
parent.context, doOnClick, doOnLongClick
)
HeaderViewHolder.ITEM_TYPE -> HeaderViewHolder.from(parent.context) HeaderViewHolder.ITEM_TYPE -> HeaderViewHolder.from(parent.context)
else -> error("Someone messed with the ViewHolder item types.") else -> error("Someone messed with the ViewHolder item types.")

View file

@ -86,7 +86,6 @@ class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
// Make marquee scroll work // Make marquee scroll work
// TODO: Add nav here as well // TODO: Add nav here as well
binding.playbackSong.isSelected = true binding.playbackSong.isSelected = true
binding.playbackSeekBar.setOnSeekBarChangeListener(this) binding.playbackSeekBar.setOnSeekBarChangeListener(this)
// --- VIEWMODEL SETUP -- // --- VIEWMODEL SETUP --
@ -127,6 +126,8 @@ class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
} else { } else {
binding.playbackShuffle.imageTintList = controlColor binding.playbackShuffle.imageTintList = controlColor
} }
Log.d(this::class.simpleName, "Shuffle swap")
} }
playbackModel.loopMode.observe(viewLifecycleOwner) { playbackModel.loopMode.observe(viewLifecycleOwner) {

View file

@ -226,6 +226,10 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
playbackManager.addToUserQueue(song) playbackManager.addToUserQueue(song)
} }
fun addToUserQueue(songs: List<Song>) {
playbackManager.addToUserQueue(songs)
}
// --- STATUS FUNCTIONS --- // --- STATUS FUNCTIONS ---
// Flip the playing status. // Flip the playing status.

View file

@ -141,7 +141,8 @@ class PlaybackStateManager private constructor() {
mQueue = song.album.songs mQueue = song.album.songs
} }
else -> {} else -> {
}
} }
mMode = mode mMode = mode
@ -254,7 +255,6 @@ class PlaybackStateManager private constructor() {
} }
updatePlayback(mQueue[mIndex]) updatePlayback(mQueue[mIndex])
forceQueueUpdate() forceQueueUpdate()
} }
} }
@ -304,6 +304,12 @@ class PlaybackStateManager private constructor() {
forceUserQueueUpdate() forceUserQueueUpdate()
} }
fun addToUserQueue(songs: List<Song>) {
mUserQueue.addAll(songs)
forceUserQueueUpdate()
}
fun removeUserQueueItem(index: Int) { fun removeUserQueueItem(index: Int) {
Log.d(this::class.simpleName, "Removing item ${mUserQueue[index].name}.") Log.d(this::class.simpleName, "Removing item ${mUserQueue[index].name}.")
@ -426,8 +432,6 @@ class PlaybackStateManager private constructor() {
val start = System.currentTimeMillis() val start = System.currentTimeMillis()
Log.d(this::class.simpleName, packQueue().size.toString())
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val playbackState = packToPlaybackState() val playbackState = packToPlaybackState()
val queueItems = packQueue() val queueItems = packQueue()
@ -450,13 +454,16 @@ class PlaybackStateManager private constructor() {
val start = System.currentTimeMillis() val start = System.currentTimeMillis()
val states: List<PlaybackState> val state: PlaybackState?
val queueItems: List<QueueItem> val queueItems: List<QueueItem>
val userQueueItems: List<QueueItem>
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val database = AuxioDatabase.getInstance(context) val database = AuxioDatabase.getInstance(context)
states = database.playbackStateDAO.getAll()
queueItems = database.queueDAO.getAll() state = database.playbackStateDAO.getRecent()
queueItems = database.queueDAO.getQueue()
userQueueItems = database.queueDAO.getUserQueue()
database.playbackStateDAO.clear() database.playbackStateDAO.clear()
database.queueDAO.clear() database.queueDAO.clear()
@ -466,7 +473,7 @@ class PlaybackStateManager private constructor() {
Log.d(this::class.simpleName, "Load finished in ${loadTime}ms") Log.d(this::class.simpleName, "Load finished in ${loadTime}ms")
if (states.isEmpty()) { if (state == null) {
Log.d(this::class.simpleName, "Nothing here. Not restoring.") Log.d(this::class.simpleName, "Nothing here. Not restoring.")
mIsRestored = true mIsRestored = true
@ -474,13 +481,13 @@ class PlaybackStateManager private constructor() {
return return
} }
Log.d(this::class.simpleName, "Old state found, ${states[0]}") Log.d(this::class.simpleName, "Old state found, $state")
unpackFromPlaybackState(states[0]) unpackFromPlaybackState(state)
Log.d(this::class.simpleName, "Found queue of size ${queueItems.size}") Log.d(this::class.simpleName, "Found queue of size ${queueItems.size}")
unpackQueue(queueItems) unpackQueues(queueItems, userQueueItems)
mSong?.let { mSong?.let {
mIndex = mQueue.indexOf(mSong) mIndex = mQueue.indexOf(mSong)
@ -543,18 +550,22 @@ class PlaybackStateManager private constructor() {
} }
} }
private fun unpackQueue(queueItems: List<QueueItem>) { private fun unpackQueues(queueItems: List<QueueItem>, userQueueItems: List<QueueItem>) {
val musicStore = MusicStore.getInstance() val musicStore = MusicStore.getInstance()
queueItems.forEach { item -> queueItems.forEach { item ->
// Traverse albums and then album songs instead of just the songs, as its faster. // Traverse albums and then album songs instead of just the songs, as its faster.
musicStore.albums.find { it.id == item.albumId }?.songs?.find { it.id == item.songId }?.let { musicStore.albums.find { it.id == item.albumId }
if (item.isUserQueue) { ?.songs?.find { it.id == item.songId }?.let {
mUserQueue.add(it)
} else {
mQueue.add(it) mQueue.add(it)
} }
} }
userQueueItems.forEach { item ->
musicStore.albums.find { it.id == item.albumId }
?.songs?.find { it.id == item.songId }?.let {
mUserQueue.add(it)
}
} }
forceQueueUpdate() forceQueueUpdate()

View file

@ -19,8 +19,9 @@ import org.oxycblt.auxio.music.Song
class GenreViewHolder private constructor( class GenreViewHolder private constructor(
private val binding: ItemGenreBinding, private val binding: ItemGenreBinding,
doOnClick: (Genre) -> Unit doOnClick: (Genre) -> Unit,
) : BaseViewHolder<Genre>(binding, doOnClick, null) { doOnLongClick: (data: Genre, view: View) -> Unit
) : BaseViewHolder<Genre>(binding, doOnClick, doOnLongClick) {
override fun onBind(data: Genre) { override fun onBind(data: Genre) {
binding.genre = data binding.genre = data
@ -30,10 +31,14 @@ class GenreViewHolder private constructor(
companion object { companion object {
const val ITEM_TYPE = 0xA010 const val ITEM_TYPE = 0xA010
fun from(context: Context, doOnClick: (Genre) -> Unit): GenreViewHolder { fun from(
context: Context,
doOnClick: (Genre) -> Unit,
doOnLongClick: (data: Genre, view: View) -> Unit
): GenreViewHolder {
return GenreViewHolder( return GenreViewHolder(
ItemGenreBinding.inflate(LayoutInflater.from(context)), ItemGenreBinding.inflate(LayoutInflater.from(context)),
doOnClick doOnClick, doOnLongClick
) )
} }
} }
@ -42,7 +47,8 @@ class GenreViewHolder private constructor(
class ArtistViewHolder private constructor( class ArtistViewHolder private constructor(
private val binding: ItemArtistBinding, private val binding: ItemArtistBinding,
doOnClick: (Artist) -> Unit, doOnClick: (Artist) -> Unit,
) : BaseViewHolder<Artist>(binding, doOnClick, null) { doOnLongClick: (data: Artist, view: View) -> Unit
) : BaseViewHolder<Artist>(binding, doOnClick, doOnLongClick) {
override fun onBind(data: Artist) { override fun onBind(data: Artist) {
binding.artist = data binding.artist = data
@ -52,10 +58,14 @@ class ArtistViewHolder private constructor(
companion object { companion object {
const val ITEM_TYPE = 0xA011 const val ITEM_TYPE = 0xA011
fun from(context: Context, doOnClick: (Artist) -> Unit): ArtistViewHolder { fun from(
context: Context,
doOnClick: (Artist) -> Unit,
doOnLongClick: (data: Artist, view: View) -> Unit
): ArtistViewHolder {
return ArtistViewHolder( return ArtistViewHolder(
ItemArtistBinding.inflate(LayoutInflater.from(context)), ItemArtistBinding.inflate(LayoutInflater.from(context)),
doOnClick doOnClick, doOnLongClick
) )
} }
} }
@ -63,8 +73,9 @@ class ArtistViewHolder private constructor(
class AlbumViewHolder private constructor( class AlbumViewHolder private constructor(
private val binding: ItemAlbumBinding, private val binding: ItemAlbumBinding,
doOnClick: (data: Album) -> Unit doOnClick: (data: Album) -> Unit,
) : BaseViewHolder<Album>(binding, doOnClick, null) { doOnLongClick: (data: Album, view: View) -> Unit
) : BaseViewHolder<Album>(binding, doOnClick, doOnLongClick) {
override fun onBind(data: Album) { override fun onBind(data: Album) {
binding.album = data binding.album = data
@ -74,10 +85,14 @@ class AlbumViewHolder private constructor(
companion object { companion object {
const val ITEM_TYPE = 0xA012 const val ITEM_TYPE = 0xA012
fun from(context: Context, doOnClick: (data: Album) -> Unit): AlbumViewHolder { fun from(
context: Context,
doOnClick: (data: Album) -> Unit,
doOnLongClick: (data: Album, view: View) -> Unit
): AlbumViewHolder {
return AlbumViewHolder( return AlbumViewHolder(
ItemAlbumBinding.inflate(LayoutInflater.from(context)), ItemAlbumBinding.inflate(LayoutInflater.from(context)),
doOnClick, doOnClick, doOnLongClick
) )
} }
} }

View file

@ -43,10 +43,8 @@ class SongsFragment : Fragment() {
binding.songRecycler.apply { binding.songRecycler.apply {
adapter = SongAdapter( adapter = SongAdapter(
musicStore.songs, musicStore.songs,
{ doOnClick = { playbackModel.playSong(it, PlaybackMode.ALL_SONGS) },
playbackModel.playSong(it, PlaybackMode.ALL_SONGS) doOnLongClick = { data, view ->
},
{ data, view ->
PopupMenu(requireContext(), view).setupSongActions( PopupMenu(requireContext(), view).setupSongActions(
data, requireContext(), playbackModel data, requireContext(), playbackModel
) )

View file

@ -14,6 +14,9 @@ import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.detail.DetailViewModel import org.oxycblt.auxio.detail.DetailViewModel
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre
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
@ -64,8 +67,8 @@ fun PopupMenu.setupSongActions(song: Song, context: Context, playbackModel: Play
setOnMenuItemClickListener { setOnMenuItemClickListener {
when (it.itemId) { when (it.itemId) {
R.id.action_queue_add -> { R.id.action_queue_add -> {
doUserQueueAdd(context, song, playbackModel) playbackModel.addToUserQueue(song)
context.getString(R.string.label_queue_added).createToast(context)
true true
} }
@ -95,7 +98,8 @@ fun PopupMenu.setupAlbumSongActions(
setOnMenuItemClickListener { setOnMenuItemClickListener {
when (it.itemId) { when (it.itemId) {
R.id.action_queue_add -> { R.id.action_queue_add -> {
doUserQueueAdd(context, song, playbackModel) playbackModel.addToUserQueue(song)
context.getString(R.string.label_queue_added).createToast(context)
true true
} }
@ -116,12 +120,95 @@ fun PopupMenu.setupAlbumSongActions(
show() show()
} }
private fun doUserQueueAdd(context: Context, song: Song, playbackModel: PlaybackViewModel) { fun PopupMenu.setupAlbumActions(
// If the song was already added to the user queue, then don't add it again. album: Album,
// This is just to prevent a bug with DiffCallback that creates strange context: Context,
// behavior when duplicate user queue items are added. playbackModel: PlaybackViewModel
// FIXME: Fix the duplicate item DiffCallback issue ) {
playbackModel.addToUserQueue(song) inflate(R.menu.menu_album_actions)
setOnMenuItemClickListener {
when (it.itemId) {
R.id.action_queue_add -> {
playbackModel.addToUserQueue(album.songs)
context.getString(R.string.label_queue_added).createToast(context) context.getString(R.string.label_queue_added).createToast(context)
true
}
R.id.action_play -> {
playbackModel.playAlbum(album, false)
true
}
R.id.action_shuffle -> {
playbackModel.playAlbum(album, true)
true
}
else -> false
}
}
show()
}
fun PopupMenu.setupArtistActions(
artist: Artist,
context: Context,
playbackModel: PlaybackViewModel
) {
inflate(R.menu.menu_detail)
setOnMenuItemClickListener {
when (it.itemId) {
R.id.action_queue_add -> {
playbackModel.addToUserQueue(artist.songs)
context.getString(R.string.label_queue_added).createToast(context)
true
}
R.id.action_play -> {
playbackModel.playArtist(artist, false)
true
}
R.id.action_shuffle -> {
playbackModel.playArtist(artist, true)
true
}
else -> false
}
}
show()
}
fun PopupMenu.setupGenreActions(
genre: Genre,
context: Context,
playbackModel: PlaybackViewModel
) {
inflate(R.menu.menu_detail)
setOnMenuItemClickListener {
when (it.itemId) {
R.id.action_queue_add -> {
playbackModel.addToUserQueue(genre.songs)
context.getString(R.string.label_queue_added).createToast(context)
true
}
R.id.action_play -> {
playbackModel.playGenre(genre, false)
true
}
R.id.action_shuffle -> {
playbackModel.playGenre(genre, true)
true
}
else -> false
}
}
show()
} }

View file

@ -30,8 +30,8 @@
android:layout_height="?android:attr/actionBarSize" android:layout_height="?android:attr/actionBarSize"
android:background="?android:attr/windowBackground" android:background="?android:attr/windowBackground"
android:elevation="@dimen/elevation_normal" android:elevation="@dimen/elevation_normal"
app:menu="@menu/menu_detail"
app:popupTheme="@style/AppThemeOverlay.Popup" app:popupTheme="@style/AppThemeOverlay.Popup"
app:menu="@menu/menu_album_actions"
app:titleTextAppearance="@style/TextAppearance.Toolbar.Header" app:titleTextAppearance="@style/TextAppearance.Toolbar.Header"
app:navigationIcon="@drawable/ic_back" app:navigationIcon="@drawable/ic_back"
app:title="@string/title_library_fragment" /> app:title="@string/title_library_fragment" />

View file

@ -30,6 +30,7 @@
android:layout_height="?android:attr/actionBarSize" android:layout_height="?android:attr/actionBarSize"
android:background="?android:attr/windowBackground" android:background="?android:attr/windowBackground"
android:elevation="@dimen/elevation_normal" android:elevation="@dimen/elevation_normal"
app:popupTheme="@style/AppThemeOverlay.Popup"
app:menu="@menu/menu_detail" app:menu="@menu/menu_detail"
app:titleTextAppearance="@style/TextAppearance.Toolbar.Header" app:titleTextAppearance="@style/TextAppearance.Toolbar.Header"
app:navigationIcon="@drawable/ic_back" app:navigationIcon="@drawable/ic_back"

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_play"
android:title="@string/label_play"
android:icon="@drawable/ic_play"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_shuffle"
android:icon="@drawable/ic_shuffle"
android:title="@string/label_shuffle"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_queue_add"
android:icon="@drawable/ic_queue_add"
app:showAsAction="never"
android:title="@string/label_queue_add" />
</menu>

View file

@ -5,5 +5,5 @@
android:id="@+id/action_queue" android:id="@+id/action_queue"
android:icon="@drawable/ic_queue" android:icon="@drawable/ic_queue"
android:title="@string/label_queue" android:title="@string/label_queue"
app:showAsAction="always" /> app:showAsAction="ifRoom" />
</menu> </menu>

View file

@ -28,6 +28,7 @@
<string name="label_play_artist">Play from artist</string> <string name="label_play_artist">Play from artist</string>
<string name="label_play_album">Play from album</string> <string name="label_play_album">Play from album</string>
<string name="label_go_artist">Go to artist</string> <string name="label_go_artist">Go to artist</string>
<string name="label_go_album">Go to 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_queue_added">Added to queue</string>