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]
|
- Playback bar now picks the larger inset in case that gesture inset is missing [#149]
|
||||||
- Fixed unusable excluded directory UI
|
- Fixed unusable excluded directory UI
|
||||||
- Songs with no data (i.e size of 0) are now filtered out
|
- 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
|
#### Dev/Meta
|
||||||
- New translations [Fjuro -> Czech, Konstantin Tutsch -> German]
|
- New translations [Fjuro -> Czech, Konstantin Tutsch -> German]
|
||||||
|
|
|
@ -46,6 +46,8 @@ import org.oxycblt.auxio.util.logD
|
||||||
*
|
*
|
||||||
* TODO: Rework padding ethos
|
* TODO: Rework padding ethos
|
||||||
*
|
*
|
||||||
|
* TODO: Add multi-select
|
||||||
|
*
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
|
|
|
@ -19,9 +19,12 @@ package org.oxycblt.auxio.detail
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.core.view.children
|
import androidx.core.view.children
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.navigation.fragment.navArgs
|
import androidx.navigation.fragment.navArgs
|
||||||
import androidx.recyclerview.widget.LinearSmoothScroller
|
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.playback.state.PlaybackMode
|
||||||
import org.oxycblt.auxio.ui.Header
|
import org.oxycblt.auxio.ui.Header
|
||||||
import org.oxycblt.auxio.ui.Item
|
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.applySpans
|
||||||
import org.oxycblt.auxio.util.canScroll
|
import org.oxycblt.auxio.util.canScroll
|
||||||
import org.oxycblt.auxio.util.collectWith
|
import org.oxycblt.auxio.util.collectWith
|
||||||
|
@ -48,17 +51,29 @@ import org.oxycblt.auxio.util.showToast
|
||||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The [DetailFragment] for an album.
|
* A fragment that shows information for a particular [Album].
|
||||||
* @author OxygenCobalt
|
* @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 args: AlbumDetailFragmentArgs by navArgs()
|
||||||
private val detailAdapter = AlbumDetailAdapter(this)
|
private val detailAdapter = AlbumDetailAdapter(this)
|
||||||
|
|
||||||
|
override fun onCreateBinding(inflater: LayoutInflater) = FragmentDetailBinding.inflate(inflater)
|
||||||
|
|
||||||
override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) {
|
override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) {
|
||||||
detailModel.setAlbumId(args.albumId)
|
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 {
|
requireBinding().detailRecycler.apply {
|
||||||
adapter = detailAdapter
|
adapter = detailAdapter
|
||||||
applySpans { pos ->
|
applySpans { pos ->
|
||||||
|
@ -75,6 +90,12 @@ class AlbumDetailFragment : DetailFragment(), AlbumDetailAdapter.Listener {
|
||||||
launch { playbackModel.song.collectWith(playbackModel.parent, ::updatePlayback) }
|
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 {
|
override fun onMenuItemClick(item: MenuItem): Boolean {
|
||||||
return when (item.itemId) {
|
return when (item.itemId) {
|
||||||
R.id.action_play_next -> {
|
R.id.action_play_next -> {
|
||||||
|
@ -102,7 +123,10 @@ class AlbumDetailFragment : DetailFragment(), AlbumDetailAdapter.Listener {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOpenMenu(item: Item, anchor: View) {
|
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() {
|
override fun onPlayParent() {
|
||||||
|
@ -114,15 +138,16 @@ class AlbumDetailFragment : DetailFragment(), AlbumDetailAdapter.Listener {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onShowSortMenu(anchor: View) {
|
override fun onShowSortMenu(anchor: View) {
|
||||||
showSortMenu(
|
menu(anchor, R.menu.menu_album_sort) {
|
||||||
anchor,
|
val sort = detailModel.albumSort
|
||||||
detailModel.albumSort,
|
requireNotNull(menu.findItem(sort.itemId)).isChecked = true
|
||||||
onConfirm = { detailModel.albumSort = it },
|
requireNotNull(menu.findItem(R.id.option_sort_asc)).isChecked = sort.isAscending
|
||||||
showItem = {
|
setOnMenuItemClickListener { item ->
|
||||||
it == R.id.option_sort_asc ||
|
item.isChecked = !item.isChecked
|
||||||
it == R.id.option_sort_disc ||
|
detailModel.albumSort = requireNotNull(sort.assignId(item.itemId))
|
||||||
it == R.id.option_sort_track
|
true
|
||||||
})
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNavigateToArtist() {
|
override fun onNavigateToArtist() {
|
||||||
|
@ -135,7 +160,10 @@ class AlbumDetailFragment : DetailFragment(), AlbumDetailAdapter.Listener {
|
||||||
private fun handleItemChange(album: Album?) {
|
private fun handleItemChange(album: Album?) {
|
||||||
if (album == null) {
|
if (album == null) {
|
||||||
findNavController().navigateUp()
|
findNavController().navigateUp()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
requireBinding().detailToolbar.title = album.resolveName(requireContext())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleNavigation(item: Music?) {
|
private fun handleNavigation(item: Music?) {
|
||||||
|
|
|
@ -18,8 +18,11 @@
|
||||||
package org.oxycblt.auxio.detail
|
package org.oxycblt.auxio.detail
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.appcompat.widget.Toolbar
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.navigation.fragment.navArgs
|
import androidx.navigation.fragment.navArgs
|
||||||
import org.oxycblt.auxio.R
|
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.playback.state.PlaybackMode
|
||||||
import org.oxycblt.auxio.ui.Header
|
import org.oxycblt.auxio.ui.Header
|
||||||
import org.oxycblt.auxio.ui.Item
|
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.applySpans
|
||||||
import org.oxycblt.auxio.util.collectWith
|
import org.oxycblt.auxio.util.collectWith
|
||||||
import org.oxycblt.auxio.util.launch
|
import org.oxycblt.auxio.util.launch
|
||||||
|
@ -45,18 +48,27 @@ import org.oxycblt.auxio.util.showToast
|
||||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The [DetailFragment] for an artist.
|
* A fragment that shows information for a particular [Artist].
|
||||||
* @author OxygenCobalt
|
* @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 args: ArtistDetailFragmentArgs by navArgs()
|
||||||
private val detailAdapter = ArtistDetailAdapter(this)
|
private val detailAdapter = ArtistDetailAdapter(this)
|
||||||
|
|
||||||
|
override fun onCreateBinding(inflater: LayoutInflater) = FragmentDetailBinding.inflate(inflater)
|
||||||
|
|
||||||
override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) {
|
override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) {
|
||||||
detailModel.setArtistId(args.artistId)
|
detailModel.setArtistId(args.artistId)
|
||||||
|
|
||||||
setupToolbar(
|
binding.detailToolbar.apply {
|
||||||
unlikelyToBeNull(detailModel.currentArtist.value), R.menu.menu_genre_artist_detail)
|
inflateMenu(R.menu.menu_genre_artist_detail)
|
||||||
|
setNavigationOnClickListener { findNavController().navigateUp() }
|
||||||
|
setOnMenuItemClickListener(this@ArtistDetailFragment)
|
||||||
|
}
|
||||||
|
|
||||||
requireBinding().detailRecycler.apply {
|
requireBinding().detailRecycler.apply {
|
||||||
adapter = detailAdapter
|
adapter = detailAdapter
|
||||||
applySpans { pos ->
|
applySpans { pos ->
|
||||||
|
@ -74,6 +86,12 @@ class ArtistDetailFragment : DetailFragment(), DetailAdapter.Listener {
|
||||||
launch { playbackModel.song.collectWith(playbackModel.parent, ::updatePlayback) }
|
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 {
|
override fun onMenuItemClick(item: MenuItem): Boolean {
|
||||||
return when (item.itemId) {
|
return when (item.itemId) {
|
||||||
R.id.action_play_next -> {
|
R.id.action_play_next -> {
|
||||||
|
@ -98,7 +116,11 @@ class ArtistDetailFragment : DetailFragment(), DetailAdapter.Listener {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOpenMenu(item: Item, anchor: View) {
|
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() {
|
override fun onPlayParent() {
|
||||||
|
@ -110,21 +132,25 @@ class ArtistDetailFragment : DetailFragment(), DetailAdapter.Listener {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onShowSortMenu(anchor: View) {
|
override fun onShowSortMenu(anchor: View) {
|
||||||
showSortMenu(
|
menu(anchor, R.menu.menu_artist_sort) {
|
||||||
anchor,
|
val sort = detailModel.artistSort
|
||||||
detailModel.artistSort,
|
requireNotNull(menu.findItem(sort.itemId)).isChecked = true
|
||||||
onConfirm = { detailModel.artistSort = it },
|
requireNotNull(menu.findItem(R.id.option_sort_asc)).isChecked = sort.isAscending
|
||||||
showItem = { id ->
|
setOnMenuItemClickListener { item ->
|
||||||
id != R.id.option_sort_artist &&
|
item.isChecked = !item.isChecked
|
||||||
id != R.id.option_sort_disc &&
|
detailModel.artistSort = requireNotNull(sort.assignId(item.itemId))
|
||||||
id != R.id.option_sort_track
|
true
|
||||||
})
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleItemChange(artist: Artist?) {
|
private fun handleItemChange(artist: Artist?) {
|
||||||
if (artist == null) {
|
if (artist == null) {
|
||||||
findNavController().navigateUp()
|
findNavController().navigateUp()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
requireBinding().detailToolbar.title = artist.resolveName(requireContext())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleNavigation(item: Music?) {
|
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
|
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
|
* - What item the fragment should be showing
|
||||||
* - The RecyclerView data for each fragment
|
* - The RecyclerView data for each fragment
|
||||||
* - Menu triggers for each fragment
|
* - The sorts for each type of data
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class DetailViewModel(application: Application) :
|
class DetailViewModel(application: Application) :
|
||||||
|
|
|
@ -18,8 +18,11 @@
|
||||||
package org.oxycblt.auxio.detail
|
package org.oxycblt.auxio.detail
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.appcompat.widget.Toolbar
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.navigation.fragment.navArgs
|
import androidx.navigation.fragment.navArgs
|
||||||
import org.oxycblt.auxio.R
|
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.playback.state.PlaybackMode
|
||||||
import org.oxycblt.auxio.ui.Header
|
import org.oxycblt.auxio.ui.Header
|
||||||
import org.oxycblt.auxio.ui.Item
|
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.applySpans
|
||||||
import org.oxycblt.auxio.util.collectWith
|
import org.oxycblt.auxio.util.collectWith
|
||||||
import org.oxycblt.auxio.util.launch
|
import org.oxycblt.auxio.util.launch
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
import org.oxycblt.auxio.util.logW
|
||||||
import org.oxycblt.auxio.util.showToast
|
import org.oxycblt.auxio.util.showToast
|
||||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The [DetailFragment] for a genre.
|
* A fragment that shows information for a particular [Genre].
|
||||||
* @author OxygenCobalt
|
* @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 args: GenreDetailFragmentArgs by navArgs()
|
||||||
private val detailAdapter = GenreDetailAdapter(this)
|
private val detailAdapter = GenreDetailAdapter(this)
|
||||||
|
|
||||||
|
override fun onCreateBinding(inflater: LayoutInflater) = FragmentDetailBinding.inflate(inflater)
|
||||||
|
|
||||||
override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) {
|
override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) {
|
||||||
detailModel.setGenreId(args.genreId)
|
detailModel.setGenreId(args.genreId)
|
||||||
|
|
||||||
setupToolbar(
|
binding.detailToolbar.apply {
|
||||||
unlikelyToBeNull(detailModel.currentArtist.value), R.menu.menu_genre_artist_detail)
|
inflateMenu(R.menu.menu_genre_artist_detail)
|
||||||
|
setNavigationOnClickListener { findNavController().navigateUp() }
|
||||||
|
setOnMenuItemClickListener(this@GenreDetailFragment)
|
||||||
|
}
|
||||||
|
|
||||||
binding.detailRecycler.apply {
|
binding.detailRecycler.apply {
|
||||||
adapter = detailAdapter
|
adapter = detailAdapter
|
||||||
applySpans { pos ->
|
applySpans { pos ->
|
||||||
|
@ -73,6 +86,12 @@ class GenreDetailFragment : DetailFragment(), DetailAdapter.Listener {
|
||||||
launch { playbackModel.song.collectWith(playbackModel.parent, ::updatePlayback) }
|
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 {
|
override fun onMenuItemClick(item: MenuItem): Boolean {
|
||||||
return when (item.itemId) {
|
return when (item.itemId) {
|
||||||
R.id.action_play_next -> {
|
R.id.action_play_next -> {
|
||||||
|
@ -99,7 +118,10 @@ class GenreDetailFragment : DetailFragment(), DetailAdapter.Listener {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOpenMenu(item: Item, anchor: View) {
|
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() {
|
override fun onPlayParent() {
|
||||||
|
@ -111,17 +133,25 @@ class GenreDetailFragment : DetailFragment(), DetailAdapter.Listener {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onShowSortMenu(anchor: View) {
|
override fun onShowSortMenu(anchor: View) {
|
||||||
showSortMenu(
|
menu(anchor, R.menu.menu_genre_sort) {
|
||||||
anchor,
|
val sort = detailModel.genreSort
|
||||||
detailModel.genreSort,
|
requireNotNull(menu.findItem(sort.itemId)).isChecked = true
|
||||||
onConfirm = { detailModel.genreSort = it },
|
requireNotNull(menu.findItem(R.id.option_sort_asc)).isChecked = sort.isAscending
|
||||||
showItem = { it != R.id.option_sort_disc && it != R.id.option_sort_track })
|
setOnMenuItemClickListener { item ->
|
||||||
|
item.isChecked = !item.isChecked
|
||||||
|
detailModel.genreSort = requireNotNull(sort.assignId(item.itemId))
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleItemChange(genre: Genre?) {
|
private fun handleItemChange(genre: Genre?) {
|
||||||
if (genre == null) {
|
if (genre == null) {
|
||||||
findNavController().navigateUp()
|
findNavController().navigateUp()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
requireBinding().detailToolbar.title = genre.resolveName(requireContext())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleNavigation(item: Music?) {
|
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.MonoAdapter
|
||||||
import org.oxycblt.auxio.ui.Sort
|
import org.oxycblt.auxio.ui.Sort
|
||||||
import org.oxycblt.auxio.ui.SyncBackingData
|
import org.oxycblt.auxio.ui.SyncBackingData
|
||||||
import org.oxycblt.auxio.ui.newMenu
|
|
||||||
import org.oxycblt.auxio.util.formatDuration
|
import org.oxycblt.auxio.util.formatDuration
|
||||||
import org.oxycblt.auxio.util.launch
|
import org.oxycblt.auxio.util.launch
|
||||||
|
import org.oxycblt.auxio.util.logW
|
||||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -84,7 +84,10 @@ class AlbumListFragment : HomeListFragment<Album>() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOpenMenu(item: Item, anchor: View) {
|
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) :
|
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.MonoAdapter
|
||||||
import org.oxycblt.auxio.ui.Sort
|
import org.oxycblt.auxio.ui.Sort
|
||||||
import org.oxycblt.auxio.ui.SyncBackingData
|
import org.oxycblt.auxio.ui.SyncBackingData
|
||||||
import org.oxycblt.auxio.ui.newMenu
|
|
||||||
import org.oxycblt.auxio.util.formatDuration
|
import org.oxycblt.auxio.util.formatDuration
|
||||||
import org.oxycblt.auxio.util.launch
|
import org.oxycblt.auxio.util.launch
|
||||||
|
import org.oxycblt.auxio.util.logW
|
||||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -78,7 +78,10 @@ class ArtistListFragment : HomeListFragment<Artist>() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOpenMenu(item: Item, anchor: View) {
|
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) :
|
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.MonoAdapter
|
||||||
import org.oxycblt.auxio.ui.Sort
|
import org.oxycblt.auxio.ui.Sort
|
||||||
import org.oxycblt.auxio.ui.SyncBackingData
|
import org.oxycblt.auxio.ui.SyncBackingData
|
||||||
import org.oxycblt.auxio.ui.newMenu
|
|
||||||
import org.oxycblt.auxio.util.formatDuration
|
import org.oxycblt.auxio.util.formatDuration
|
||||||
import org.oxycblt.auxio.util.launch
|
import org.oxycblt.auxio.util.launch
|
||||||
|
import org.oxycblt.auxio.util.logW
|
||||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -78,7 +78,10 @@ class GenreListFragment : HomeListFragment<Genre>() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOpenMenu(item: Item, anchor: View) {
|
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) :
|
class GenreAdapter(listener: MenuItemListener) :
|
||||||
|
|
|
@ -24,11 +24,9 @@ import androidx.fragment.app.activityViewModels
|
||||||
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
|
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
|
||||||
import org.oxycblt.auxio.home.HomeViewModel
|
import org.oxycblt.auxio.home.HomeViewModel
|
||||||
import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView
|
import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
|
||||||
import org.oxycblt.auxio.ui.Item
|
import org.oxycblt.auxio.ui.Item
|
||||||
|
import org.oxycblt.auxio.ui.MenuFragment
|
||||||
import org.oxycblt.auxio.ui.MenuItemListener
|
import org.oxycblt.auxio.ui.MenuItemListener
|
||||||
import org.oxycblt.auxio.ui.NavigationViewModel
|
|
||||||
import org.oxycblt.auxio.ui.ViewBindingFragment
|
|
||||||
import org.oxycblt.auxio.util.applySpans
|
import org.oxycblt.auxio.util.applySpans
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -36,12 +34,10 @@ import org.oxycblt.auxio.util.applySpans
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
abstract class HomeListFragment<T : Item> :
|
abstract class HomeListFragment<T : Item> :
|
||||||
ViewBindingFragment<FragmentHomeListBinding>(),
|
MenuFragment<FragmentHomeListBinding>(),
|
||||||
MenuItemListener,
|
MenuItemListener,
|
||||||
FastScrollRecyclerView.PopupProvider,
|
FastScrollRecyclerView.PopupProvider,
|
||||||
FastScrollRecyclerView.OnFastScrollListener {
|
FastScrollRecyclerView.OnFastScrollListener {
|
||||||
protected val playbackModel: PlaybackViewModel by activityViewModels()
|
|
||||||
protected val navModel: NavigationViewModel by activityViewModels()
|
|
||||||
protected val homeModel: HomeViewModel by activityViewModels()
|
protected val homeModel: HomeViewModel by activityViewModels()
|
||||||
|
|
||||||
override fun onCreateBinding(inflater: LayoutInflater) =
|
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.SongViewHolder
|
||||||
import org.oxycblt.auxio.ui.Sort
|
import org.oxycblt.auxio.ui.Sort
|
||||||
import org.oxycblt.auxio.ui.SyncBackingData
|
import org.oxycblt.auxio.ui.SyncBackingData
|
||||||
import org.oxycblt.auxio.ui.newMenu
|
|
||||||
import org.oxycblt.auxio.util.formatDuration
|
import org.oxycblt.auxio.util.formatDuration
|
||||||
import org.oxycblt.auxio.util.launch
|
import org.oxycblt.auxio.util.launch
|
||||||
|
import org.oxycblt.auxio.util.logW
|
||||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -85,7 +85,10 @@ class SongListFragment : HomeListFragment<Song>() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOpenMenu(item: Item, anchor: View) {
|
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) :
|
inner class SongsAdapter(listener: MenuItemListener) :
|
||||||
|
|
|
@ -27,7 +27,6 @@ import androidx.core.view.isInvisible
|
||||||
import androidx.core.view.postDelayed
|
import androidx.core.view.postDelayed
|
||||||
import androidx.core.widget.addTextChangedListener
|
import androidx.core.widget.addTextChangedListener
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.FragmentSearchBinding
|
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.Music
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
|
||||||
import org.oxycblt.auxio.ui.Header
|
import org.oxycblt.auxio.ui.Header
|
||||||
import org.oxycblt.auxio.ui.Item
|
import org.oxycblt.auxio.ui.Item
|
||||||
|
import org.oxycblt.auxio.ui.MenuFragment
|
||||||
import org.oxycblt.auxio.ui.MenuItemListener
|
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.androidViewModels
|
||||||
import org.oxycblt.auxio.util.applySpans
|
import org.oxycblt.auxio.util.applySpans
|
||||||
import org.oxycblt.auxio.util.getSystemServiceSafe
|
import org.oxycblt.auxio.util.getSystemServiceSafe
|
||||||
import org.oxycblt.auxio.util.launch
|
import org.oxycblt.auxio.util.launch
|
||||||
|
import org.oxycblt.auxio.util.logW
|
||||||
import org.oxycblt.auxio.util.requireAttached
|
import org.oxycblt.auxio.util.requireAttached
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -55,13 +52,9 @@ import org.oxycblt.auxio.util.requireAttached
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class SearchFragment :
|
class SearchFragment :
|
||||||
ViewBindingFragment<FragmentSearchBinding>(),
|
MenuFragment<FragmentSearchBinding>(), MenuItemListener, Toolbar.OnMenuItemClickListener {
|
||||||
MenuItemListener,
|
|
||||||
Toolbar.OnMenuItemClickListener {
|
|
||||||
// SearchViewModel is only scoped to this Fragment
|
// SearchViewModel is only scoped to this Fragment
|
||||||
private val searchModel: SearchViewModel by androidViewModels()
|
private val searchModel: SearchViewModel by androidViewModels()
|
||||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
|
||||||
private val navModel: NavigationViewModel by activityViewModels()
|
|
||||||
|
|
||||||
private val searchAdapter = SearchAdapter(this)
|
private val searchAdapter = SearchAdapter(this)
|
||||||
private var imm: InputMethodManager? = null
|
private var imm: InputMethodManager? = null
|
||||||
|
@ -137,7 +130,13 @@ class SearchFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOpenMenu(item: Item, anchor: View) {
|
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>) {
|
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? {
|
fun assignId(@IdRes id: Int): Sort? {
|
||||||
return when (id) {
|
return when (id) {
|
||||||
|
R.id.option_sort_asc -> ascending(!isAscending)
|
||||||
R.id.option_sort_name -> ByName(isAscending)
|
R.id.option_sort_name -> ByName(isAscending)
|
||||||
R.id.option_sort_artist -> ByArtist(isAscending)
|
R.id.option_sort_artist -> ByArtist(isAscending)
|
||||||
R.id.option_sort_album -> ByAlbum(isAscending)
|
R.id.option_sort_album -> ByAlbum(isAscending)
|
||||||
|
|
|
@ -9,4 +9,7 @@
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_go_artist"
|
android:id="@+id/action_go_artist"
|
||||||
android:title="@string/lbl_go_artist" />
|
android:title="@string/lbl_go_artist" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_song_detail"
|
||||||
|
android:title="@string/lbl_song_detail" />
|
||||||
</menu>
|
</menu>
|
|
@ -9,4 +9,7 @@
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_go_artist"
|
android:id="@+id/action_go_artist"
|
||||||
android:title="@string/lbl_go_artist" />
|
android:title="@string/lbl_go_artist" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_song_detail"
|
||||||
|
android:title="@string/lbl_song_detail" />
|
||||||
</menu>
|
</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
|
<item
|
||||||
android:id="@+id/action_go_album"
|
android:id="@+id/action_go_album"
|
||||||
android:title="@string/lbl_go_album" />
|
android:title="@string/lbl_go_album" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_song_detail"
|
||||||
|
android:title="@string/lbl_song_detail" />
|
||||||
</menu>
|
</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
|
<item
|
||||||
android:id="@+id/option_sort_duration"
|
android:id="@+id/option_sort_duration"
|
||||||
android:title="@string/lbl_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>
|
||||||
<group android:checkableBehavior="all">
|
<group android:checkableBehavior="all">
|
||||||
<item
|
<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.
|
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.
|
- 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!
|
#### 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.
|
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
|
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".
|
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!
|
#### ReplayGain isn't working on my music!
|
||||||
This is for a couple reason:
|
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
|
- 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
|
- Auxio doesn't recognize your ReplayGain tags. This is usually because of a non-standard tag like ID3v2's `RVAD` or
|
||||||
an unrecognized name.
|
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?
|
#### What is dynamic ReplayGain?
|
||||||
Dynamic ReplayGain is a quirk setting based off the FooBar2000 plugin that dynamically switches from track gain to album
|
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.
|
gain depending on if the current playback is from an album or not.
|
||||||
|
|
Loading…
Reference in a new issue