From 2f85d694d175d3e66a869d784aac4e58b590c5ab Mon Sep 17 00:00:00 2001 From: OxygenCobalt Date: Wed, 15 Jun 2022 10:02:52 -0600 Subject: [PATCH] ui: remove actionmenu for menufragment Introduce MenuFragment in order to replace ActionMenu. ActionMenu was a terrible class filled with hacks. Introduce a new fragment called MenuFragment that enables the same features, plus: 1. Requiring consumers the specify the menu, which prevents issues from one-size-fits-all menus (unless absolutely necessary) 2. Fixing an issue where multiple menus appear at once --- CHANGELOG.md | 2 + .../java/org/oxycblt/auxio/MainActivity.kt | 2 + .../auxio/detail/AlbumDetailFragment.kt | 56 +++-- .../auxio/detail/ArtistDetailFragment.kt | 56 +++-- .../oxycblt/auxio/detail/DetailFragment.kt | 115 --------- .../oxycblt/auxio/detail/DetailViewModel.kt | 4 +- .../auxio/detail/GenreDetailFragment.kt | 52 +++- .../auxio/home/list/AlbumListFragment.kt | 7 +- .../auxio/home/list/ArtistListFragment.kt | 7 +- .../auxio/home/list/GenreListFragment.kt | 7 +- .../auxio/home/list/HomeListFragment.kt | 8 +- .../auxio/home/list/SongListFragment.kt | 7 +- .../oxycblt/auxio/search/SearchFragment.kt | 21 +- .../java/org/oxycblt/auxio/ui/ActionMenu.kt | 226 ------------------ .../java/org/oxycblt/auxio/ui/MenuFragment.kt | 186 ++++++++++++++ .../main/java/org/oxycblt/auxio/ui/Sort.kt | 1 + app/src/main/res/menu/menu_album_detail.xml | 3 + .../main/res/menu/menu_album_song_actions.xml | 3 + app/src/main/res/menu/menu_album_sort.xml | 16 ++ .../res/menu/menu_artist_song_actions.xml | 3 + app/src/main/res/menu/menu_artist_sort.xml | 22 ++ ...nu_detail_sort.xml => menu_genre_sort.xml} | 6 - info/FAQ.md | 27 +++ 23 files changed, 423 insertions(+), 414 deletions(-) delete mode 100644 app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt delete mode 100644 app/src/main/java/org/oxycblt/auxio/ui/ActionMenu.kt create mode 100644 app/src/main/java/org/oxycblt/auxio/ui/MenuFragment.kt create mode 100644 app/src/main/res/menu/menu_album_sort.xml create mode 100644 app/src/main/res/menu/menu_artist_sort.xml rename app/src/main/res/menu/{menu_detail_sort.xml => menu_genre_sort.xml} (80%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96d9af42f..c5ed2bee1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,8 @@ - Playback bar now picks the larger inset in case that gesture inset is missing [#149] - Fixed unusable excluded directory UI - Songs with no data (i.e size of 0) are now filtered out +- Fixed non-sensical menu items from appearing on songs +- Fixed issue where multiple menus would open if long-clicks occured simultaniously #### Dev/Meta - New translations [Fjuro -> Czech, Konstantin Tutsch -> German] diff --git a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt index da665fb94..157f4a1e7 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt @@ -46,6 +46,8 @@ import org.oxycblt.auxio.util.logD * * TODO: Rework padding ethos * + * TODO: Add multi-select + * * @author OxygenCobalt */ class MainActivity : AppCompatActivity() { diff --git a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt index ee5eecc5c..15e8abc59 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt @@ -19,9 +19,12 @@ package org.oxycblt.auxio.detail import android.content.Context import android.os.Bundle +import android.view.LayoutInflater import android.view.MenuItem import android.view.View +import androidx.appcompat.widget.Toolbar import androidx.core.view.children +import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import androidx.recyclerview.widget.LinearSmoothScroller @@ -37,7 +40,7 @@ import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.state.PlaybackMode import org.oxycblt.auxio.ui.Header import org.oxycblt.auxio.ui.Item -import org.oxycblt.auxio.ui.newMenu +import org.oxycblt.auxio.ui.MenuFragment import org.oxycblt.auxio.util.applySpans import org.oxycblt.auxio.util.canScroll import org.oxycblt.auxio.util.collectWith @@ -48,17 +51,29 @@ import org.oxycblt.auxio.util.showToast import org.oxycblt.auxio.util.unlikelyToBeNull /** - * The [DetailFragment] for an album. + * A fragment that shows information for a particular [Album]. * @author OxygenCobalt */ -class AlbumDetailFragment : DetailFragment(), AlbumDetailAdapter.Listener { +class AlbumDetailFragment : + MenuFragment(), + Toolbar.OnMenuItemClickListener, + AlbumDetailAdapter.Listener { + private val detailModel: DetailViewModel by activityViewModels() + private val args: AlbumDetailFragmentArgs by navArgs() private val detailAdapter = AlbumDetailAdapter(this) + override fun onCreateBinding(inflater: LayoutInflater) = FragmentDetailBinding.inflate(inflater) + override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) { detailModel.setAlbumId(args.albumId) - setupToolbar(unlikelyToBeNull(detailModel.currentAlbum.value), R.menu.menu_album_detail) + binding.detailToolbar.apply { + inflateMenu(R.menu.menu_album_detail) + setNavigationOnClickListener { findNavController().navigateUp() } + setOnMenuItemClickListener(this@AlbumDetailFragment) + } + requireBinding().detailRecycler.apply { adapter = detailAdapter applySpans { pos -> @@ -75,6 +90,12 @@ class AlbumDetailFragment : DetailFragment(), AlbumDetailAdapter.Listener { launch { playbackModel.song.collectWith(playbackModel.parent, ::updatePlayback) } } + override fun onDestroyBinding(binding: FragmentDetailBinding) { + super.onDestroyBinding(binding) + binding.detailToolbar.setOnMenuItemClickListener(null) + binding.detailRecycler.adapter = null + } + override fun onMenuItemClick(item: MenuItem): Boolean { return when (item.itemId) { R.id.action_play_next -> { @@ -102,7 +123,10 @@ class AlbumDetailFragment : DetailFragment(), AlbumDetailAdapter.Listener { } override fun onOpenMenu(item: Item, anchor: View) { - newMenu(anchor, item) + when (item) { + is Song -> musicMenu(anchor, R.menu.menu_album_song_actions, item) + else -> logW("Unexpected datatype when opening menu: ${item::class.java}") + } } override fun onPlayParent() { @@ -114,15 +138,16 @@ class AlbumDetailFragment : DetailFragment(), AlbumDetailAdapter.Listener { } override fun onShowSortMenu(anchor: View) { - showSortMenu( - anchor, - detailModel.albumSort, - onConfirm = { detailModel.albumSort = it }, - showItem = { - it == R.id.option_sort_asc || - it == R.id.option_sort_disc || - it == R.id.option_sort_track - }) + menu(anchor, R.menu.menu_album_sort) { + val sort = detailModel.albumSort + requireNotNull(menu.findItem(sort.itemId)).isChecked = true + requireNotNull(menu.findItem(R.id.option_sort_asc)).isChecked = sort.isAscending + setOnMenuItemClickListener { item -> + item.isChecked = !item.isChecked + detailModel.albumSort = requireNotNull(sort.assignId(item.itemId)) + true + } + } } override fun onNavigateToArtist() { @@ -135,7 +160,10 @@ class AlbumDetailFragment : DetailFragment(), AlbumDetailAdapter.Listener { private fun handleItemChange(album: Album?) { if (album == null) { findNavController().navigateUp() + return } + + requireBinding().detailToolbar.title = album.resolveName(requireContext()) } private fun handleNavigation(item: Music?) { diff --git a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt index 4b1ac45e2..71e879492 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt @@ -18,8 +18,11 @@ package org.oxycblt.auxio.detail import android.os.Bundle +import android.view.LayoutInflater import android.view.MenuItem import android.view.View +import androidx.appcompat.widget.Toolbar +import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import org.oxycblt.auxio.R @@ -35,7 +38,7 @@ import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.state.PlaybackMode import org.oxycblt.auxio.ui.Header import org.oxycblt.auxio.ui.Item -import org.oxycblt.auxio.ui.newMenu +import org.oxycblt.auxio.ui.MenuFragment import org.oxycblt.auxio.util.applySpans import org.oxycblt.auxio.util.collectWith import org.oxycblt.auxio.util.launch @@ -45,18 +48,27 @@ import org.oxycblt.auxio.util.showToast import org.oxycblt.auxio.util.unlikelyToBeNull /** - * The [DetailFragment] for an artist. + * A fragment that shows information for a particular [Artist]. * @author OxygenCobalt */ -class ArtistDetailFragment : DetailFragment(), DetailAdapter.Listener { +class ArtistDetailFragment : + MenuFragment(), Toolbar.OnMenuItemClickListener, DetailAdapter.Listener { + private val detailModel: DetailViewModel by activityViewModels() + private val args: ArtistDetailFragmentArgs by navArgs() private val detailAdapter = ArtistDetailAdapter(this) + override fun onCreateBinding(inflater: LayoutInflater) = FragmentDetailBinding.inflate(inflater) + override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) { detailModel.setArtistId(args.artistId) - setupToolbar( - unlikelyToBeNull(detailModel.currentArtist.value), R.menu.menu_genre_artist_detail) + binding.detailToolbar.apply { + inflateMenu(R.menu.menu_genre_artist_detail) + setNavigationOnClickListener { findNavController().navigateUp() } + setOnMenuItemClickListener(this@ArtistDetailFragment) + } + requireBinding().detailRecycler.apply { adapter = detailAdapter applySpans { pos -> @@ -74,6 +86,12 @@ class ArtistDetailFragment : DetailFragment(), DetailAdapter.Listener { launch { playbackModel.song.collectWith(playbackModel.parent, ::updatePlayback) } } + override fun onDestroyBinding(binding: FragmentDetailBinding) { + super.onDestroyBinding(binding) + binding.detailToolbar.setOnMenuItemClickListener(null) + binding.detailRecycler.adapter = null + } + override fun onMenuItemClick(item: MenuItem): Boolean { return when (item.itemId) { R.id.action_play_next -> { @@ -98,7 +116,11 @@ class ArtistDetailFragment : DetailFragment(), DetailAdapter.Listener { } override fun onOpenMenu(item: Item, anchor: View) { - newMenu(anchor, item) + when (item) { + is Song -> musicMenu(anchor, R.menu.menu_artist_song_actions, item) + is Album -> musicMenu(anchor, R.menu.menu_artist_album_actions, item) + else -> logW("Unexpected datatype when opening menu: ${item::class.java}") + } } override fun onPlayParent() { @@ -110,21 +132,25 @@ class ArtistDetailFragment : DetailFragment(), DetailAdapter.Listener { } override fun onShowSortMenu(anchor: View) { - showSortMenu( - anchor, - detailModel.artistSort, - onConfirm = { detailModel.artistSort = it }, - showItem = { id -> - id != R.id.option_sort_artist && - id != R.id.option_sort_disc && - id != R.id.option_sort_track - }) + menu(anchor, R.menu.menu_artist_sort) { + val sort = detailModel.artistSort + requireNotNull(menu.findItem(sort.itemId)).isChecked = true + requireNotNull(menu.findItem(R.id.option_sort_asc)).isChecked = sort.isAscending + setOnMenuItemClickListener { item -> + item.isChecked = !item.isChecked + detailModel.artistSort = requireNotNull(sort.assignId(item.itemId)) + true + } + } } private fun handleItemChange(artist: Artist?) { if (artist == null) { findNavController().navigateUp() + return } + + requireBinding().detailToolbar.title = artist.resolveName(requireContext()) } private fun handleNavigation(item: Music?) { diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt deleted file mode 100644 index 6cd98cafe..000000000 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright (c) 2021 Auxio Project - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.oxycblt.auxio.detail - -import android.view.LayoutInflater -import android.view.View -import androidx.annotation.MenuRes -import androidx.appcompat.widget.PopupMenu -import androidx.appcompat.widget.Toolbar -import androidx.core.view.children -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.navigation.fragment.findNavController -import org.oxycblt.auxio.R -import org.oxycblt.auxio.databinding.FragmentDetailBinding -import org.oxycblt.auxio.music.MusicParent -import org.oxycblt.auxio.playback.PlaybackViewModel -import org.oxycblt.auxio.ui.NavigationViewModel -import org.oxycblt.auxio.ui.Sort -import org.oxycblt.auxio.ui.ViewBindingFragment -import org.oxycblt.auxio.util.androidActivityViewModels -import org.oxycblt.auxio.util.logD -import org.oxycblt.auxio.util.unlikelyToBeNull - -/** - * A Base [Fragment] implementing the base features shared across all detail fragments. - * @author OxygenCobalt - */ -abstract class DetailFragment : - ViewBindingFragment(), Toolbar.OnMenuItemClickListener { - protected val detailModel: DetailViewModel by androidActivityViewModels() - protected val navModel: NavigationViewModel by activityViewModels() - protected val playbackModel: PlaybackViewModel by activityViewModels() - - override fun onCreateBinding(inflater: LayoutInflater): FragmentDetailBinding = - FragmentDetailBinding.inflate(inflater) - - override fun onDestroyBinding(binding: FragmentDetailBinding) { - super.onDestroyBinding(binding) - binding.detailToolbar.setOnMenuItemClickListener(null) - binding.detailRecycler.adapter = null - } - - /** - * Shortcut method for doing setup of the detail toolbar. - * @param data Parent data to use as the toolbar title - * @param menuId Menu resource to use - */ - protected fun setupToolbar(data: MusicParent, @MenuRes menuId: Int) { - requireBinding().detailToolbar.apply { - title = data.resolveName(context) - inflateMenu(menuId) - setNavigationOnClickListener { findNavController().navigateUp() } - setOnMenuItemClickListener(this@DetailFragment) - } - } - - /** - * Shortcut method for spinning up the sorting [PopupMenu] - * @param anchor The view to anchor the sort menu to - * @param sort The initial sort - * @param onConfirm What to do when the sort is confirmed - * @param showItem Which menu items to keep - */ - protected fun showSortMenu( - anchor: View, - sort: Sort, - onConfirm: (Sort) -> Unit, - showItem: ((Int) -> Boolean)? = null, - ) { - logD("Launching menu") - - PopupMenu(anchor.context, anchor).apply { - inflate(R.menu.menu_detail_sort) - - setOnMenuItemClickListener { item -> - if (item.itemId == R.id.option_sort_asc) { - item.isChecked = !item.isChecked - onConfirm(sort.ascending(item.isChecked)) - } else { - item.isChecked = true - onConfirm(unlikelyToBeNull(sort.assignId(item.itemId))) - } - - true - } - - if (showItem != null) { - for (item in menu.children) { - item.isVisible = showItem(item.itemId) - } - } - - menu.findItem(sort.itemId).isChecked = true - menu.findItem(R.id.option_sort_asc).isChecked = sort.isAscending - - show() - } - } -} diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt index 8ef9e9d40..1d5063bfa 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt @@ -46,10 +46,10 @@ import org.oxycblt.auxio.util.logW import org.oxycblt.auxio.util.unlikelyToBeNull /** - * ViewModel that stores data for the [DetailFragment]s. This includes: + * ViewModel that stores data for the detail fragments. This includes: * - What item the fragment should be showing * - The RecyclerView data for each fragment - * - Menu triggers for each fragment + * - The sorts for each type of data * @author OxygenCobalt */ class DetailViewModel(application: Application) : diff --git a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt index 9c8e0b7db..48aec8aeb 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt @@ -18,8 +18,11 @@ package org.oxycblt.auxio.detail import android.os.Bundle +import android.view.LayoutInflater import android.view.MenuItem import android.view.View +import androidx.appcompat.widget.Toolbar +import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import org.oxycblt.auxio.R @@ -36,27 +39,37 @@ import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.state.PlaybackMode import org.oxycblt.auxio.ui.Header import org.oxycblt.auxio.ui.Item -import org.oxycblt.auxio.ui.newMenu +import org.oxycblt.auxio.ui.MenuFragment 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.logW import org.oxycblt.auxio.util.showToast import org.oxycblt.auxio.util.unlikelyToBeNull /** - * The [DetailFragment] for a genre. + * A fragment that shows information for a particular [Genre]. * @author OxygenCobalt */ -class GenreDetailFragment : DetailFragment(), DetailAdapter.Listener { +class GenreDetailFragment : + MenuFragment(), Toolbar.OnMenuItemClickListener, DetailAdapter.Listener { + private val detailModel: DetailViewModel by activityViewModels() + private val args: GenreDetailFragmentArgs by navArgs() private val detailAdapter = GenreDetailAdapter(this) + override fun onCreateBinding(inflater: LayoutInflater) = FragmentDetailBinding.inflate(inflater) + override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) { detailModel.setGenreId(args.genreId) - setupToolbar( - unlikelyToBeNull(detailModel.currentArtist.value), R.menu.menu_genre_artist_detail) + binding.detailToolbar.apply { + inflateMenu(R.menu.menu_genre_artist_detail) + setNavigationOnClickListener { findNavController().navigateUp() } + setOnMenuItemClickListener(this@GenreDetailFragment) + } + binding.detailRecycler.apply { adapter = detailAdapter applySpans { pos -> @@ -73,6 +86,12 @@ class GenreDetailFragment : DetailFragment(), DetailAdapter.Listener { launch { playbackModel.song.collectWith(playbackModel.parent, ::updatePlayback) } } + override fun onDestroyBinding(binding: FragmentDetailBinding) { + super.onDestroyBinding(binding) + binding.detailToolbar.setOnMenuItemClickListener(null) + binding.detailRecycler.adapter = null + } + override fun onMenuItemClick(item: MenuItem): Boolean { return when (item.itemId) { R.id.action_play_next -> { @@ -99,7 +118,10 @@ class GenreDetailFragment : DetailFragment(), DetailAdapter.Listener { } override fun onOpenMenu(item: Item, anchor: View) { - newMenu(anchor, item) + when (item) { + is Song -> musicMenu(anchor, R.menu.menu_song_actions, item) + else -> logW("Unexpected datatype when opening menu: ${item::class.java}") + } } override fun onPlayParent() { @@ -111,17 +133,25 @@ class GenreDetailFragment : DetailFragment(), DetailAdapter.Listener { } override fun onShowSortMenu(anchor: View) { - showSortMenu( - anchor, - detailModel.genreSort, - onConfirm = { detailModel.genreSort = it }, - showItem = { it != R.id.option_sort_disc && it != R.id.option_sort_track }) + menu(anchor, R.menu.menu_genre_sort) { + val sort = detailModel.genreSort + requireNotNull(menu.findItem(sort.itemId)).isChecked = true + requireNotNull(menu.findItem(R.id.option_sort_asc)).isChecked = sort.isAscending + setOnMenuItemClickListener { item -> + item.isChecked = !item.isChecked + detailModel.genreSort = requireNotNull(sort.assignId(item.itemId)) + true + } + } } private fun handleItemChange(genre: Genre?) { if (genre == null) { findNavController().navigateUp() + return } + + requireBinding().detailToolbar.title = genre.resolveName(requireContext()) } private fun handleNavigation(item: Music?) { diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt index bc2893784..a699b9584 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt @@ -30,9 +30,9 @@ import org.oxycblt.auxio.ui.MenuItemListener import org.oxycblt.auxio.ui.MonoAdapter import org.oxycblt.auxio.ui.Sort import org.oxycblt.auxio.ui.SyncBackingData -import org.oxycblt.auxio.ui.newMenu import org.oxycblt.auxio.util.formatDuration import org.oxycblt.auxio.util.launch +import org.oxycblt.auxio.util.logW import org.oxycblt.auxio.util.unlikelyToBeNull /** @@ -84,7 +84,10 @@ class AlbumListFragment : HomeListFragment() { } override fun onOpenMenu(item: Item, anchor: View) { - newMenu(anchor, item) + when (item) { + is Album -> musicMenu(anchor, R.menu.menu_album_actions, item) + else -> logW("Unexpected datatype when opening menu: ${item::class.java}") + } } class AlbumAdapter(listener: MenuItemListener) : diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt index 034bea7ff..d926bd191 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt @@ -30,9 +30,9 @@ import org.oxycblt.auxio.ui.MenuItemListener import org.oxycblt.auxio.ui.MonoAdapter import org.oxycblt.auxio.ui.Sort import org.oxycblt.auxio.ui.SyncBackingData -import org.oxycblt.auxio.ui.newMenu import org.oxycblt.auxio.util.formatDuration import org.oxycblt.auxio.util.launch +import org.oxycblt.auxio.util.logW import org.oxycblt.auxio.util.unlikelyToBeNull /** @@ -78,7 +78,10 @@ class ArtistListFragment : HomeListFragment() { } override fun onOpenMenu(item: Item, anchor: View) { - newMenu(anchor, item) + when (item) { + is Artist -> musicMenu(anchor, R.menu.menu_genre_artist_actions, item) + else -> logW("Unexpected datatype when opening menu: ${item::class.java}") + } } class ArtistAdapter(listener: MenuItemListener) : diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt index 37f280e3a..146f34a57 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt @@ -30,9 +30,9 @@ import org.oxycblt.auxio.ui.MenuItemListener import org.oxycblt.auxio.ui.MonoAdapter import org.oxycblt.auxio.ui.Sort import org.oxycblt.auxio.ui.SyncBackingData -import org.oxycblt.auxio.ui.newMenu import org.oxycblt.auxio.util.formatDuration import org.oxycblt.auxio.util.launch +import org.oxycblt.auxio.util.logW import org.oxycblt.auxio.util.unlikelyToBeNull /** @@ -78,7 +78,10 @@ class GenreListFragment : HomeListFragment() { } override fun onOpenMenu(item: Item, anchor: View) { - newMenu(anchor, item) + when (item) { + is Genre -> musicMenu(anchor, R.menu.menu_genre_artist_actions, item) + else -> logW("Unexpected datatype when opening menu: ${item::class.java}") + } } class GenreAdapter(listener: MenuItemListener) : diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/HomeListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/HomeListFragment.kt index 743ef17f8..dabe612dc 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/HomeListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/HomeListFragment.kt @@ -24,11 +24,9 @@ import androidx.fragment.app.activityViewModels import org.oxycblt.auxio.databinding.FragmentHomeListBinding import org.oxycblt.auxio.home.HomeViewModel import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView -import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.ui.Item +import org.oxycblt.auxio.ui.MenuFragment import org.oxycblt.auxio.ui.MenuItemListener -import org.oxycblt.auxio.ui.NavigationViewModel -import org.oxycblt.auxio.ui.ViewBindingFragment import org.oxycblt.auxio.util.applySpans /** @@ -36,12 +34,10 @@ import org.oxycblt.auxio.util.applySpans * @author OxygenCobalt */ abstract class HomeListFragment : - ViewBindingFragment(), + MenuFragment(), MenuItemListener, FastScrollRecyclerView.PopupProvider, FastScrollRecyclerView.OnFastScrollListener { - protected val playbackModel: PlaybackViewModel by activityViewModels() - protected val navModel: NavigationViewModel by activityViewModels() protected val homeModel: HomeViewModel by activityViewModels() override fun onCreateBinding(inflater: LayoutInflater) = diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt index 089a71030..ac2d3c43c 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt @@ -29,9 +29,9 @@ import org.oxycblt.auxio.ui.MonoAdapter import org.oxycblt.auxio.ui.SongViewHolder import org.oxycblt.auxio.ui.Sort import org.oxycblt.auxio.ui.SyncBackingData -import org.oxycblt.auxio.ui.newMenu import org.oxycblt.auxio.util.formatDuration import org.oxycblt.auxio.util.launch +import org.oxycblt.auxio.util.logW import org.oxycblt.auxio.util.unlikelyToBeNull /** @@ -85,7 +85,10 @@ class SongListFragment : HomeListFragment() { } override fun onOpenMenu(item: Item, anchor: View) { - newMenu(anchor, item) + when (item) { + is Song -> musicMenu(anchor, R.menu.menu_song_actions, item) + else -> logW("Unexpected datatype when opening menu: ${item::class.java}") + } } inner class SongsAdapter(listener: MenuItemListener) : diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt index 3393ffaab..01afdbf55 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt @@ -27,7 +27,6 @@ import androidx.core.view.isInvisible import androidx.core.view.postDelayed import androidx.core.widget.addTextChangedListener import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentSearchBinding @@ -37,17 +36,15 @@ import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.ui.Header import org.oxycblt.auxio.ui.Item +import org.oxycblt.auxio.ui.MenuFragment import org.oxycblt.auxio.ui.MenuItemListener -import org.oxycblt.auxio.ui.NavigationViewModel -import org.oxycblt.auxio.ui.ViewBindingFragment -import org.oxycblt.auxio.ui.newMenu import org.oxycblt.auxio.util.androidViewModels import org.oxycblt.auxio.util.applySpans import org.oxycblt.auxio.util.getSystemServiceSafe import org.oxycblt.auxio.util.launch +import org.oxycblt.auxio.util.logW import org.oxycblt.auxio.util.requireAttached /** @@ -55,13 +52,9 @@ import org.oxycblt.auxio.util.requireAttached * @author OxygenCobalt */ class SearchFragment : - ViewBindingFragment(), - MenuItemListener, - Toolbar.OnMenuItemClickListener { + MenuFragment(), MenuItemListener, Toolbar.OnMenuItemClickListener { // SearchViewModel is only scoped to this Fragment private val searchModel: SearchViewModel by androidViewModels() - private val playbackModel: PlaybackViewModel by activityViewModels() - private val navModel: NavigationViewModel by activityViewModels() private val searchAdapter = SearchAdapter(this) private var imm: InputMethodManager? = null @@ -137,7 +130,13 @@ class SearchFragment : } override fun onOpenMenu(item: Item, anchor: View) { - newMenu(anchor, item) + when (item) { + is Song -> musicMenu(anchor, R.menu.menu_song_actions, item) + is Album -> musicMenu(anchor, R.menu.menu_album_actions, item) + is Artist -> musicMenu(anchor, R.menu.menu_genre_artist_actions, item) + is Genre -> musicMenu(anchor, R.menu.menu_genre_artist_actions, item) + else -> logW("Unexpected datatype when opening menu: ${item::class.java}") + } } private fun updateResults(results: List) { diff --git a/app/src/main/java/org/oxycblt/auxio/ui/ActionMenu.kt b/app/src/main/java/org/oxycblt/auxio/ui/ActionMenu.kt deleted file mode 100644 index 32917a277..000000000 --- a/app/src/main/java/org/oxycblt/auxio/ui/ActionMenu.kt +++ /dev/null @@ -1,226 +0,0 @@ -/* - * Copyright (c) 2021 Auxio Project - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.oxycblt.auxio.ui - -import android.view.View -import androidx.annotation.IdRes -import androidx.annotation.MenuRes -import androidx.appcompat.app.AppCompatActivity -import androidx.appcompat.widget.PopupMenu -import androidx.core.view.children -import androidx.fragment.app.Fragment -import androidx.lifecycle.ViewModelProvider -import org.oxycblt.auxio.R -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.playback.PlaybackViewModel -import org.oxycblt.auxio.util.showToast - -/** - * Extension method for creating and showing a new [ActionMenu]. - * @param anchor [View] This should be centered around - * @param data [Item] this menu corresponds to - * @param flag (Optional, defaults to [ActionMenu.FLAG_NONE]) Any extra flags to accompany the data. - * @see ActionMenu - */ -fun Fragment.newMenu(anchor: View, data: Item, flag: Int = ActionMenu.FLAG_NONE) { - ActionMenu(requireActivity() as AppCompatActivity, anchor, data, flag).show() -} - -/** - * A wrapper around [PopupMenu] that automates the menu creation for nearly every datatype in Auxio. - * @param activity [AppCompatActivity] required as both a context and ViewModelStore owner. - * @param anchor [View] This should be centered around - * @param data [Item] this menu corresponds to - * @param flag Any extra flags to accompany the data. See [FLAG_NONE], [FLAG_IN_ALBUM], - * [FLAG_IN_ARTIST], [FLAG_IN_GENRE] for more details. - * @throws IllegalStateException When there is no menu for this specific datatype/flag - * @author OxygenCobalt - * - * TODO: Prevent duplicate menus from showing up (merge into ViewBindingFragment) - * - * TODO: Add multi-select - */ -class ActionMenu( - private val activity: AppCompatActivity, - anchor: View, - private val data: Item, - private val flag: Int -) : PopupMenu(activity, anchor) { - private val context = activity.applicationContext - - // Get ViewModels using the activity as the store owner - private val navModel: NavigationViewModel by lazy { - ViewModelProvider(activity)[NavigationViewModel::class.java] - } - - private val playbackModel: PlaybackViewModel by lazy { - ViewModelProvider(activity)[PlaybackViewModel::class.java] - } - - init { - val menuRes = determineMenu() - - check(menuRes != -1) { - "There is no menu associated with datatype ${data::class.simpleName} and flag $flag" - } - - inflate(menuRes) - - // Disable any queue options if we don't have anything playing. - for (item in menu.children) { - if (item.itemId == R.id.action_play_next || item.itemId == R.id.action_queue_add) { - item.isEnabled = playbackModel.song.value != null - } - } - - setOnMenuItemClickListener { item -> - onMenuClick(item.itemId) - true - } - } - - /** Figure out what menu to use here, based on the data & flags */ - @MenuRes - private fun determineMenu(): Int { - return when (data) { - is Song -> { - when (flag) { - FLAG_NONE, - FLAG_IN_GENRE -> R.menu.menu_song_actions - FLAG_IN_ALBUM -> R.menu.menu_album_song_actions - FLAG_IN_ARTIST -> R.menu.menu_artist_song_actions - else -> -1 - } - } - is Album -> { - when (flag) { - FLAG_NONE -> R.menu.menu_album_actions - FLAG_IN_ARTIST -> R.menu.menu_artist_album_actions - else -> -1 - } - } - is Artist, - is Genre -> R.menu.menu_genre_artist_actions - else -> -1 - } - } - - /** Determine what to do when a MenuItem is clicked. */ - private fun onMenuClick(@IdRes id: Int) { - when (id) { - R.id.action_play -> { - when (data) { - is Album -> playbackModel.play(data, false) - is Artist -> playbackModel.play(data, false) - is Genre -> playbackModel.play(data, false) - else -> {} - } - } - R.id.action_shuffle -> { - when (data) { - is Album -> playbackModel.play(data, true) - is Artist -> playbackModel.play(data, true) - is Genre -> playbackModel.play(data, true) - else -> {} - } - } - R.id.action_play_next -> { - when (data) { - is Song -> { - playbackModel.playNext(data) - context.showToast(R.string.lbl_queue_added) - } - is Album -> { - 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 -> {} - } - } - R.id.action_queue_add -> { - when (data) { - is Song -> { - playbackModel.addToQueue(data) - context.showToast(R.string.lbl_queue_added) - } - is Album -> { - 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 -> {} - } - } - R.id.action_go_album -> { - if (data is Song) { - navModel.exploreNavigateTo(data.album) - } - } - R.id.action_go_artist -> { - if (data is Song) { - navModel.exploreNavigateTo(data.album.artist) - } else if (data is Album) { - navModel.exploreNavigateTo(data.artist) - } - } - R.id.action_song_detail -> { - if (data is Song) { - navModel.mainNavigateTo(MainNavigationAction.SongDetails(data)) - } - } - } - } - - companion object { - /** No Flags */ - const val FLAG_NONE = -1 - /** - * Flag for when a menu is opened from an artist (See - * [org.oxycblt.auxio.detail.ArtistDetailFragment]) - */ - const val FLAG_IN_ARTIST = 0 - /** - * Flag for when a menu is opened from an album (See - * [org.oxycblt.auxio.detail.AlbumDetailFragment]) - */ - const val FLAG_IN_ALBUM = 1 - /** - * Flag for when a menu is opened from a genre (See - * [org.oxycblt.auxio.detail.GenreDetailFragment]) - */ - const val FLAG_IN_GENRE = 2 - } -} diff --git a/app/src/main/java/org/oxycblt/auxio/ui/MenuFragment.kt b/app/src/main/java/org/oxycblt/auxio/ui/MenuFragment.kt new file mode 100644 index 000000000..e7bbefd32 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/ui/MenuFragment.kt @@ -0,0 +1,186 @@ +/* + * Copyright (c) 2022 Auxio Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.auxio.ui + +import android.view.View +import androidx.annotation.MenuRes +import androidx.appcompat.widget.PopupMenu +import androidx.core.view.children +import androidx.fragment.app.activityViewModels +import androidx.viewbinding.ViewBinding +import org.oxycblt.auxio.R +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.playback.PlaybackViewModel +import org.oxycblt.auxio.util.logW +import org.oxycblt.auxio.util.showToast + +abstract class MenuFragment : ViewBindingFragment() { + private var currentMenu: PopupMenu? = null + + protected val playbackModel: PlaybackViewModel by activityViewModels() + protected val navModel: NavigationViewModel by activityViewModels() + + protected fun musicMenu(anchor: View, @MenuRes menuRes: Int, song: Song) { + musicMenuImpl(anchor, menuRes) { id -> + when (id) { + R.id.action_play_next -> { + playbackModel.playNext(song) + requireContext().showToast(R.string.lbl_queue_added) + } + R.id.action_queue_add -> { + playbackModel.addToQueue(song) + requireContext().showToast(R.string.lbl_queue_added) + } + R.id.action_go_artist -> { + navModel.exploreNavigateTo(song.album.artist) + } + R.id.action_go_album -> { + navModel.exploreNavigateTo(song.album) + } + R.id.action_song_detail -> { + navModel.mainNavigateTo(MainNavigationAction.SongDetails(song)) + } + else -> { + logW("Unknown menu item selected") + return@musicMenuImpl false + } + } + + true + } + } + + protected fun musicMenu(anchor: View, @MenuRes menuRes: Int, album: Album) { + musicMenuImpl(anchor, menuRes) { id -> + when (id) { + R.id.action_play -> { + playbackModel.play(album, false) + } + R.id.action_shuffle -> { + playbackModel.play(album, true) + } + R.id.action_play_next -> { + playbackModel.playNext(album) + requireContext().showToast(R.string.lbl_queue_added) + } + R.id.action_queue_add -> { + playbackModel.addToQueue(album) + requireContext().showToast(R.string.lbl_queue_added) + } + R.id.action_go_artist -> { + navModel.exploreNavigateTo(album.artist) + } + else -> { + logW("Unknown menu item selected") + return@musicMenuImpl false + } + } + + true + } + } + + protected fun musicMenu(anchor: View, @MenuRes menuRes: Int, artist: Artist) { + musicMenuImpl(anchor, menuRes) { id -> + when (id) { + R.id.action_play -> { + playbackModel.play(artist, false) + } + R.id.action_shuffle -> { + playbackModel.play(artist, true) + } + R.id.action_play_next -> { + playbackModel.playNext(artist) + requireContext().showToast(R.string.lbl_queue_added) + } + R.id.action_queue_add -> { + playbackModel.addToQueue(artist) + requireContext().showToast(R.string.lbl_queue_added) + } + else -> { + logW("Unknown menu item selected") + return@musicMenuImpl false + } + } + + true + } + } + + protected fun musicMenu(anchor: View, @MenuRes menuRes: Int, genre: Genre) { + musicMenuImpl(anchor, menuRes) { id -> + when (id) { + R.id.action_play -> { + playbackModel.play(genre, false) + } + R.id.action_shuffle -> { + playbackModel.play(genre, true) + } + R.id.action_play_next -> { + playbackModel.playNext(genre) + requireContext().showToast(R.string.lbl_queue_added) + } + R.id.action_queue_add -> { + playbackModel.addToQueue(genre) + requireContext().showToast(R.string.lbl_queue_added) + } + else -> { + logW("Unknown menu item selected") + return@musicMenuImpl false + } + } + + true + } + } + + private fun musicMenuImpl(anchor: View, @MenuRes menuRes: Int, onSelect: (Int) -> Boolean) { + menu(anchor, menuRes) { + for (item in menu.children) { + if (item.itemId == R.id.action_play_next || item.itemId == R.id.action_queue_add) { + item.isEnabled = playbackModel.song.value != null + } + } + + setOnMenuItemClickListener { item -> onSelect(item.itemId) } + } + } + + protected fun menu(anchor: View, @MenuRes menuRes: Int, block: PopupMenu.() -> Unit) { + if (currentMenu != null) { + return + } + + currentMenu = + PopupMenu(requireContext(), anchor).apply { + inflate(menuRes) + block() + setOnDismissListener { currentMenu = null } + show() + } + } + + override fun onDestroyBinding(binding: T) { + super.onDestroyBinding(binding) + currentMenu?.dismiss() + currentMenu = null + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/ui/Sort.kt b/app/src/main/java/org/oxycblt/auxio/ui/Sort.kt index 085d58163..5d26c200e 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/Sort.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/Sort.kt @@ -318,6 +318,7 @@ sealed class Sort(open val isAscending: Boolean) { */ fun assignId(@IdRes id: Int): Sort? { return when (id) { + R.id.option_sort_asc -> ascending(!isAscending) R.id.option_sort_name -> ByName(isAscending) R.id.option_sort_artist -> ByArtist(isAscending) R.id.option_sort_album -> ByAlbum(isAscending) diff --git a/app/src/main/res/menu/menu_album_detail.xml b/app/src/main/res/menu/menu_album_detail.xml index 4c5d8d7d5..9bf6ba0a6 100644 --- a/app/src/main/res/menu/menu_album_detail.xml +++ b/app/src/main/res/menu/menu_album_detail.xml @@ -9,4 +9,7 @@ + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_album_song_actions.xml b/app/src/main/res/menu/menu_album_song_actions.xml index 4c5d8d7d5..9bf6ba0a6 100644 --- a/app/src/main/res/menu/menu_album_song_actions.xml +++ b/app/src/main/res/menu/menu_album_song_actions.xml @@ -9,4 +9,7 @@ + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_album_sort.xml b/app/src/main/res/menu/menu_album_sort.xml new file mode 100644 index 000000000..749c77b90 --- /dev/null +++ b/app/src/main/res/menu/menu_album_sort.xml @@ -0,0 +1,16 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_artist_song_actions.xml b/app/src/main/res/menu/menu_artist_song_actions.xml index e1e9c4a5d..14987e8f5 100644 --- a/app/src/main/res/menu/menu_artist_song_actions.xml +++ b/app/src/main/res/menu/menu_artist_song_actions.xml @@ -9,4 +9,7 @@ + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_artist_sort.xml b/app/src/main/res/menu/menu_artist_sort.xml new file mode 100644 index 000000000..a5e073b61 --- /dev/null +++ b/app/src/main/res/menu/menu_artist_sort.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_detail_sort.xml b/app/src/main/res/menu/menu_genre_sort.xml similarity index 80% rename from app/src/main/res/menu/menu_detail_sort.xml rename to app/src/main/res/menu/menu_genre_sort.xml index f1e0da365..07847918e 100644 --- a/app/src/main/res/menu/menu_detail_sort.xml +++ b/app/src/main/res/menu/menu_genre_sort.xml @@ -16,12 +16,6 @@ - -