detail: re-add sorting
Re-add sorting to the detail fragments, now with the new system.
This commit is contained in:
parent
a820724ff2
commit
7a17282c30
10 changed files with 297 additions and 68 deletions
|
@ -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
|
||||
|
|
|
@ -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 -> {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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].
|
||||
*/
|
||||
|
|
20
app/src/main/res/menu/menu_detail_sort.xml
Normal file
20
app/src/main/res/menu/menu_detail_sort.xml
Normal 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>
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue