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) 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 -> detailModel.navToItem.observe(viewLifecycleOwner) { item ->
when (item) { when (item) {
// Songs should be scrolled to if the album matches, or a new detail // 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 android.view.ViewGroup
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.detail.recycler.ArtistDetailAdapter import org.oxycblt.auxio.detail.recycler.ArtistDetailAdapter
import org.oxycblt.auxio.music.ActionHeader import org.oxycblt.auxio.music.ActionHeader
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
@ -85,6 +86,14 @@ class ArtistDetailFragment : DetailFragment() {
detailAdapter.submitList(data) 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 -> detailModel.navToItem.observe(viewLifecycleOwner) { item ->
when (item) { when (item) {
is Artist -> { is Artist -> {

View file

@ -22,14 +22,18 @@ import android.os.Bundle
import android.view.View import android.view.View
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.annotation.MenuRes import androidx.annotation.MenuRes
import androidx.appcompat.widget.PopupMenu
import androidx.core.view.forEach
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentDetailBinding import org.oxycblt.auxio.databinding.FragmentDetailBinding
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.SortMode
import org.oxycblt.auxio.ui.memberBinding import org.oxycblt.auxio.ui.memberBinding
import org.oxycblt.auxio.util.applyEdge import org.oxycblt.auxio.util.applyEdge
import org.oxycblt.auxio.util.isLandscape import org.oxycblt.auxio.util.isLandscape
@ -63,6 +67,13 @@ abstract class DetailFragment : Fragment() {
callback.isEnabled = false 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. * Shortcut method for doing setup of the detail toolbar.
* @param menu Menu resource to use * @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 // Override the back button so that going back will only exit the detail fragments instead of
// the entire app. // the entire app.
private val callback = object : OnBackPressedCallback(false) { private val callback = object : OnBackPressedCallback(false) {

View file

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

View file

@ -237,5 +237,28 @@ data class ActionHeader(
override val name: String, override val name: String,
@DrawableRes val icon: Int, @DrawableRes val icon: Int,
@StringRes val desc: Int, @StringRes val desc: Int,
val onClick: (View) -> Unit val onClick: (View) -> Unit,
) : BaseModel() ) : 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.accent.Accent
import org.oxycblt.auxio.playback.state.PlaybackMode import org.oxycblt.auxio.playback.state.PlaybackMode
import org.oxycblt.auxio.ui.DisplayMode import org.oxycblt.auxio.ui.DisplayMode
import org.oxycblt.auxio.ui.SortMode
/** /**
* Wrapper around the [SharedPreferences] class that writes & reads values without a context. * 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 --- // --- CALLBACKS ---
private val callbacks = mutableListOf<Callback>() private val callbacks = mutableListOf<Callback>()
@ -181,6 +212,9 @@ class SettingsManager private constructor(context: Context) :
const val KEY_BLACKLIST = "KEY_BLACKLIST" const val KEY_BLACKLIST = "KEY_BLACKLIST"
const val KEY_SEARCH_FILTER_MODE = "KEY_SEARCH_FILTER" 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 @Volatile
private var INSTANCE: SettingsManager? = null private var INSTANCE: SettingsManager? = null

View file

@ -147,7 +147,36 @@ enum class SortMode(@IdRes val itemId: Int) {
return sortSongs(genre.songs) 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 { 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]. * 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 0xA109 | DisplayMode.SHOW_ARTISTS
0xA10A | DisplayMode.SHOW_ALBUMS 0xA10A | DisplayMode.SHOW_ALBUMS
0xA10B | DisplayMode.SHOW_SONGS 0xA10B | DisplayMode.SHOW_SONGS
0xA10C | SortMode.ASCENDING
0xA10D | SortMode.DESCENDING
0xA10E | SortMode.ARTIST
0xA10F | SortMode.ALBUM
0xA110 | SortMode.YEAR
``` ```
#### Package structure overview #### Package structure overview