list: add selection menu

Add a menu dialog for selections. This more or less completes the
bottom sheet menu functionality.

Resolves #454.
This commit is contained in:
Alexander Capehart 2023-08-03 11:52:09 -06:00
parent 6d342325ea
commit 151b69bedb
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
35 changed files with 177 additions and 58 deletions

View file

@ -298,7 +298,11 @@ class MainFragment :
initialNavDestinationChange = true
return
}
listModel.dropSelection()
if (destination.id != R.id.selection_menu_dialog) {
// Drop any pending playlist edits when navigating away. This could actually happen
// if the user is quick enough.
listModel.dropSelection()
}
}
private fun handleShowOuter(outer: Outer?) {

View file

@ -101,7 +101,7 @@ class AlbumDetailFragment :
setNavigationOnClickListener { findNavController().navigateUp() }
overrideOnOverflowMenuClick {
listModel.openMenu(
R.menu.item_detail_album, unlikelyToBeNull(detailModel.currentAlbum.value))
R.menu.detail_album, unlikelyToBeNull(detailModel.currentAlbum.value))
}
}
@ -145,7 +145,7 @@ class AlbumDetailFragment :
}
override fun onOpenMenu(item: Song) {
listModel.openMenu(R.menu.item_album_song, item, detailModel.playInAlbumWith)
listModel.openMenu(R.menu.album_song, item, detailModel.playInAlbumWith)
}
override fun onPlay() {
@ -243,6 +243,7 @@ class AlbumDetailFragment :
when (menu) {
is Menu.ForSong -> AlbumDetailFragmentDirections.openSongMenu(menu.parcel)
is Menu.ForAlbum -> AlbumDetailFragmentDirections.openAlbumMenu(menu.parcel)
is Menu.ForSelection -> AlbumDetailFragmentDirections.openSelectionMenu(menu.parcel)
is Menu.ForArtist,
is Menu.ForGenre,
is Menu.ForPlaylist -> error("Unexpected menu $menu")

View file

@ -100,7 +100,7 @@ class ArtistDetailFragment :
setOnMenuItemClickListener(this@ArtistDetailFragment)
overrideOnOverflowMenuClick {
listModel.openMenu(
R.menu.item_detail_parent, unlikelyToBeNull(detailModel.currentArtist.value))
R.menu.detail_parent, unlikelyToBeNull(detailModel.currentArtist.value))
}
}
@ -152,9 +152,8 @@ class ArtistDetailFragment :
override fun onOpenMenu(item: Music) {
when (item) {
is Song ->
listModel.openMenu(R.menu.item_artist_song, item, detailModel.playInArtistWith)
is Album -> listModel.openMenu(R.menu.item_artist_album, item)
is Song -> listModel.openMenu(R.menu.artist_song, item, detailModel.playInArtistWith)
is Album -> listModel.openMenu(R.menu.artist_album, item)
else -> error("Unexpected datatype: ${item::class.simpleName}")
}
}
@ -239,6 +238,8 @@ class ArtistDetailFragment :
is Menu.ForSong -> ArtistDetailFragmentDirections.openSongMenu(menu.parcel)
is Menu.ForAlbum -> ArtistDetailFragmentDirections.openAlbumMenu(menu.parcel)
is Menu.ForArtist -> ArtistDetailFragmentDirections.openArtistMenu(menu.parcel)
is Menu.ForSelection ->
ArtistDetailFragmentDirections.openSelectionMenu(menu.parcel)
is Menu.ForGenre,
is Menu.ForPlaylist -> error("Unexpected menu $menu")
}

View file

@ -98,7 +98,7 @@ class GenreDetailFragment :
setOnMenuItemClickListener(this@GenreDetailFragment)
overrideOnOverflowMenuClick {
listModel.openMenu(
R.menu.item_detail_parent, unlikelyToBeNull(detailModel.currentGenre.value))
R.menu.detail_parent, unlikelyToBeNull(detailModel.currentGenre.value))
}
}
@ -150,8 +150,8 @@ class GenreDetailFragment :
override fun onOpenMenu(item: Music) {
when (item) {
is Artist -> listModel.openMenu(R.menu.item_parent, item)
is Song -> listModel.openMenu(R.menu.item_song, item, detailModel.playInGenreWith)
is Artist -> listModel.openMenu(R.menu.parent, item)
is Song -> listModel.openMenu(R.menu.song, item, detailModel.playInGenreWith)
else -> error("Unexpected datatype: ${item::class.simpleName}")
}
}
@ -240,6 +240,7 @@ class GenreDetailFragment :
is Menu.ForSong -> GenreDetailFragmentDirections.openSongMenu(menu.parcel)
is Menu.ForArtist -> GenreDetailFragmentDirections.openArtistMenu(menu.parcel)
is Menu.ForGenre -> GenreDetailFragmentDirections.openGenreMenu(menu.parcel)
is Menu.ForSelection -> GenreDetailFragmentDirections.openSelectionMenu(menu.parcel)
is Menu.ForAlbum,
is Menu.ForPlaylist -> error("Unexpected menu $menu")
}

View file

@ -104,8 +104,7 @@ class PlaylistDetailFragment :
setOnMenuItemClickListener(this@PlaylistDetailFragment)
overrideOnOverflowMenuClick {
listModel.openMenu(
R.menu.item_detail_playlist,
unlikelyToBeNull(detailModel.currentPlaylist.value))
R.menu.detail_playlist, unlikelyToBeNull(detailModel.currentPlaylist.value))
}
}
@ -200,7 +199,7 @@ class PlaylistDetailFragment :
}
override fun onOpenMenu(item: Song) {
listModel.openMenu(R.menu.item_playlist_song, item, detailModel.playInPlaylistWith)
listModel.openMenu(R.menu.playlist_song, item, detailModel.playInPlaylistWith)
}
override fun onPlay() {
@ -302,6 +301,8 @@ class PlaylistDetailFragment :
is Menu.ForSong -> PlaylistDetailFragmentDirections.openSongMenu(menu.parcel)
is Menu.ForPlaylist ->
PlaylistDetailFragmentDirections.openPlaylistMenu(menu.parcel)
is Menu.ForSelection ->
PlaylistDetailFragmentDirections.openSelectionMenu(menu.parcel)
is Menu.ForArtist,
is Menu.ForAlbum,
is Menu.ForGenre -> error("Unexpected menu $menu")

View file

@ -501,6 +501,7 @@ class HomeFragment :
is Menu.ForArtist -> HomeFragmentDirections.openArtistMenu(menu.parcel)
is Menu.ForGenre -> HomeFragmentDirections.openGenreMenu(menu.parcel)
is Menu.ForPlaylist -> HomeFragmentDirections.openPlaylistMenu(menu.parcel)
is Menu.ForSelection -> HomeFragmentDirections.openSelectionMenu(menu.parcel)
}
findNavController().navigateSafe(directions)
}

View file

@ -140,7 +140,7 @@ class AlbumListFragment :
}
override fun onOpenMenu(item: Album) {
listModel.openMenu(R.menu.item_album, item)
listModel.openMenu(R.menu.album, item)
}
private fun updateAlbums(albums: List<Album>) {

View file

@ -116,7 +116,7 @@ class ArtistListFragment :
}
override fun onOpenMenu(item: Artist) {
listModel.openMenu(R.menu.item_parent, item)
listModel.openMenu(R.menu.parent, item)
}
private fun updateArtists(artists: List<Artist>) {

View file

@ -115,7 +115,7 @@ class GenreListFragment :
}
override fun onOpenMenu(item: Genre) {
listModel.openMenu(R.menu.item_parent, item)
listModel.openMenu(R.menu.parent, item)
}
private fun updateGenres(genres: List<Genre>) {

View file

@ -113,7 +113,7 @@ class PlaylistListFragment :
}
override fun onOpenMenu(item: Playlist) {
listModel.openMenu(R.menu.item_playlist, item)
listModel.openMenu(R.menu.playlist, item)
}
private fun updatePlaylists(playlists: List<Playlist>) {

View file

@ -139,7 +139,7 @@ class SongListFragment :
}
override fun onOpenMenu(item: Song) {
listModel.openMenu(R.menu.item_song, item, homeModel.playWith)
listModel.openMenu(R.menu.song, item, homeModel.playWith)
}
private fun updateSongs(songs: List<Song>) {

View file

@ -201,6 +201,18 @@ constructor(private val listSettings: ListSettings, private val musicRepository:
openImpl(Menu.ForPlaylist(menuRes, playlist))
}
/**
* Open a menu for a [Song] selection. This is not a popup menu, instead actually a dialog of
* menu options with additional information.
*
* @param menuRes The resource of the menu to use.
* @param songs The [Song] selection to show.
*/
fun openMenu(@MenuRes menuRes: Int, songs: List<Song>) {
logD("Opening menu for ${songs.size} songs")
openImpl(Menu.ForSelection(menuRes, songs))
}
private fun openImpl(menu: Menu) {
val existing = _menu.flow.value
if (existing != null) {

View file

@ -26,7 +26,7 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.ViewBindingFragment
import org.oxycblt.auxio.util.share
import org.oxycblt.auxio.util.overrideOnOverflowMenuClick
import org.oxycblt.auxio.util.showToast
/**
@ -48,6 +48,9 @@ abstract class SelectionFragment<VB : ViewBinding> :
// Add cancel and menu item listeners to manage what occurs with the selection.
setNavigationOnClickListener { listModel.dropSelection() }
setOnMenuItemClickListener(this@SelectionFragment)
overrideOnOverflowMenuClick {
listModel.openMenu(R.menu.selection, listModel.takeSelection())
}
}
}
@ -67,23 +70,6 @@ abstract class SelectionFragment<VB : ViewBinding> :
musicModel.addToPlaylist(listModel.takeSelection())
true
}
R.id.action_selection_queue_add -> {
playbackModel.addToQueue(listModel.takeSelection())
requireContext().showToast(R.string.lng_queue_added)
true
}
R.id.action_selection_play -> {
playbackModel.play(listModel.takeSelection())
true
}
R.id.action_selection_shuffle -> {
playbackModel.shuffle(listModel.takeSelection())
true
}
R.id.action_selection_share -> {
requireContext().share(listModel.takeSelection())
true
}
else -> false
}

View file

@ -99,4 +99,11 @@ sealed interface Menu {
@Parcelize data class Parcel(val res: Int, val playlistUid: Music.UID) : Menu.Parcel
}
class ForSelection(@MenuRes override val res: Int, val songs: List<Song>) : Menu {
override val parcel: Parcel
get() = Parcel(res, songs.map { it.uid })
@Parcelize data class Parcel(val res: Int, val songUids: List<Music.UID>) : Menu.Parcel
}
}

View file

@ -34,6 +34,7 @@ import org.oxycblt.auxio.music.Playlist
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.resolveNames
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.playback.formatDurationMs
import org.oxycblt.auxio.util.getPlural
import org.oxycblt.auxio.util.share
import org.oxycblt.auxio.util.showToast
@ -321,3 +322,50 @@ class PlaylistMenuDialogFragment : MenuDialogFragment<Menu.ForPlaylist>() {
}
}
}
/**
* [MenuDialogFragment] implementation for a [Song] selection.
*
* @author Alexander Capehart (OxygenCobalt)
*/
@AndroidEntryPoint
class SelectionMenuDialogFragment : MenuDialogFragment<Menu.ForSelection>() {
override val menuModel: MenuViewModel by activityViewModels()
override val listModel: ListViewModel by activityViewModels()
private val musicModel: MusicViewModel by activityViewModels()
private val playbackModel: PlaybackViewModel by activityViewModels()
private val args: SelectionMenuDialogFragmentArgs by navArgs()
override val parcel
get() = args.parcel
// Nothing to disable in song menus.
override fun getDisabledItemIds(menu: Menu.ForSelection) = setOf<Int>()
override fun updateMenu(binding: DialogMenuBinding, menu: Menu.ForSelection) {
binding.menuCover.bind(
menu.songs, getString(R.string.desc_selection_image), R.drawable.ic_song_24)
binding.menuType.text = getString(R.string.lbl_selection)
binding.menuName.text =
requireContext().getPlural(R.plurals.fmt_song_count, menu.songs.size)
binding.menuInfo.text = menu.songs.sumOf { it.durationMs }.formatDurationMs(true)
}
override fun onClick(item: MenuItem, menu: Menu.ForSelection) {
when (item.itemId) {
R.id.action_play -> playbackModel.play(menu.songs)
R.id.action_shuffle -> playbackModel.shuffle(menu.songs)
R.id.action_play_next -> {
playbackModel.playNext(menu.songs)
requireContext().showToast(R.string.lng_queue_added)
}
R.id.action_queue_add -> {
playbackModel.addToQueue(menu.songs)
requireContext().showToast(R.string.lng_queue_added)
}
R.id.action_share -> requireContext().share(menu.songs)
R.id.action_playlist_add -> musicModel.addToPlaylist(menu.songs)
else -> error("Unexpected menu item selected $item")
}
}
}

View file

@ -66,6 +66,7 @@ class MenuViewModel @Inject constructor(private val musicRepository: MusicReposi
is Menu.ForArtist.Parcel -> unpackArtistParcel(parcel)
is Menu.ForGenre.Parcel -> unpackGenreParcel(parcel)
is Menu.ForPlaylist.Parcel -> unpackPlaylistParcel(parcel)
is Menu.ForSelection.Parcel -> unpackSelectionParcel(parcel)
}
private fun unpackSongParcel(parcel: Menu.ForSong.Parcel): Menu.ForSong? {
@ -94,4 +95,10 @@ class MenuViewModel @Inject constructor(private val musicRepository: MusicReposi
val playlist = musicRepository.userLibrary?.findPlaylist(parcel.playlistUid) ?: return null
return Menu.ForPlaylist(parcel.res, playlist)
}
private fun unpackSelectionParcel(parcel: Menu.ForSelection.Parcel): Menu.ForSelection? {
val deviceLibrary = musicRepository.deviceLibrary ?: return null
val songs = parcel.songUids.mapNotNull(deviceLibrary::findSong)
return Menu.ForSelection(parcel.res, songs)
}
}

View file

@ -96,7 +96,7 @@ class PlaybackPanelFragment :
playbackModel.song.value?.let {
// No playback options are actually available in the menu, so use a junk
// PlaySong option.
listModel.openMenu(R.menu.item_playback_song, it, PlaySong.ByItself)
listModel.openMenu(R.menu.playback_song, it, PlaySong.ByItself)
}
}
}

View file

@ -184,11 +184,11 @@ class SearchFragment : ListFragment<Music, FragmentSearchBinding>() {
override fun onOpenMenu(item: Music) {
when (item) {
is Song -> listModel.openMenu(R.menu.item_song, item, searchModel.playWith)
is Album -> listModel.openMenu(R.menu.item_album, item)
is Artist -> listModel.openMenu(R.menu.item_parent, item)
is Genre -> listModel.openMenu(R.menu.item_parent, item)
is Playlist -> listModel.openMenu(R.menu.item_playlist, item)
is Song -> listModel.openMenu(R.menu.song, item, searchModel.playWith)
is Album -> listModel.openMenu(R.menu.album, item)
is Artist -> listModel.openMenu(R.menu.parent, item)
is Genre -> listModel.openMenu(R.menu.parent, item)
is Playlist -> listModel.openMenu(R.menu.playlist, item)
}
}
@ -261,6 +261,7 @@ class SearchFragment : ListFragment<Music, FragmentSearchBinding>() {
is Menu.ForArtist -> SearchFragmentDirections.openArtistMenu(menu.parcel)
is Menu.ForGenre -> SearchFragmentDirections.openGenreMenu(menu.parcel)
is Menu.ForPlaylist -> SearchFragmentDirections.openPlaylistMenu(menu.parcel)
is Menu.ForSelection -> SearchFragmentDirections.openSelectionMenu(menu.parcel)
}
findNavController().navigateSafe(directions)
// Keyboard is no longer needed.

View file

@ -28,6 +28,7 @@ import androidx.annotation.RequiresApi
import androidx.appcompat.view.menu.ActionMenuItemView
import androidx.appcompat.widget.ActionMenuView
import androidx.appcompat.widget.AppCompatButton
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.app.ShareCompat
import androidx.core.graphics.Insets
@ -111,7 +112,7 @@ val ViewBinding.context: Context
* Override the behavior of a [MaterialToolbar]'s overflow menu to do something else. This is
* extremely dumb, but required to hook overflow menus to bottom sheet menus.
*/
fun MaterialToolbar.overrideOnOverflowMenuClick(block: (View) -> Unit) {
fun Toolbar.overrideOnOverflowMenuClick(block: (View) -> Unit) {
for (toolbarChild in children) {
if (toolbarChild is ActionMenuView) {
for (menuChild in toolbarChild.children) {

View file

@ -0,0 +1,30 @@
<?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/lbl_play"
android:icon="@drawable/ic_play_24"
app:showAsAction="never"/>
<item
android:id="@+id/action_shuffle"
android:title="@string/lbl_shuffle"
android:icon="@drawable/ic_shuffle_off_24"
app:showAsAction="never"/>
<item
android:id="@+id/action_play_next"
android:title="@string/lbl_play_next"
android:icon="@drawable/ic_play_next_24" />
<item
android:id="@+id/action_queue_add"
android:title="@string/lbl_queue_add"
android:icon="@drawable/ic_queue_add_24" />
<item
android:id="@+id/action_playlist_add"
android:title="@string/lbl_playlist_add"
android:icon="@drawable/ic_playlist_add_24" />
<item
android:id="@+id/action_share"
android:title="@string/lbl_share"
android:icon="@drawable/ic_share_24" />
</menu>

View file

@ -12,19 +12,7 @@
android:icon="@drawable/ic_playlist_add_24"
app:showAsAction="ifRoom"/>
<item
android:id="@+id/action_selection_queue_add"
android:title="@string/lbl_queue_add"
android:id="@+id/placeholder"
android:title=""
app:showAsAction="never" />
<item
android:id="@+id/action_selection_play"
android:title="@string/lbl_play_selected"
app:showAsAction="never"/>
<item
android:id="@+id/action_selection_shuffle"
android:title="@string/lbl_shuffle_selected"
app:showAsAction="never"/>
<item
android:id="@+id/action_selection_share"
android:title="@string/lbl_share"
app:showAsAction="never"/>
</menu>

View file

@ -57,6 +57,9 @@
<action
android:id="@+id/open_playlist_menu"
app:destination="@id/playlist_menu_dialog" />
<action
android:id="@+id/open_selection_menu"
app:destination="@id/selection_menu_dialog" />
<action
android:id="@+id/new_playlist"
app:destination="@id/new_playlist_dialog" />
@ -152,6 +155,9 @@
<action
android:id="@+id/open_genre_menu"
app:destination="@id/genre_menu_dialog" />
<action
android:id="@+id/open_selection_menu"
app:destination="@id/selection_menu_dialog" />
<action
android:id="@+id/open_playlist_menu"
app:destination="@id/playlist_menu_dialog" />
@ -204,6 +210,9 @@
<action
android:id="@+id/open_album_menu"
app:destination="@id/album_menu_dialog" />
<action
android:id="@+id/open_selection_menu"
app:destination="@id/selection_menu_dialog" />
<action
android:id="@+id/add_to_playlist"
app:destination="@id/add_to_playlist_dialog" />
@ -250,6 +259,9 @@
<action
android:id="@+id/open_artist_menu"
app:destination="@id/artist_menu_dialog" />
<action
android:id="@+id/open_selection_menu"
app:destination="@id/selection_menu_dialog" />
<action
android:id="@+id/add_to_playlist"
app:destination="@id/add_to_playlist_dialog" />
@ -296,6 +308,9 @@
<action
android:id="@+id/open_genre_menu"
app:destination="@id/genre_menu_dialog" />
<action
android:id="@+id/open_selection_menu"
app:destination="@id/selection_menu_dialog" />
<action
android:id="@+id/add_to_playlist"
app:destination="@id/add_to_playlist_dialog" />
@ -339,6 +354,9 @@
<action
android:id="@+id/open_playlist_menu"
app:destination="@id/playlist_menu_dialog" />
<action
android:id="@+id/open_selection_menu"
app:destination="@id/selection_menu_dialog" />
<action
android:id="@+id/rename_playlist"
app:destination="@id/rename_playlist_dialog" />
@ -481,4 +499,14 @@
android:name="parcel"
app:argType="org.oxycblt.auxio.list.menu.Menu$ForPlaylist$Parcel" />
</dialog>
<dialog
android:id="@+id/selection_menu_dialog"
android:name="org.oxycblt.auxio.list.menu.SelectionMenuDialogFragment"
android:label="selection_menu_dialog"
tools:layout="@layout/dialog_menu">
<argument
android:name="parcel"
app:argType="org.oxycblt.auxio.list.menu.Menu$ForSelection$Parcel" />
</dialog>
</navigation>

View file

@ -122,7 +122,6 @@
<string name="lbl_playlist_add">Add to playlist</string>
<string name="lbl_artist_details">Go to artist</string>
<string name="lbl_album_details">Go to album</string>
<string name="lbl_song_detail">View properties</string>
@ -167,6 +166,8 @@
<string name="lbl_licenses">Licenses</string>
<string name="lbl_library_counts">Library statistics</string>
<string name="lbl_selection">Selection</string>
<!-- Long Namespace | Longer Descriptions -->
<eat-comment />
@ -335,6 +336,7 @@
<string name="desc_artist_image">Artist image for %s</string>
<string name="desc_genre_image">Genre image for %s</string>
<string name="desc_playlist_image">Playlist image for %s</string>
<string name="desc_selection_image">Selection image</string>
<!-- Default Namespace | Placeholder values -->
<eat-comment />