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
This commit is contained in:
OxygenCobalt 2022-06-15 10:02:52 -06:00
parent 5d1eaf72dd
commit 2f85d694d1
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
23 changed files with 423 additions and 414 deletions

View file

@ -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]

View file

@ -46,6 +46,8 @@ import org.oxycblt.auxio.util.logD
*
* TODO: Rework padding ethos
*
* TODO: Add multi-select
*
* @author OxygenCobalt
*/
class MainActivity : AppCompatActivity() {

View file

@ -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<FragmentDetailBinding>(),
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?) {

View file

@ -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<FragmentDetailBinding>(), 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?) {

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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<FragmentDetailBinding>(), 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()
}
}
}

View file

@ -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) :

View file

@ -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<FragmentDetailBinding>(), 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?) {

View file

@ -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<Album>() {
}
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) :

View file

@ -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<Artist>() {
}
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) :

View file

@ -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<Genre>() {
}
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) :

View file

@ -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<T : Item> :
ViewBindingFragment<FragmentHomeListBinding>(),
MenuFragment<FragmentHomeListBinding>(),
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) =

View file

@ -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<Song>() {
}
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) :

View file

@ -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<FragmentSearchBinding>(),
MenuItemListener,
Toolbar.OnMenuItemClickListener {
MenuFragment<FragmentSearchBinding>(), 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<Item>) {

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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
}
}

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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<T : ViewBinding> : ViewBindingFragment<T>() {
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
}
}

View file

@ -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)

View file

@ -9,4 +9,7 @@
<item
android:id="@+id/action_go_artist"
android:title="@string/lbl_go_artist" />
<item
android:id="@+id/action_song_detail"
android:title="@string/lbl_song_detail" />
</menu>

View file

@ -9,4 +9,7 @@
<item
android:id="@+id/action_go_artist"
android:title="@string/lbl_go_artist" />
<item
android:id="@+id/action_song_detail"
android:title="@string/lbl_song_detail" />
</menu>

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<group android:checkableBehavior="single">
<item
android:id="@+id/option_sort_disc"
android:title="@string/lbl_sort_disc" />
<item
android:id="@+id/option_sort_track"
android:title="@string/lbl_sort_track" />
</group>
<group android:checkableBehavior="all">
<item
android:id="@+id/option_sort_asc"
android:title="@string/lbl_sort_asc" />
</group>
</menu>

View file

@ -9,4 +9,7 @@
<item
android:id="@+id/action_go_album"
android:title="@string/lbl_go_album" />
<item
android:id="@+id/action_song_detail"
android:title="@string/lbl_song_detail" />
</menu>

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<group android:checkableBehavior="single">
<item
android:id="@+id/option_sort_name"
android:title="@string/lbl_sort_name" />
<item
android:id="@+id/option_sort_album"
android:title="@string/lbl_sort_album" />
<item
android:id="@+id/option_sort_year"
android:title="@string/lbl_sort_year" />
<item
android:id="@+id/option_sort_duration"
android:title="@string/lbl_sort_duration" />
</group>
<group android:checkableBehavior="all">
<item
android:id="@+id/option_sort_asc"
android:title="@string/lbl_sort_asc" />
</group>
</menu>

View file

@ -16,12 +16,6 @@
<item
android:id="@+id/option_sort_duration"
android:title="@string/lbl_sort_duration" />
<item
android:id="@+id/option_sort_disc"
android:title="@string/lbl_sort_disc" />
<item
android:id="@+id/option_sort_track"
android:title="@string/lbl_sort_track" />
</group>
<group android:checkableBehavior="all">
<item

View file

@ -25,6 +25,17 @@ This is probably caused by one of two reasons:
2. If the aforementioned players don't work, but players like Vanilla Music and VLC do, then it's a problem with the Media APIs that Auxio relies on. There is nothing I can do about it.
- I hope to mitigate these issues in the future by extracting metadata myself or adding Subsonic/SoundPod support, however this is extremely far off.
Some common issues are listed below.
#### My FLAC/OGG/OPUS files don't have dates!
Android does not read the `DATE` tag from vorbis files. It reads the `YEAR` tag. This is because android's metadata parser is
stuck in 2008.
#### Some files with accented/symbolic characters have corrupted tags!
When Android extracts metadata, at some point it tries to convert the bytes it extracted to a java string, which apparently involves detecting the encoding of the data dynamically and
then converting it to Java's Unicode dialect. Of course, trying to detect codings on the fly like that is a [terrible idea](https://en.wikipedia.org/wiki/Bush_hid_the_facts), and more
often than not it results in UTF-8 tags (Seen on FLAC/OGG/OPUS files most often) being corrupted.
#### I have a large library and Auxio takes really long to load it!
This is expected since reading from the audio database takes awhile, especially with libraries containing 10k songs or more.
@ -39,6 +50,11 @@ such a field, it will result in fragmented artists. The reason why Auxio does no
for separators and then extract artists that way is that it risks mangling artists that don't
actually have collaborators, such as "Black Country, New Road" becoming "Black Country".
#### Why does Auxio not detect disc numbers on my device?
If your device runs Android 10, then Auxio cannot parse a disc from the media database due to
a regression introduced by Google in that version. If your device is running another version,
please file an issue.
#### ReplayGain isn't working on my music!
This is for a couple reason:
- Auxio doesn't extract ReplayGain tags for your format. This is a problem on ExoPlayer's end and should be
@ -46,6 +62,17 @@ investigated there.
- Auxio doesn't recognize your ReplayGain tags. This is usually because of a non-standard tag like ID3v2's `RVAD` or
an unrecognized name.
#### My lossless audio sounds lower-quality in Auxio!
This is a current limitation with the ExoPlayer. Basically, all audio tend is downsampled to 16-bit PCM audio, even
if the source audio is higher quality. I can enable something that might be able to remedy such, but implementing it
fully may take some time.
#### Why is playback distorted when I play my FLAC/WAV files?
ExoPlayer, while powerful, does add some overhead when playing exceptionally high-quality files (2000+ KB/s bitrate,
90000+ Hz sample rate). This is worsened by the ReplayGain system, as it has to copy the audio buffer no matter what.
This results in choppy, distorted playback in some case as audio data cannot be delivered in time. Sadly, there is
not much I can do about this right now.
#### What is dynamic ReplayGain?
Dynamic ReplayGain is a quirk setting based off the FooBar2000 plugin that dynamically switches from track gain to album
gain depending on if the current playback is from an album or not.