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:
parent
5d1eaf72dd
commit
2f85d694d1
23 changed files with 423 additions and 414 deletions
|
@ -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]
|
||||
|
|
|
@ -46,6 +46,8 @@ import org.oxycblt.auxio.util.logD
|
|||
*
|
||||
* TODO: Rework padding ethos
|
||||
*
|
||||
* TODO: Add multi-select
|
||||
*
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
|
|
@ -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?) {
|
||||
|
|
|
@ -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?) {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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) :
|
||||
|
|
|
@ -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?) {
|
||||
|
|
|
@ -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) :
|
||||
|
|
|
@ -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) :
|
||||
|
|
|
@ -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) :
|
||||
|
|
|
@ -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) =
|
||||
|
|
|
@ -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) :
|
||||
|
|
|
@ -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>) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
186
app/src/main/java/org/oxycblt/auxio/ui/MenuFragment.kt
Normal file
186
app/src/main/java/org/oxycblt/auxio/ui/MenuFragment.kt
Normal 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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
16
app/src/main/res/menu/menu_album_sort.xml
Normal file
16
app/src/main/res/menu/menu_album_sort.xml
Normal 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>
|
|
@ -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>
|
22
app/src/main/res/menu/menu_artist_sort.xml
Normal file
22
app/src/main/res/menu/menu_artist_sort.xml
Normal 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>
|
|
@ -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
|
27
info/FAQ.md
27
info/FAQ.md
|
@ -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.
|
||||
|
|
Loading…
Reference in a new issue