detail: add queue actions to artist/genre

Add play next/add to queue actions to artists and genres, as the queue
system now makes their UX reasonable.
This commit is contained in:
OxygenCobalt 2022-06-14 17:39:18 -06:00
parent 8adf5e978d
commit 5d1eaf72dd
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
18 changed files with 139 additions and 57 deletions

View file

@ -18,6 +18,7 @@
- "Rounded album covers" option is no longer dependent on "Show album covers" option
- Added song actions to the playback panel
- Playback controls are now easier to reach when gesture navigation is enabled
- Added Play Next/Add to Queue options to artists and genres
#### What's Fixed
- Playback bar now picks the larger inset in case that gesture inset is missing [#149]

View file

@ -88,14 +88,17 @@ class MainFragment : ViewBindingFragment<FragmentMainBinding>() {
val binding = requireBinding()
when (action) {
MainNavigationAction.EXPAND -> binding.bottomSheetLayout.expand()
MainNavigationAction.COLLAPSE -> binding.bottomSheetLayout.collapse()
MainNavigationAction.SETTINGS ->
is MainNavigationAction.Expand -> binding.bottomSheetLayout.expand()
is MainNavigationAction.Collapse -> binding.bottomSheetLayout.collapse()
is MainNavigationAction.Settings ->
findNavController().navigate(MainFragmentDirections.actionShowSettings())
MainNavigationAction.ABOUT ->
is MainNavigationAction.About ->
findNavController().navigate(MainFragmentDirections.actionShowAbout())
MainNavigationAction.QUEUE ->
is MainNavigationAction.Queue ->
findNavController().navigate(MainFragmentDirections.actionShowQueue())
is MainNavigationAction.SongDetails ->
findNavController()
.navigate(MainFragmentDirections.actionShowDetails(action.song.id))
}
navModel.finishMainNavigation()

View file

@ -87,6 +87,10 @@ class AlbumDetailFragment : DetailFragment(), AlbumDetailAdapter.Listener {
requireContext().showToast(R.string.lbl_queue_added)
true
}
R.id.action_go_artist -> {
navModel.exploreNavigateTo(unlikelyToBeNull(detailModel.currentAlbum.value))
true
}
else -> false
}
}

View file

@ -41,6 +41,7 @@ import org.oxycblt.auxio.util.collectWith
import org.oxycblt.auxio.util.launch
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logW
import org.oxycblt.auxio.util.showToast
import org.oxycblt.auxio.util.unlikelyToBeNull
/**
@ -54,7 +55,8 @@ class ArtistDetailFragment : DetailFragment(), DetailAdapter.Listener {
override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) {
detailModel.setArtistId(args.artistId)
setupToolbar(unlikelyToBeNull(detailModel.currentArtist.value))
setupToolbar(
unlikelyToBeNull(detailModel.currentArtist.value), R.menu.menu_genre_artist_detail)
requireBinding().detailRecycler.apply {
adapter = detailAdapter
applySpans { pos ->
@ -72,7 +74,21 @@ class ArtistDetailFragment : DetailFragment(), DetailAdapter.Listener {
launch { playbackModel.song.collectWith(playbackModel.parent, ::updatePlayback) }
}
override fun onMenuItemClick(item: MenuItem): Boolean = false
override fun onMenuItemClick(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_play_next -> {
playbackModel.playNext(unlikelyToBeNull(detailModel.currentArtist.value))
requireContext().showToast(R.string.lbl_queue_added)
true
}
R.id.action_queue_add -> {
playbackModel.addToQueue(unlikelyToBeNull(detailModel.currentArtist.value))
requireContext().showToast(R.string.lbl_queue_added)
true
}
else -> false
}
}
override fun onItemClick(item: Item) {
when (item) {

View file

@ -61,14 +61,10 @@ abstract class DetailFragment :
* @param data Parent data to use as the toolbar title
* @param menuId Menu resource to use
*/
protected fun setupToolbar(data: MusicParent, @MenuRes menuId: Int = -1) {
protected fun setupToolbar(data: MusicParent, @MenuRes menuId: Int) {
requireBinding().detailToolbar.apply {
title = data.resolveName(context)
if (menuId != -1) {
inflateMenu(menuId)
}
inflateMenu(menuId)
setNavigationOnClickListener { findNavController().navigateUp() }
setOnMenuItemClickListener(this@DetailFragment)
}

View file

@ -41,6 +41,7 @@ import org.oxycblt.auxio.util.applySpans
import org.oxycblt.auxio.util.collectWith
import org.oxycblt.auxio.util.launch
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.showToast
import org.oxycblt.auxio.util.unlikelyToBeNull
/**
@ -54,7 +55,8 @@ class GenreDetailFragment : DetailFragment(), DetailAdapter.Listener {
override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) {
detailModel.setGenreId(args.genreId)
setupToolbar(unlikelyToBeNull(detailModel.currentGenre.value))
setupToolbar(
unlikelyToBeNull(detailModel.currentArtist.value), R.menu.menu_genre_artist_detail)
binding.detailRecycler.apply {
adapter = detailAdapter
applySpans { pos ->
@ -71,7 +73,21 @@ class GenreDetailFragment : DetailFragment(), DetailAdapter.Listener {
launch { playbackModel.song.collectWith(playbackModel.parent, ::updatePlayback) }
}
override fun onMenuItemClick(item: MenuItem): Boolean = false
override fun onMenuItemClick(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_play_next -> {
playbackModel.playNext(unlikelyToBeNull(detailModel.currentGenre.value))
requireContext().showToast(R.string.lbl_queue_added)
true
}
R.id.action_queue_add -> {
playbackModel.addToQueue(unlikelyToBeNull(detailModel.currentGenre.value))
requireContext().showToast(R.string.lbl_queue_added)
true
}
else -> false
}
}
override fun onItemClick(item: Item) {
when (item) {

View file

@ -22,10 +22,9 @@ import android.text.format.Formatter
import android.view.LayoutInflater
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isGone
import org.oxycblt.auxio.BuildConfig
import androidx.navigation.fragment.navArgs
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogSongDetailBinding
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
import org.oxycblt.auxio.util.androidActivityViewModels
import org.oxycblt.auxio.util.formatDuration
@ -33,6 +32,7 @@ import org.oxycblt.auxio.util.launch
class SongDetailDialog : ViewBindingDialogFragment<DialogSongDetailBinding>() {
private val detailModel: DetailViewModel by androidActivityViewModels()
private val args: SongDetailDialogArgs by navArgs()
override fun onCreateBinding(inflater: LayoutInflater) =
DialogSongDetailBinding.inflate(inflater)
@ -44,7 +44,7 @@ class SongDetailDialog : ViewBindingDialogFragment<DialogSongDetailBinding>() {
override fun onBindingCreated(binding: DialogSongDetailBinding, savedInstanceState: Bundle?) {
super.onBindingCreated(binding, savedInstanceState)
detailModel.setSongId(requireNotNull(arguments).getLong(ARG_ID))
detailModel.setSongId(args.songId)
launch { detailModel.currentSong.collect(::updateSong) }
}
@ -80,15 +80,4 @@ class SongDetailDialog : ViewBindingDialogFragment<DialogSongDetailBinding>() {
binding.detailContainer.isGone = true
}
}
companion object {
fun from(song: Song): SongDetailDialog {
val instance = SongDetailDialog()
instance.arguments = Bundle().apply { putLong(ARG_ID, song.id) }
return instance
}
const val TAG = BuildConfig.APPLICATION_ID + ".tag.SONG_DETAILS"
private const val ARG_ID = BuildConfig.APPLICATION_ID + ".arg.SONG_ID"
}
}

View file

@ -161,11 +161,11 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
}
R.id.action_settings -> {
logD("Navigating to settings")
navModel.mainNavigateTo(MainNavigationAction.SETTINGS)
navModel.mainNavigateTo(MainNavigationAction.Settings)
}
R.id.action_about -> {
logD("Navigating to about")
navModel.mainNavigateTo(MainNavigationAction.ABOUT)
navModel.mainNavigateTo(MainNavigationAction.About)
}
R.id.submenu_sorting -> {
// Junk click event when opening the menu

View file

@ -51,7 +51,7 @@ class PlaybackBarFragment : ViewBindingFragment<FragmentPlaybackBarBinding>() {
savedInstanceState: Bundle?
) {
binding.root.apply {
setOnClickListener { navModel.mainNavigateTo(MainNavigationAction.EXPAND) }
setOnClickListener { navModel.mainNavigateTo(MainNavigationAction.Expand) }
setOnLongClickListener {
playbackModel.song.value?.let(navModel::exploreNavigateTo)

View file

@ -27,7 +27,6 @@ import androidx.fragment.app.activityViewModels
import kotlin.math.max
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentPlaybackPanelBinding
import org.oxycblt.auxio.detail.SongDetailDialog
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.state.RepeatMode
@ -80,7 +79,7 @@ class PlaybackPanelFragment :
}
binding.playbackToolbar.apply {
setNavigationOnClickListener { navModel.mainNavigateTo(MainNavigationAction.COLLAPSE) }
setNavigationOnClickListener { navModel.mainNavigateTo(MainNavigationAction.Collapse) }
setOnMenuItemClickListener(this@PlaybackPanelFragment)
queueItem = menu.findItem(R.id.action_queue)
}
@ -136,7 +135,7 @@ class PlaybackPanelFragment :
override fun onMenuItemClick(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_queue -> {
navModel.mainNavigateTo(MainNavigationAction.QUEUE)
navModel.mainNavigateTo(MainNavigationAction.Queue)
true
}
R.id.action_go_artist -> {
@ -149,7 +148,7 @@ class PlaybackPanelFragment :
}
R.id.action_song_detail -> {
playbackModel.song.value?.let {
SongDetailDialog.from(it).show(childFragmentManager, SongDetailDialog.TAG)
navModel.mainNavigateTo(MainNavigationAction.SongDetails(it))
}
true
}

View file

@ -223,6 +223,16 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback, MusicStore
playbackManager.playNext(settingsManager.detailAlbumSort.songs(album.songs))
}
/** Add an [Artist] to the top of the queue. */
fun playNext(artist: Artist) {
playbackManager.playNext(settingsManager.detailArtistSort.songs(artist.songs))
}
/** Add a [Genre] to the top of the queue. */
fun playNext(genre: Genre) {
playbackManager.playNext(settingsManager.detailGenreSort.songs(genre.songs))
}
/** Add a [Song] to the end of the queue. */
fun addToQueue(song: Song) {
playbackManager.addToQueue(song)
@ -233,6 +243,16 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback, MusicStore
playbackManager.addToQueue(settingsManager.detailAlbumSort.songs(album.songs))
}
/** Add an [Artist] to the end of the queue. */
fun addToQueue(artist: Artist) {
playbackManager.addToQueue(settingsManager.detailArtistSort.songs(artist.songs))
}
/** Add a [Genre] to the end of the queue. */
fun addToQueue(genre: Genre) {
playbackManager.addToQueue(settingsManager.detailGenreSort.songs(genre.songs))
}
// --- STATUS FUNCTIONS ---
/** Flip the playing status, e.g from playing to paused */

View file

@ -26,7 +26,6 @@ import androidx.core.view.children
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import org.oxycblt.auxio.R
import org.oxycblt.auxio.detail.SongDetailDialog
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre
@ -55,7 +54,7 @@ fun Fragment.newMenu(anchor: View, data: Item, flag: Int = ActionMenu.FLAG_NONE)
* @throws IllegalStateException When there is no menu for this specific datatype/flag
* @author OxygenCobalt
*
* TODO: Prevent duplicate menus from showing up
* TODO: Prevent duplicate menus from showing up (merge into ViewBindingFragment)
*
* TODO: Add multi-select
*/
@ -118,8 +117,8 @@ class ActionMenu(
else -> -1
}
}
is Artist -> R.menu.menu_artist_actions
is Genre -> R.menu.menu_genre_actions
is Artist,
is Genre -> R.menu.menu_genre_artist_actions
else -> -1
}
}
@ -153,6 +152,14 @@ class ActionMenu(
playbackModel.playNext(data)
context.showToast(R.string.lbl_queue_added)
}
is Artist -> {
playbackModel.playNext(data)
context.showToast(R.string.lbl_queue_added)
}
is Genre -> {
playbackModel.playNext(data)
context.showToast(R.string.lbl_queue_added)
}
else -> {}
}
}
@ -166,6 +173,14 @@ class ActionMenu(
playbackModel.addToQueue(data)
context.showToast(R.string.lbl_queue_added)
}
is Artist -> {
playbackModel.addToQueue(data)
context.showToast(R.string.lbl_queue_added)
}
is Genre -> {
playbackModel.addToQueue(data)
context.showToast(R.string.lbl_queue_added)
}
else -> {}
}
}
@ -183,8 +198,7 @@ class ActionMenu(
}
R.id.action_song_detail -> {
if (data is Song) {
SongDetailDialog.from(data)
.show(activity.supportFragmentManager, SongDetailDialog.TAG)
navModel.mainNavigateTo(MainNavigationAction.SongDetails(data))
}
}
}

View file

@ -21,6 +21,7 @@ import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.Song
/**
* A ViewModel that handles complicated navigation situations.
@ -68,15 +69,17 @@ class NavigationViewModel : ViewModel() {
* normal fragments. This can be passed to [NavigationViewModel.mainNavigateTo] in order to
* facilitate navigation without stupid fragment hacks.
*/
enum class MainNavigationAction {
sealed class MainNavigationAction {
/** Expand the playback panel. */
EXPAND,
object Expand : MainNavigationAction()
/** Collapse the playback panel. */
COLLAPSE,
object Collapse : MainNavigationAction()
/** Go to settings. */
SETTINGS,
object Settings : MainNavigationAction()
/** Go to the about page. */
ABOUT,
object About : MainNavigationAction()
/** Go to the queue. */
QUEUE
object Queue : MainNavigationAction()
/** Show song details. */
data class SongDetails(val song: Song) : MainNavigationAction()
}

View file

@ -6,4 +6,7 @@
<item
android:id="@+id/action_queue_add"
android:title="@string/lbl_queue_add" />
<item
android:id="@+id/action_go_artist"
android:title="@string/lbl_go_artist" />
</menu>

View file

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/action_play"
android:title="@string/lbl_play" />
<item
android:id="@+id/action_shuffle"
android:title="@string/lbl_shuffle" />
</menu>

View file

@ -6,4 +6,10 @@
<item
android:id="@+id/action_shuffle"
android:title="@string/lbl_shuffle" />
<item
android:id="@+id/action_play_next"
android:title="@string/lbl_play_next" />
<item
android:id="@+id/action_queue_add"
android:title="@string/lbl_queue_add" />
</menu>

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/action_play_next"
android:title="@string/lbl_play_next" />
<item
android:id="@+id/action_queue_add"
android:title="@string/lbl_queue_add" />
</menu>

View file

@ -29,6 +29,9 @@
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_show_details"
app:destination="@id/song_detail_dialog" />
</fragment>
<fragment
@ -46,4 +49,13 @@
android:name="org.oxycblt.auxio.settings.SettingsFragment"
android:label="fragment_settings"
tools:layout="@layout/fragment_settings" />
<dialog
android:id="@+id/song_detail_dialog"
android:name="org.oxycblt.auxio.detail.SongDetailDialog"
android:label="song_detail_dialog"
tools:layout="@layout/dialog_song_detail">
<argument
android:name="songId"
app:argType="long" />
</dialog>
</navigation>