diff --git a/AuxioTODO b/AuxioTODO index 5e29058bb..8de2461d2 100644 --- a/AuxioTODO +++ b/AuxioTODO @@ -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/ diff --git a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt index 5ac7968d8..bcd4c649c 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt @@ -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) ) } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt index 05357f4c2..a6efc4377 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt @@ -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) ) } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt index 7b8793ad0..2558c9f1e 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt @@ -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) ) } diff --git a/app/src/main/java/org/oxycblt/auxio/library/LibraryFragment.kt b/app/src/main/java/org/oxycblt/auxio/library/LibraryFragment.kt index 0af43f242..21ed41fd0 100644 --- a/app/src/main/java/org/oxycblt/auxio/library/LibraryFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/library/LibraryFragment.kt @@ -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) - } - ) - - SHOW_ALBUMS -> AlbumAdapter( - musicModel.albums.value!!, - ClickListener { - navToAlbum(it) - } - ) - - SHOW_GENRES -> GenreAdapter( - musicModel.genres.value!!, - ClickListener { - navToGenre(it) - } - ) - - else -> null - } + val artistAdapter = ArtistAdapter( + ClickListener { navToItem(it) } + ) + 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 - ) - ) - } - } - - private fun navToGenre(genre: Genre) { - if (!libraryModel.isAlreadyNavigating) { - libraryModel.isAlreadyNavigating = true - - findNavController().navigate( - MainFragmentDirections.actionShowGenre( - genre.id - ) + else -> return + } ) } } diff --git a/app/src/main/java/org/oxycblt/auxio/library/LibraryViewModel.kt b/app/src/main/java/org/oxycblt/auxio/library/LibraryViewModel.kt index d23605d04..fc4933552 100644 --- a/app/src/main/java/org/oxycblt/auxio/library/LibraryViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/library/LibraryViewModel.kt @@ -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 get() = mShowMode + + private val mSortMode = MutableLiveData(SortMode.ALPHA_UP) + val sortMode: LiveData 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 + } + } } diff --git a/app/src/main/java/org/oxycblt/auxio/library/adapters/AlbumAdapter.kt b/app/src/main/java/org/oxycblt/auxio/library/adapters/AlbumAdapter.kt index 3d3fd3936..9ee2cbeac 100644 --- a/app/src/main/java/org/oxycblt/auxio/library/adapters/AlbumAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/library/adapters/AlbumAdapter.kt @@ -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, private val listener: ClickListener -) : RecyclerView.Adapter() { - - override fun getItemCount(): Int = data.size +) : ListAdapter(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( diff --git a/app/src/main/java/org/oxycblt/auxio/library/adapters/ArtistAdapter.kt b/app/src/main/java/org/oxycblt/auxio/library/adapters/ArtistAdapter.kt index f54bafeba..8b68d5893 100644 --- a/app/src/main/java/org/oxycblt/auxio/library/adapters/ArtistAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/library/adapters/ArtistAdapter.kt @@ -9,10 +9,11 @@ import org.oxycblt.auxio.recycler.BaseViewHolder import org.oxycblt.auxio.recycler.ClickListener class ArtistAdapter( - private val data: List, private val listener: ClickListener ) : RecyclerView.Adapter() { + private var data = listOf() + 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) { + data = newData + + notifyDataSetChanged() + } + inner class ViewHolder( private val binding: ItemArtistBinding ) : BaseViewHolder(binding, listener) { diff --git a/app/src/main/java/org/oxycblt/auxio/library/adapters/GenreAdapter.kt b/app/src/main/java/org/oxycblt/auxio/library/adapters/GenreAdapter.kt index c324d06e6..604754d84 100644 --- a/app/src/main/java/org/oxycblt/auxio/library/adapters/GenreAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/library/adapters/GenreAdapter.kt @@ -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, private val listener: ClickListener -) : RecyclerView.Adapter() { - - override fun getItemCount(): Int = data.size +) : ListAdapter(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( diff --git a/app/src/main/java/org/oxycblt/auxio/music/processing/MusicSorter.kt b/app/src/main/java/org/oxycblt/auxio/music/processing/MusicSorter.kt index 6557e582f..a6729c2d8 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/processing/MusicSorter.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/processing/MusicSorter.kt @@ -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 }) - ) - } } diff --git a/app/src/main/java/org/oxycblt/auxio/recycler/RecyclerUtils.kt b/app/src/main/java/org/oxycblt/auxio/recycler/RecyclerUtils.kt index a20084d14..f9fc0e8da 100644 --- a/app/src/main/java/org/oxycblt/auxio/recycler/RecyclerUtils.kt +++ b/app/src/main/java/org/oxycblt/auxio/recycler/RecyclerUtils.kt @@ -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(val onClick: (T) -> Unit) @@ -44,44 +40,3 @@ abstract class BaseViewHolder( 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>( - NUMERIC_DOWN to compareBy { it.track }, - NUMERIC_UP to compareByDescending { it.track } - ) - - val albumSortComparators = mapOf>( - 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>( - // 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 } - ) - } -} diff --git a/app/src/main/java/org/oxycblt/auxio/recycler/SortMode.kt b/app/src/main/java/org/oxycblt/auxio/recycler/SortMode.kt new file mode 100644 index 000000000..c001ad809 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/recycler/SortMode.kt @@ -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): List { + 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): List { + 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): List { + 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): List { + 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 + } + } +} diff --git a/app/src/main/res/drawable/ic_sort_none.xml b/app/src/main/res/drawable/ic_sort_none.xml new file mode 100644 index 000000000..4162e16d0 --- /dev/null +++ b/app/src/main/res/drawable/ic_sort_none.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_library.xml b/app/src/main/res/layout/fragment_library.xml index 480ad171c..661ec482c 100644 --- a/app/src/main/res/layout/fragment_library.xml +++ b/app/src/main/res/layout/fragment_library.xml @@ -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"> + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/nav_main.xml b/app/src/main/res/navigation/nav_main.xml index 0055fb079..048e2d345 100644 --- a/app/src/main/res/navigation/nav_main.xml +++ b/app/src/main/res/navigation/nav_main.xml @@ -73,7 +73,7 @@ android:name="albumId" app:argType="long" /> Auxio + Library All Songs + No music found. Music loading failed. Permissions to read storage are needed. + Retry Grant Artists Albums Songs + Default + A-Z + Z-A + Album Cover for %s Artist Image for %s Genre Image for %s Track %s Error Change Sorting Mode + Default Sort Order + Sort from A to Z + Sort from Z to A + Unknown Genre Unknown Artist Unknown Album No Date + %1$s / %2$s %1$s / %2$s / %3$s %1$s, %2$s + %s Song %s Songs