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:
parent
8adf5e978d
commit
5d1eaf72dd
18 changed files with 139 additions and 57 deletions
|
@ -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]
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
9
app/src/main/res/menu/menu_genre_artist_detail.xml
Normal file
9
app/src/main/res/menu/menu_genre_artist_detail.xml
Normal 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>
|
|
@ -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>
|
Loading…
Reference in a new issue