detail: re-add sorting

Re-add sorting to the detail fragments, now with the new system.
This commit is contained in:
OxygenCobalt 2021-09-25 18:12:42 -06:00
parent a820724ff2
commit 7a17282c30
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
10 changed files with 297 additions and 68 deletions

View file

@ -85,6 +85,14 @@ class AlbumDetailFragment : DetailFragment() {
detailAdapter.submitList(data)
}
detailModel.showMenu.observe(viewLifecycleOwner) { config ->
if (config != null) {
showMenu(config) { id ->
id == R.id.option_sort_asc || id == R.id.option_sort_dsc
}
}
}
detailModel.navToItem.observe(viewLifecycleOwner) { item ->
when (item) {
// Songs should be scrolled to if the album matches, or a new detail

View file

@ -24,6 +24,7 @@ import android.view.View
import android.view.ViewGroup
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import org.oxycblt.auxio.R
import org.oxycblt.auxio.detail.recycler.ArtistDetailAdapter
import org.oxycblt.auxio.music.ActionHeader
import org.oxycblt.auxio.music.Album
@ -85,6 +86,14 @@ class ArtistDetailFragment : DetailFragment() {
detailAdapter.submitList(data)
}
detailModel.showMenu.observe(viewLifecycleOwner) { config ->
if (config != null) {
showMenu(config) { id ->
id != R.id.option_sort_artist
}
}
}
detailModel.navToItem.observe(viewLifecycleOwner) { item ->
when (item) {
is Artist -> {

View file

@ -22,14 +22,18 @@ import android.os.Bundle
import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.annotation.MenuRes
import androidx.appcompat.widget.PopupMenu
import androidx.core.view.forEach
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentDetailBinding
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.SortMode
import org.oxycblt.auxio.ui.memberBinding
import org.oxycblt.auxio.util.applyEdge
import org.oxycblt.auxio.util.isLandscape
@ -63,6 +67,13 @@ abstract class DetailFragment : Fragment() {
callback.isEnabled = false
}
override fun onStop() {
super.onStop()
// Cancel all pending menus when this fragment stops to prevent bugs/crashes
detailModel.finishShowMenu(null, requireContext())
}
/**
* Shortcut method for doing setup of the detail toolbar.
* @param menu Menu resource to use
@ -113,6 +124,37 @@ abstract class DetailFragment : Fragment() {
}
}
/**
* Shortcut method for spinning up the sorting [PopupMenu]
* @param config The initial configuration to apply to the menu. This is provided by [DetailViewModel.showMenu].
* @param showItem Which menu items to keep
*/
protected fun showMenu(config: DetailViewModel.MenuConfig, showItem: ((Int) -> Boolean)? = null) {
PopupMenu(config.anchor.context, config.anchor).apply {
inflate(R.menu.menu_detail_sort)
setOnMenuItemClickListener { item ->
item.isChecked = true
detailModel.finishShowMenu(SortMode.fromId(item.itemId)!!, config.anchor.context)
true
}
setOnDismissListener {
detailModel.finishShowMenu(null, config.anchor.context)
}
if (showItem != null) {
menu.forEach { item ->
item.isVisible = showItem(item.itemId)
}
}
menu.findItem(config.sortMode.itemId).isChecked = true
show()
}
}
// Override the back button so that going back will only exit the detail fragments instead of
// the entire app.
private val callback = object : OnBackPressedCallback(false) {

View file

@ -19,6 +19,7 @@
package org.oxycblt.auxio.detail
import android.content.Context
import android.view.View
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
@ -30,12 +31,16 @@ import org.oxycblt.auxio.music.BaseModel
import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Header
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.settings.SettingsManager
import org.oxycblt.auxio.ui.DisplayMode
import org.oxycblt.auxio.ui.SortMode
/**
* ViewModel that stores data for the [DetailFragment]s, such as what they're showing & what
* [SortMode] they are currently on.
* TODO: Re-add sorting
* ViewModel that stores data for the [DetailFragment]s. This includes:
* - What item the fragment should be showing
* - The RecyclerView data for each fragment
* - Menu triggers for each fragment
* - Navigation triggers for each fragment [e.g "Go to artist"]
* @author OxygenCobalt
*/
class DetailViewModel : ViewModel() {
@ -59,93 +64,67 @@ class DetailViewModel : ViewModel() {
private val mAlbumData = MutableLiveData(listOf<BaseModel>())
val albumData: LiveData<List<BaseModel>> get() = mAlbumData
var isNavigating = false
private set
data class MenuConfig(val anchor: View, val sortMode: SortMode)
private val mShowMenu = MutableLiveData<MenuConfig?>(null)
val showMenu: LiveData<MenuConfig?> = mShowMenu
private val mNavToItem = MutableLiveData<BaseModel?>()
/** Flag for unified navigation. Observe this to coordinate navigation to an item's UI. */
val navToItem: LiveData<BaseModel?> get() = mNavToItem
var isNavigating = false
private set
private var currentMenuContext: DisplayMode? = null
private val musicStore = MusicStore.getInstance()
private val settingsManager = SettingsManager.getInstance()
fun setGenre(id: Long, context: Context) {
if (mCurGenre.value?.id == id) return
mCurGenre.value = musicStore.genres.find { it.id == id }
val data = mutableListOf<BaseModel>(curGenre.value!!)
data.add(
ActionHeader(
id = -2,
name = context.getString(R.string.lbl_songs),
icon = R.drawable.ic_sort,
desc = R.string.lbl_sort,
onClick = {
}
)
)
data.addAll(SortMode.ASCENDING.sortGenre(curGenre.value!!))
mGenreData.value = data
refreshGenreData(context)
}
fun setArtist(id: Long, context: Context) {
if (mCurArtist.value?.id == id) return
mCurArtist.value = musicStore.artists.find { it.id == id }
val artist = curArtist.value!!
val data = mutableListOf<BaseModel>(artist)
data.add(
Header(
id = -2,
name = context.getString(R.string.lbl_albums)
)
)
data.addAll(SortMode.YEAR.sortAlbums(artist.albums))
data.add(
ActionHeader(
id = -3,
name = context.getString(R.string.lbl_songs),
icon = R.drawable.ic_sort,
desc = R.string.lbl_sort,
onClick = {
}
)
)
data.addAll(SortMode.YEAR.sortArtist(artist))
mArtistData.value = data.toList()
refreshArtistData(context)
}
fun setAlbum(id: Long, context: Context) {
if (mCurAlbum.value?.id == id) return
mCurAlbum.value = musicStore.albums.find { it.id == id }
refreshAlbumData(context)
}
val data = mutableListOf<BaseModel>(curAlbum.value!!)
/**
* Mark that the menu process is done with the new [SortMode].
* Pass null if there was no change.
*/
fun finishShowMenu(newMode: SortMode?, context: Context) {
mShowMenu.value = null
data.add(
ActionHeader(
id = -2,
name = context.getString(R.string.lbl_songs),
icon = R.drawable.ic_sort,
desc = R.string.lbl_sort,
onClick = {
if (newMode != null) {
when (currentMenuContext) {
DisplayMode.SHOW_ALBUMS -> {
settingsManager.albumSortMode = newMode
refreshAlbumData(context)
}
)
)
DisplayMode.SHOW_ARTISTS -> {
settingsManager.artistSortMode = newMode
refreshArtistData(context)
}
DisplayMode.SHOW_GENRES -> {
settingsManager.genreSortMode = newMode
refreshGenreData(context)
}
else -> {}
}
}
data.addAll(SortMode.ASCENDING.sortAlbum(curAlbum.value!!))
mAlbumData.value = data
currentMenuContext = null
}
/**
@ -168,4 +147,77 @@ class DetailViewModel : ViewModel() {
fun setNavigating(navigating: Boolean) {
isNavigating = navigating
}
private fun refreshGenreData(context: Context) {
val data = mutableListOf<BaseModel>(curGenre.value!!)
data.add(
ActionHeader(
id = -2,
name = context.getString(R.string.lbl_songs),
icon = R.drawable.ic_sort,
desc = R.string.lbl_sort,
onClick = { view ->
currentMenuContext = DisplayMode.SHOW_GENRES
mShowMenu.value = MenuConfig(view, settingsManager.genreSortMode)
}
)
)
data.addAll(settingsManager.genreSortMode.sortGenre(curGenre.value!!))
mGenreData.value = data
}
private fun refreshArtistData(context: Context) {
val artist = curArtist.value!!
val data = mutableListOf<BaseModel>(artist)
data.add(
Header(
id = -2,
name = context.getString(R.string.lbl_albums)
)
)
data.addAll(SortMode.YEAR.sortAlbums(artist.albums))
data.add(
ActionHeader(
id = -3,
name = context.getString(R.string.lbl_songs),
icon = R.drawable.ic_sort,
desc = R.string.lbl_sort,
onClick = { view ->
currentMenuContext = DisplayMode.SHOW_ARTISTS
mShowMenu.value = MenuConfig(view, settingsManager.artistSortMode)
}
)
)
data.addAll(settingsManager.artistSortMode.sortArtist(artist))
mArtistData.value = data.toList()
}
private fun refreshAlbumData(context: Context) {
val data = mutableListOf<BaseModel>(curAlbum.value!!)
data.add(
ActionHeader(
id = -2,
name = context.getString(R.string.lbl_songs),
icon = R.drawable.ic_sort,
desc = R.string.lbl_sort,
onClick = { view ->
currentMenuContext = DisplayMode.SHOW_ALBUMS
mShowMenu.value = MenuConfig(view, settingsManager.albumSortMode)
}
)
)
data.addAll(settingsManager.albumSortMode.sortAlbum(curAlbum.value!!))
mAlbumData.value = data
}
}

View file

@ -109,6 +109,12 @@ class GenreDetailFragment : DetailFragment() {
}
}
detailModel.showMenu.observe(viewLifecycleOwner) { config ->
if (config != null) {
showMenu(config)
}
}
playbackModel.isInUserQueue.observe(viewLifecycleOwner) { inUserQueue ->
if (inUserQueue) {
detailAdapter.highlightSong(null, binding.detailRecycler)

View file

@ -237,5 +237,28 @@ data class ActionHeader(
override val name: String,
@DrawableRes val icon: Int,
@StringRes val desc: Int,
val onClick: (View) -> Unit
) : BaseModel()
val onClick: (View) -> Unit,
) : BaseModel() {
// JVM can't into comparing lambdas, so we override equals/hashCode and exclude them.
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is ActionHeader) return false
if (id != other.id) return false
if (name != other.name) return false
if (icon != other.icon) return false
if (desc != other.desc) return false
return true
}
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + name.hashCode()
result = 31 * result + icon
result = 31 * result + desc
return result
}
}

View file

@ -26,6 +26,7 @@ import org.oxycblt.auxio.accent.ACCENTS
import org.oxycblt.auxio.accent.Accent
import org.oxycblt.auxio.playback.state.PlaybackMode
import org.oxycblt.auxio.ui.DisplayMode
import org.oxycblt.auxio.ui.SortMode
/**
* Wrapper around the [SharedPreferences] class that writes & reads values without a context.
@ -118,6 +119,36 @@ class SettingsManager private constructor(context: Context) :
}
}
var albumSortMode: SortMode
get() = SortMode.fromInt(sharedPrefs.getInt(KEY_ALBUM_SORT, Int.MIN_VALUE))
?: SortMode.ASCENDING
set(value) {
sharedPrefs.edit {
putInt(KEY_ALBUM_SORT, value.toInt())
apply()
}
}
var artistSortMode: SortMode
get() = SortMode.fromInt(sharedPrefs.getInt(KEY_ARTIST_SORT, Int.MIN_VALUE))
?: SortMode.YEAR
set(value) {
sharedPrefs.edit {
putInt(KEY_ARTIST_SORT, value.toInt())
apply()
}
}
var genreSortMode: SortMode
get() = SortMode.fromInt(sharedPrefs.getInt(KEY_GENRE_SORT, Int.MIN_VALUE))
?: SortMode.ASCENDING
set(value) {
sharedPrefs.edit {
putInt(KEY_GENRE_SORT, value.toInt())
apply()
}
}
// --- CALLBACKS ---
private val callbacks = mutableListOf<Callback>()
@ -181,6 +212,9 @@ class SettingsManager private constructor(context: Context) :
const val KEY_BLACKLIST = "KEY_BLACKLIST"
const val KEY_SEARCH_FILTER_MODE = "KEY_SEARCH_FILTER"
const val KEY_ALBUM_SORT = "KEY_ALBUM_SORT"
const val KEY_ARTIST_SORT = "KEY_ARTIST_SORT"
const val KEY_GENRE_SORT = "KEY_GENRE_SORT"
@Volatile
private var INSTANCE: SettingsManager? = null

View file

@ -147,7 +147,36 @@ enum class SortMode(@IdRes val itemId: Int) {
return sortSongs(genre.songs)
}
/**
* Converts this mode into an integer constant. Use this when writing a [SortMode]
* to storage, as it will be more efficent.
*/
fun toInt(): Int {
return ordinal + INT_ASCENDING
}
companion object {
private const val INT_ASCENDING = 0xA10C
private const val INT_DESCENDING = 0xA10D
private const val INT_ARTIST = 0xA10E
private const val INT_ALBUM = 0xA10F
private const val INT_YEAR = 0xA110
/**
* Returns a [SortMode] depending on the integer constant, use this when restoring
* a [SortMode] from storage.
*/
fun fromInt(value: Int): SortMode? {
return when (value) {
INT_ASCENDING -> ASCENDING
INT_DESCENDING -> DESCENDING
INT_ARTIST -> ASCENDING
INT_ALBUM -> ALBUM
INT_YEAR -> YEAR
else -> null
}
}
/**
* Convert a menu [id] to an instance of [SortMode].
*/

View file

@ -0,0 +1,20 @@
<?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_asc"
android:title="@string/lbl_sort_asc" />
<item
android:id="@+id/option_sort_dsc"
android:title="@string/lbl_sort_dsc" />
<item
android:id="@+id/option_sort_artist"
android:title="@string/lbl_sort_artist" />
<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" />
</group>
</menu>

View file

@ -76,6 +76,12 @@ To prevent any strange bugs, all integer representations must be unique. A table
0xA109 | DisplayMode.SHOW_ARTISTS
0xA10A | DisplayMode.SHOW_ALBUMS
0xA10B | DisplayMode.SHOW_SONGS
0xA10C | SortMode.ASCENDING
0xA10D | SortMode.DESCENDING
0xA10E | SortMode.ARTIST
0xA10F | SortMode.ALBUM
0xA110 | SortMode.YEAR
```
#### Package structure overview