Add sorting to LibraryFragment

Add basic sorting to LibraryFragment.
This commit is contained in:
OxygenCobalt 2020-09-28 15:10:14 -06:00
parent aad42b5201
commit 37c52d9e5c
17 changed files with 227 additions and 174 deletions

View file

@ -5,7 +5,6 @@ TODO:
/detail/
- Add genre detail
- ? Implement Toolbar update functionality ?
- ! Implement shared element transitions !
@ -24,11 +23,14 @@ TODO:
/library/
- ? Move into ViewPager ?
- Sorting
- Re-add albums/genres into a single adapter
- Add highlighting to the current sortmode
- Search
- ? Show Artists, Albums, and Songs in search ?
- Exit functionality
- ? Show Artists, Albums, and Songs in search ?
- ? Move into ViewPager ?
- ! Move Adapter functionality to ListAdapter!
To be added:
/prefs/

View file

@ -15,7 +15,6 @@ import org.oxycblt.auxio.databinding.FragmentAlbumDetailBinding
import org.oxycblt.auxio.detail.adapters.DetailSongAdapter
import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.recycler.ClickListener
import org.oxycblt.auxio.recycler.SortMode
import org.oxycblt.auxio.theme.applyDivider
import org.oxycblt.auxio.theme.toColor
@ -65,7 +64,7 @@ class AlbumDetailFragment : Fragment() {
// If the album was shown directly from LibraryFragment, Then enable the ability to
// navigate upwards to the parent artist
if (args.fromLibrary) {
if (args.enableParentNav) {
detailModel.navToParent.observe(viewLifecycleOwner) {
if (it) {
findNavController().navigate(
@ -89,14 +88,7 @@ class AlbumDetailFragment : Fragment() {
// Then update the sort mode of the album adapter.
songAdapter.submitList(
detailModel.currentAlbum.value!!.songs.sortedWith(
SortMode.songSortComparators.getOrDefault(
mode,
// If any invalid value is given, just default to the normal sort order.
compareByDescending { it.track }
)
)
mode.getSortedSongList(detailModel.currentAlbum.value!!.songs)
)
}

View file

@ -16,7 +16,6 @@ import org.oxycblt.auxio.detail.adapters.DetailAlbumAdapter
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.recycler.ClickListener
import org.oxycblt.auxio.recycler.SortMode
import org.oxycblt.auxio.theme.applyDivider
import org.oxycblt.auxio.theme.toColor
@ -72,14 +71,7 @@ class ArtistDetailFragment : Fragment() {
// Then update the sort mode of the album adapter.
albumAdapter.submitList(
detailModel.currentArtist.value!!.albums.sortedWith(
SortMode.albumSortComparators.getOrDefault(
mode,
// If any invalid value is given, just default to the normal sort order.
compareByDescending { it.year }
)
)
mode.getSortedAlbumList(detailModel.currentArtist.value!!.albums)
)
}

View file

@ -16,7 +16,6 @@ import org.oxycblt.auxio.detail.adapters.DetailArtistAdapter
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.recycler.ClickListener
import org.oxycblt.auxio.recycler.SortMode
import org.oxycblt.auxio.theme.applyDivider
import org.oxycblt.auxio.theme.toColor
@ -70,14 +69,7 @@ class GenreDetailFragment : Fragment() {
// Then update the sort mode of the artist adapter.
albumAdapter.submitList(
detailModel.currentGenre.value!!.artists.sortedWith(
SortMode.artistSortComparators.getOrDefault(
mode,
// If any invalid value is given, just default to the normal sort order.
compareByDescending { it.name }
)
)
mode.getSortedArtistList(detailModel.currentGenre.value!!.artists)
)
}

View file

@ -5,29 +5,24 @@ import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import org.oxycblt.auxio.MainFragmentDirections
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentLibraryBinding
import org.oxycblt.auxio.library.adapters.AlbumAdapter
import org.oxycblt.auxio.library.adapters.ArtistAdapter
import org.oxycblt.auxio.library.adapters.GenreAdapter
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.BaseModel
import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.recycler.ClickListener
import org.oxycblt.auxio.theme.SHOW_ALBUMS
import org.oxycblt.auxio.theme.SHOW_ARTISTS
import org.oxycblt.auxio.theme.SHOW_GENRES
import org.oxycblt.auxio.theme.applyDivider
class LibraryFragment : Fragment() {
// FIXME: Temp value, remove when there are actual preferences
private val libraryMode = SHOW_ARTISTS
private val musicModel: MusicViewModel by activityViewModels()
private val libraryModel: LibraryViewModel by activityViewModels()
@ -38,34 +33,34 @@ class LibraryFragment : Fragment() {
): View? {
val binding = FragmentLibraryBinding.inflate(inflater)
binding.libraryRecycler.adapter = when (libraryMode) {
SHOW_ARTISTS -> ArtistAdapter(
musicModel.artists.value!!,
ClickListener {
navToArtist(it)
}
val artistAdapter = ArtistAdapter(
ClickListener { navToItem(it) }
)
SHOW_ALBUMS -> AlbumAdapter(
musicModel.albums.value!!,
ClickListener {
navToAlbum(it)
}
)
SHOW_GENRES -> GenreAdapter(
musicModel.genres.value!!,
ClickListener {
navToGenre(it)
}
)
else -> null
}
binding.libraryRecycler.adapter = artistAdapter
binding.libraryRecycler.applyDivider()
binding.libraryRecycler.setHasFixedSize(true)
libraryModel.sortMode.observe(viewLifecycleOwner) { mode ->
binding.libraryToolbar.overflowIcon = ContextCompat.getDrawable(
requireContext(), mode.iconRes
)
artistAdapter.updateData(
mode.getSortedArtistList(
musicModel.artists.value!!
)
)
}
binding.libraryToolbar.setOnMenuItemClickListener {
libraryModel.updateSortMode(it)
true
}
binding.libraryToolbar.inflateMenu(R.menu.menu_library)
Log.d(this::class.simpleName, "Fragment created.")
return binding.root
@ -77,39 +72,19 @@ class LibraryFragment : Fragment() {
libraryModel.isAlreadyNavigating = false
}
private fun navToArtist(artist: Artist) {
// Dont navigate if an item has already been selected
private fun navToItem(baseModel: BaseModel) {
// Don't navigate if an item has already been selected
if (!libraryModel.isAlreadyNavigating) {
libraryModel.isAlreadyNavigating = true
findNavController().navigate(
MainFragmentDirections.actionShowArtist(
artist.id
)
)
}
}
when (baseModel) {
is Genre -> MainFragmentDirections.actionShowGenre(baseModel.id)
is Artist -> MainFragmentDirections.actionShowArtist(baseModel.id)
is Album -> MainFragmentDirections.actionShowAlbum(baseModel.id, true)
private fun navToAlbum(album: Album) {
if (!libraryModel.isAlreadyNavigating) {
libraryModel.isAlreadyNavigating = true
findNavController().navigate(
MainFragmentDirections.actionShowAlbum(
album.id, true
)
)
else -> return
}
}
private fun navToGenre(genre: Genre) {
if (!libraryModel.isAlreadyNavigating) {
libraryModel.isAlreadyNavigating = true
findNavController().navigate(
MainFragmentDirections.actionShowGenre(
genre.id
)
)
}
}

View file

@ -1,7 +1,34 @@
package org.oxycblt.auxio.library
import android.view.MenuItem
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import org.oxycblt.auxio.R
import org.oxycblt.auxio.recycler.SortMode
import org.oxycblt.auxio.theme.SHOW_ARTISTS
class LibraryViewModel : ViewModel() {
var isAlreadyNavigating = false
// TODO: Move these to pref values when they're added
private val mShowMode = MutableLiveData(SHOW_ARTISTS)
val showMode: LiveData<Int> get() = mShowMode
private val mSortMode = MutableLiveData(SortMode.ALPHA_UP)
val sortMode: LiveData<SortMode> get() = mSortMode
fun updateSortMode(item: MenuItem) {
val mode = when (item.itemId) {
R.id.sort_none -> SortMode.NONE
R.id.sort_alpha_down -> SortMode.ALPHA_DOWN
R.id.sort_alpha_up -> SortMode.ALPHA_UP
else -> SortMode.NONE
}
if (mode != mSortMode.value) {
mSortMode.value = mode
}
}
}

View file

@ -2,18 +2,16 @@ package org.oxycblt.auxio.library.adapters
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.ListAdapter
import org.oxycblt.auxio.databinding.ItemAlbumBinding
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.recycler.BaseViewHolder
import org.oxycblt.auxio.recycler.ClickListener
import org.oxycblt.auxio.recycler.DiffCallback
class AlbumAdapter(
private val data: List<Album>,
private val listener: ClickListener<Album>
) : RecyclerView.Adapter<AlbumAdapter.ViewHolder>() {
override fun getItemCount(): Int = data.size
) : ListAdapter<Album, AlbumAdapter.ViewHolder>(DiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
@ -22,7 +20,7 @@ class AlbumAdapter(
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(data[position])
holder.bind(getItem(position))
}
inner class ViewHolder(

View file

@ -9,10 +9,11 @@ import org.oxycblt.auxio.recycler.BaseViewHolder
import org.oxycblt.auxio.recycler.ClickListener
class ArtistAdapter(
private val data: List<Artist>,
private val listener: ClickListener<Artist>
) : RecyclerView.Adapter<ArtistAdapter.ViewHolder>() {
private var data = listOf<Artist>()
override fun getItemCount(): Int = data.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
@ -25,6 +26,12 @@ class ArtistAdapter(
holder.bind(data[position])
}
fun updateData(newData: List<Artist>) {
data = newData
notifyDataSetChanged()
}
inner class ViewHolder(
private val binding: ItemArtistBinding
) : BaseViewHolder<Artist>(binding, listener) {

View file

@ -2,18 +2,16 @@ package org.oxycblt.auxio.library.adapters
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.ListAdapter
import org.oxycblt.auxio.databinding.ItemGenreBinding
import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.recycler.BaseViewHolder
import org.oxycblt.auxio.recycler.ClickListener
import org.oxycblt.auxio.recycler.DiffCallback
class GenreAdapter(
private val data: List<Genre>,
private val listener: ClickListener<Genre>
) : RecyclerView.Adapter<GenreAdapter.ViewHolder>() {
override fun getItemCount(): Int = data.size
) : ListAdapter<Genre, GenreAdapter.ViewHolder>(DiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
@ -22,7 +20,7 @@ class GenreAdapter(
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(data[position])
holder.bind(getItem(position))
}
inner class ViewHolder(

View file

@ -21,7 +21,11 @@ class MusicSorter(
sortAlbumsIntoArtists()
sortArtistsIntoGenres()
finalizeMusic()
// Remove genre duplicates at the end, as duplicate genres can be added during
// the sorting process as well.
genres = genres.distinctBy {
it.name
}.toMutableList()
}
private fun sortSongsIntoAlbums() {
@ -155,25 +159,4 @@ class MusicSorter(
)
}
}
// Finalize music
private fun finalizeMusic() {
// Remove genre duplicates now, as duplicate genres can be added during the sorting process.
genres = genres.distinctBy {
it.name
}.toMutableList()
// Then finally sort the music
genres.sortWith(
compareBy(String.CASE_INSENSITIVE_ORDER, { it.name })
)
artists.sortWith(
compareBy(String.CASE_INSENSITIVE_ORDER, { it.name })
)
albums.sortWith(
compareBy(String.CASE_INSENSITIVE_ORDER, { it.name })
)
}
}

View file

@ -3,11 +3,7 @@ package org.oxycblt.auxio.recycler
import androidx.databinding.ViewDataBinding
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.BaseModel
import org.oxycblt.auxio.music.Song
// RecyclerView click listener
class ClickListener<T>(val onClick: (T) -> Unit)
@ -44,44 +40,3 @@ abstract class BaseViewHolder<T : BaseModel>(
abstract fun onBind(model: T)
}
// Sorting modes
enum class SortMode(val iconRes: Int) {
// Icons for each mode are assigned to the enums themselves
NONE(R.drawable.ic_sort_alpha_down),
ALPHA_UP(R.drawable.ic_sort_alpha_up),
ALPHA_DOWN(R.drawable.ic_sort_alpha_down),
NUMERIC_UP(R.drawable.ic_sort_numeric_up),
NUMERIC_DOWN(R.drawable.ic_sort_numeric_down);
companion object {
// Sort comparators are different for each music model, so they are static maps instead.
val songSortComparators = mapOf<SortMode, Comparator<Song>>(
NUMERIC_DOWN to compareBy { it.track },
NUMERIC_UP to compareByDescending { it.track }
)
val albumSortComparators = mapOf<SortMode, Comparator<Album>>(
NUMERIC_DOWN to compareByDescending { it.year },
NUMERIC_UP to compareBy { it.year },
// Alphabetic sorting needs to be case-insensitive
ALPHA_DOWN to compareByDescending(
String.CASE_INSENSITIVE_ORDER
) { it.name },
ALPHA_UP to compareBy(
String.CASE_INSENSITIVE_ORDER
) { it.name }
)
val artistSortComparators = mapOf<SortMode, Comparator<Artist>>(
// Alphabetic sorting needs to be case-insensitive
ALPHA_DOWN to compareBy(
String.CASE_INSENSITIVE_ORDER
) { it.name },
ALPHA_UP to compareByDescending(
String.CASE_INSENSITIVE_ORDER
) { it.name }
)
}
}

View file

@ -0,0 +1,89 @@
package org.oxycblt.auxio.recycler
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
// Sorting modes
enum class SortMode(val iconRes: Int) {
// Icons for each mode are assigned to the enums themselves
NONE(R.drawable.ic_sort_none),
ALPHA_UP(R.drawable.ic_sort_alpha_up),
ALPHA_DOWN(R.drawable.ic_sort_alpha_down),
NUMERIC_UP(R.drawable.ic_sort_numeric_up),
NUMERIC_DOWN(R.drawable.ic_sort_numeric_down);
fun getSortedGenreList(list: List<Genre>): List<Genre> {
return when (this) {
ALPHA_UP -> list.sortedWith(
compareByDescending(String.CASE_INSENSITIVE_ORDER) { it.name }
)
ALPHA_DOWN -> list.sortedWith(
compareBy(String.CASE_INSENSITIVE_ORDER) { it.name }
)
else -> list
}
}
fun getSortedArtistList(list: List<Artist>): List<Artist> {
return when (this) {
ALPHA_UP -> list.sortedWith(
compareByDescending(String.CASE_INSENSITIVE_ORDER) { it.name }
)
ALPHA_DOWN -> list.sortedWith(
compareBy(String.CASE_INSENSITIVE_ORDER) { it.name }
)
else -> list
}
}
fun getSortedAlbumList(list: List<Album>): List<Album> {
return when (this) {
ALPHA_UP -> list.sortedWith(
compareByDescending(String.CASE_INSENSITIVE_ORDER) { it.name }
)
ALPHA_DOWN -> list.sortedWith(
compareBy(String.CASE_INSENSITIVE_ORDER) { it.name }
)
NUMERIC_UP -> list.sortedBy { it.year }
NUMERIC_DOWN -> list.sortedByDescending { it.year }
else -> list
}
}
fun getSortedSongList(list: List<Song>): List<Song> {
return when (this) {
ALPHA_UP -> list.sortedWith(
compareByDescending(String.CASE_INSENSITIVE_ORDER) { it.name }
)
ALPHA_DOWN -> list.sortedWith(
compareBy(String.CASE_INSENSITIVE_ORDER) { it.name }
)
NUMERIC_UP -> list.sortedWith(compareByDescending { it.track })
NUMERIC_DOWN -> list.sortedWith(compareBy { it.track })
else -> list
}
}
fun toMenuId(): Int {
return when (this) {
NONE -> R.id.sort_none
ALPHA_UP -> R.id.sort_alpha_up
ALPHA_DOWN -> R.id.sort_alpha_down
else -> R.id.sort_none
}
}
}

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorPrimary">
<path
android:fillColor="@android:color/white"
android:pathData="M3,18h6v-2L3,16v2zM3,6v2h18L21,6L3,6zM3,13h12v-2L3,11v2z"/>
</vector>

View file

@ -7,7 +7,8 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:animateLayoutChanges="true">
android:animateLayoutChanges="true"
android:descendantFocusability="blocksDescendants">
<androidx.appcompat.widget.Toolbar
android:id="@+id/library_toolbar"

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/sort_none"
android:icon="@drawable/ic_sort_none"
android:title="@string/label_sort_none"
android:contentDescription="@string/description_sort_none"/>
<item
android:id="@+id/sort_alpha_down"
android:icon="@drawable/ic_sort_alpha_down"
android:title="@string/label_sort_alpha_down"
android:contentDescription="@string/description_sort_alpha_down"/>
<item
android:id="@+id/sort_alpha_up"
android:icon="@drawable/ic_sort_alpha_up"
android:title="@string/label_sort_alpha_up"
android:contentDescription="@string/description_sort_alpha_up"/>
</menu>

View file

@ -73,7 +73,7 @@
android:name="albumId"
app:argType="long" />
<argument
android:name="fromLibrary"
android:name="enableParentNav"
app:argType="boolean" />
<action
android:id="@+id/action_show_parent_artist"

View file

@ -2,35 +2,48 @@
<resources>
<string name="app_name">Auxio</string>
<!-- Title Namespace | Toolbar titles -->
<string name="title_library_fragment">Library</string>
<string name="title_all_songs">All Songs</string>
<!-- Error Namespace | Error Labels -->
<string name="error_no_music">No music found.</string>
<string name="error_music_load_failed">Music loading failed.</string>
<string name="error_no_perms">Permissions to read storage are needed.</string>
<!-- Label Namespace | Static Labels -->
<string name="label_retry">Retry</string>
<string name="label_grant">Grant</string>
<string name="label_artists">Artists</string>
<string name="label_albums">Albums</string>
<string name="label_songs">Songs</string>
<string name="label_sort_none">Default</string>
<string name="label_sort_alpha_down">A-Z</string>
<string name="label_sort_alpha_up">Z-A</string>
<!-- Description Namespace | Accessibility Strings -->
<string name="description_album_cover">Album Cover for %s</string>
<string name="description_artist_image">Artist Image for %s</string>
<string name="description_genre_image">Genre Image for %s</string>
<string name="description_track_number">Track %s</string>
<string name="description_error">Error</string>
<string name="description_sort_button">Change Sorting Mode</string>
<string name="description_sort_none">Default Sort Order</string>
<string name="description_sort_alpha_down">Sort from A to Z</string>
<string name="description_sort_alpha_up">Sort from Z to A</string>
<!-- Placeholder Namespace | Placeholder values -->
<string name="placeholder_genre">Unknown Genre</string>
<string name="placeholder_artist">Unknown Artist</string>
<string name="placeholder_album">Unknown Album</string>
<string name="placeholder_no_date">No Date</string>
<!-- Format Namespace | Value formatting -->
<string name="format_info">%1$s / %2$s</string>
<string name="format_double_info">%1$s / %2$s / %3$s</string>
<string name="format_double_counts">%1$s, %2$s</string>
<!-- Format Namespace | Value formatting with plurals -->
<plurals name="format_song_count">
<item quantity="one">%s Song</item>
<item quantity="other">%s Songs</item>