From 9ffc194c4d860dd2de62dc37ebc8112bf43629e6 Mon Sep 17 00:00:00 2001 From: OxygenCobalt Date: Sun, 27 Sep 2020 09:47:53 -0600 Subject: [PATCH] Add DetailFragment for genres Add a DetailFragment for genres. --- AuxioTODO | 4 + .../oxycblt/auxio/detail/DetailViewModel.kt | 14 ++ .../auxio/detail/GenreDetailFragment.kt | 120 ++++++++++++++ .../detail/adapters/DetailArtistAdapter.kt | 54 +++++++ .../oxycblt/auxio/library/LibraryFragment.kt | 15 +- .../org/oxycblt/auxio/music/coil/CoilUtils.kt | 11 +- .../oxycblt/auxio/music/coil/MosaicFetcher.kt | 13 +- .../org/oxycblt/auxio/music/models/Genre.kt | 7 + .../auxio/music/processing/MusicSorter.kt | 3 +- .../oxycblt/auxio/recycler/RecyclerUtils.kt | 21 +++ .../res/layout/fragment_artist_detail.xml | 10 +- .../main/res/layout/fragment_genre_detail.xml | 147 ++++++++++++++++++ app/src/main/res/layout/item_artist_album.xml | 4 +- app/src/main/res/layout/item_genre_artist.xml | 63 ++++++++ app/src/main/res/navigation/nav_main.xml | 26 ++++ app/src/main/res/values/strings.xml | 1 + 16 files changed, 497 insertions(+), 16 deletions(-) create mode 100644 app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt create mode 100644 app/src/main/java/org/oxycblt/auxio/detail/adapters/DetailArtistAdapter.kt create mode 100644 app/src/main/res/layout/fragment_genre_detail.xml create mode 100644 app/src/main/res/layout/item_genre_artist.xml diff --git a/AuxioTODO b/AuxioTODO index 553e26b0d..9199d6afd 100644 --- a/AuxioTODO +++ b/AuxioTODO @@ -33,6 +33,10 @@ TODO: /other/ +- Create inherited adapter/viewholder so I dont have to repeat as much code +- ? Condense detail fragments into a single fragment ? +- Make data items inherit a single class +- Condense artist/album recyclerview items into single item - Remove binding adapters To be added: diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt index edcbe3707..e25819c2d 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import org.oxycblt.auxio.music.models.Album import org.oxycblt.auxio.music.models.Artist +import org.oxycblt.auxio.music.models.Genre import org.oxycblt.auxio.recycler.SortMode class DetailViewModel : ViewModel() { @@ -13,12 +14,16 @@ class DetailViewModel : ViewModel() { private val mNavToParentArtist = MutableLiveData() val navToParentArtist: LiveData get() = mNavToParentArtist + private val mGenreSortMode = MutableLiveData(SortMode.ALPHA_DOWN) + val genreSortMode: LiveData get() = mGenreSortMode + private val mArtistSortMode = MutableLiveData(SortMode.NUMERIC_DOWN) val artistSortMode: LiveData get() = mArtistSortMode private val mAlbumSortMode = MutableLiveData(SortMode.NUMERIC_DOWN) val albumSortMode: LiveData get() = mAlbumSortMode + var currentGenre: Genre? = null var currentArtist: Artist? = null var currentAlbum: Album? = null @@ -30,6 +35,15 @@ class DetailViewModel : ViewModel() { mNavToParentArtist.value = false } + fun incrementGenreSortMode() { + mGenreSortMode.value = when (mGenreSortMode.value) { + SortMode.ALPHA_DOWN -> SortMode.ALPHA_UP + SortMode.ALPHA_UP -> SortMode.ALPHA_DOWN + + else -> SortMode.ALPHA_DOWN + } + } + fun incrementArtistSortMode() { mArtistSortMode.value = when (mArtistSortMode.value) { SortMode.NUMERIC_DOWN -> SortMode.NUMERIC_UP diff --git a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt new file mode 100644 index 000000000..d736cda08 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt @@ -0,0 +1,120 @@ +package org.oxycblt.auxio.detail + +import android.content.res.ColorStateList +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.navigation.fragment.findNavController +import androidx.navigation.fragment.navArgs +import org.oxycblt.auxio.R +import org.oxycblt.auxio.databinding.FragmentArtistDetailBinding +import org.oxycblt.auxio.databinding.FragmentGenreDetailBinding +import org.oxycblt.auxio.detail.adapters.DetailAlbumAdapter +import org.oxycblt.auxio.detail.adapters.DetailArtistAdapter +import org.oxycblt.auxio.music.MusicViewModel +import org.oxycblt.auxio.music.models.Album +import org.oxycblt.auxio.music.models.Artist +import org.oxycblt.auxio.recycler.ClickListener +import org.oxycblt.auxio.recycler.SortMode +import org.oxycblt.auxio.theme.applyDivider +import org.oxycblt.auxio.theme.toColor + +class GenreDetailFragment : Fragment() { + + private val args: GenreDetailFragmentArgs by navArgs() + private val detailModel: DetailViewModel by activityViewModels() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + val binding = FragmentGenreDetailBinding.inflate(inflater) + + // If DetailViewModel isn't already storing the genre, get it from MusicViewModel + // using the ID given by the navigation arguments + if (detailModel.currentGenre == null) { + val musicModel: MusicViewModel by activityViewModels() + detailModel.currentGenre = musicModel.genres.value!!.find { + it.id == args.genreId + }!! + } + + val albumAdapter = DetailArtistAdapter( + ClickListener { + navToArtist(it) + } + ) + + binding.lifecycleOwner = this + binding.detailModel = detailModel + binding.genre = detailModel.currentGenre!! + + binding.albumRecycler.adapter = albumAdapter + binding.albumRecycler.applyDivider() + binding.albumRecycler.setHasFixedSize(true) + + binding.toolbar.setNavigationOnClickListener { + findNavController().navigateUp() + } + + detailModel.genreSortMode.observe(viewLifecycleOwner) { mode -> + // Update the current sort icon + binding.sortButton.setImageResource(mode.iconRes) + + // Then update the sort mode of the album adapter. + albumAdapter.submitList( + detailModel.currentGenre!!.artists.sortedWith( + SortMode.artistSortComparators.getOrDefault( + mode, + + // If any invalid value is given, just default to the normal sort order. + compareByDescending { it.name } + ) + ) + ) + } + + // Don't enable the sort button if there is only one artist [Or less] + if (detailModel.currentGenre!!.numArtists < 2) { + binding.sortButton.imageTintList = ColorStateList.valueOf( + R.color.inactive_color.toColor(requireContext()) + ) + + binding.sortButton.isEnabled = false + } + + Log.d(this::class.simpleName, "Fragment created.") + + return binding.root + } + + override fun onResume() { + super.onResume() + + detailModel.isAlreadyNavigating = false + } + + override fun onDestroy() { + super.onDestroy() + + // Reset the stored artist so that the next instance of GenreDetailFragment + // will not read it. + detailModel.currentGenre = null + } + + private fun navToArtist(artist: Artist) { + // Don't navigate if an item already has been selected. + if (!detailModel.isAlreadyNavigating) { + detailModel.isAlreadyNavigating = true + + findNavController().navigate( + GenreDetailFragmentDirections.actionShowArtist(artist.id) + ) + } + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/detail/adapters/DetailArtistAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/adapters/DetailArtistAdapter.kt new file mode 100644 index 000000000..f14c3742b --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/detail/adapters/DetailArtistAdapter.kt @@ -0,0 +1,54 @@ +package org.oxycblt.auxio.detail.adapters + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import org.oxycblt.auxio.databinding.ItemArtistAlbumBinding +import org.oxycblt.auxio.databinding.ItemGenreArtistBinding +import org.oxycblt.auxio.music.models.Album +import org.oxycblt.auxio.music.models.Artist +import org.oxycblt.auxio.recycler.AlbumDiffCallback +import org.oxycblt.auxio.recycler.ArtistDiffCallback +import org.oxycblt.auxio.recycler.ClickListener + +class DetailArtistAdapter( + private val listener: ClickListener +) : ListAdapter(ArtistDiffCallback()) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + return ViewHolder( + ItemGenreArtistBinding.inflate(LayoutInflater.from(parent.context)) + ) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(getItem(position)) + } + + // Generic ViewHolder for an album + inner class ViewHolder( + private val binding: ItemGenreArtistBinding + ) : RecyclerView.ViewHolder(binding.root) { + + init { + // Force the viewholder to *actually* be the screen width so ellipsizing can work. + binding.root.layoutParams = RecyclerView.LayoutParams( + RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT + ) + } + + // Bind the view w/new data + fun bind(artist: Artist) { + binding.artist = artist + + binding.root.setOnClickListener { + listener.onClick(artist) + } + + // Force-update the layout so ellipsizing works. + binding.artistImage.requestLayout() + binding.executePendingBindings() + } + } +} 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 efd628235..b379b8903 100644 --- a/app/src/main/java/org/oxycblt/auxio/library/LibraryFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/library/LibraryFragment.kt @@ -16,6 +16,7 @@ import org.oxycblt.auxio.library.adapters.GenreAdapter import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.music.models.Album import org.oxycblt.auxio.music.models.Artist +import org.oxycblt.auxio.music.models.Genre import org.oxycblt.auxio.recycler.ClickListener import org.oxycblt.auxio.theme.SHOW_ALBUMS import org.oxycblt.auxio.theme.SHOW_ARTISTS @@ -55,7 +56,7 @@ class LibraryFragment : Fragment() { SHOW_GENRES -> GenreAdapter( musicModel.genres.value!!, ClickListener { - Log.d(this::class.simpleName, it.name) + navToGenre(it) } ) @@ -100,4 +101,16 @@ class LibraryFragment : Fragment() { ) } } + + private fun navToGenre(genre: Genre) { + if (!libraryModel.isAlreadyNavigating) { + libraryModel.isAlreadyNavigating = true + + findNavController().navigate( + MainFragmentDirections.actionShowGenre( + genre.id + ) + ) + } + } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/coil/CoilUtils.kt b/app/src/main/java/org/oxycblt/auxio/music/coil/CoilUtils.kt index 3f407661f..e1bfef4e2 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/coil/CoilUtils.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/coil/CoilUtils.kt @@ -78,8 +78,17 @@ fun ImageView.getGenreImage(genre: Genre) { if (genre.numArtists >= 4) { val uris = mutableListOf() + // For each artist, get the nth album from them [if possible]. for (i in 0..3) { - uris.add(genre.artists[i].albums[0].coverUri) + val artist = genre.artists[i] + + uris.add( + if (artist.albums.size > i) { + artist.albums[i].coverUri + } else { + artist.albums[0].coverUri + } + ) } val fetcher = MosaicFetcher(context) diff --git a/app/src/main/java/org/oxycblt/auxio/music/coil/MosaicFetcher.kt b/app/src/main/java/org/oxycblt/auxio/music/coil/MosaicFetcher.kt index 7d0cdba6e..4c54ac893 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/coil/MosaicFetcher.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/coil/MosaicFetcher.kt @@ -5,6 +5,7 @@ import android.graphics.Bitmap import android.graphics.BitmapFactory import android.graphics.Canvas import android.net.Uri +import android.util.Log import androidx.core.graphics.drawable.toDrawable import coil.bitmap.BitmapPool import coil.decode.DataSource @@ -17,8 +18,11 @@ import coil.size.Size import okio.buffer import okio.source import java.io.InputStream +import kotlin.math.pow +import kotlin.math.sqrt const val MOSAIC_BITMAP_SIZE = 512 +const val MOSAIC_BITMAP_INCREMENT = 256 class MosaicFetcher(private val context: Context) : Fetcher> { override suspend fun fetch( @@ -59,25 +63,24 @@ class MosaicFetcher(private val context: Context) : Fetcher> { var x = 0 var y = 0 - val increment = MOSAIC_BITMAP_SIZE / 2 // For each stream, create a bitmap scaled to 1/4th of the mosaics combined size // and place it on a corner of the canvas. for (stream in streams) { val bitmap = Bitmap.createScaledBitmap( BitmapFactory.decodeStream(stream), - increment, - increment, + MOSAIC_BITMAP_INCREMENT, + MOSAIC_BITMAP_INCREMENT, true ) canvas.drawBitmap(bitmap, x.toFloat(), y.toFloat(), null) - x += increment + x += MOSAIC_BITMAP_INCREMENT if (x == MOSAIC_BITMAP_SIZE) { x = 0 - y += increment + y += MOSAIC_BITMAP_INCREMENT if (y == MOSAIC_BITMAP_SIZE) { break diff --git a/app/src/main/java/org/oxycblt/auxio/music/models/Genre.kt b/app/src/main/java/org/oxycblt/auxio/music/models/Genre.kt index 3666b27c6..0b3dc52a1 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/models/Genre.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/models/Genre.kt @@ -14,4 +14,11 @@ data class Genre( } return num } + val numSongs: Int get() { + var num = 0 + artists.forEach { + num += it.numSongs + } + return num + } } 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 ed1d57377..876f8851d 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 @@ -158,8 +158,7 @@ class MusicSorter( // Finalize music private fun finalizeMusic() { - // Remove genre duplicates now, as there's a risk duplicates could be added during the - // sorting process. + // Remove genre duplicates now, as duplicate genres can be added during the sorting process. genres = genres.distinctBy { it.name }.toMutableList() 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 423c5ff46..801d780c0 100644 --- a/app/src/main/java/org/oxycblt/auxio/recycler/RecyclerUtils.kt +++ b/app/src/main/java/org/oxycblt/auxio/recycler/RecyclerUtils.kt @@ -3,6 +3,7 @@ package org.oxycblt.auxio.recycler import androidx.recyclerview.widget.DiffUtil import org.oxycblt.auxio.R import org.oxycblt.auxio.music.models.Album +import org.oxycblt.auxio.music.models.Artist import org.oxycblt.auxio.music.models.Song // RecyclerView click listener @@ -30,6 +31,16 @@ class AlbumDiffCallback : DiffUtil.ItemCallback() { } } +class ArtistDiffCallback : DiffUtil.ItemCallback() { + override fun areContentsTheSame(oldItem: Artist, newItem: Artist): Boolean { + return oldItem.id == newItem.id + } + + override fun areItemsTheSame(oldItem: Artist, newItem: Artist): Boolean { + return oldItem == newItem + } +} + // Sorting modes enum class SortMode(val iconRes: Int) { // Icons for each mode are assigned to the enums themselves @@ -56,7 +67,17 @@ enum class SortMode(val iconRes: Int) { ) { 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/res/layout/fragment_artist_detail.xml b/app/src/main/res/layout/fragment_artist_detail.xml index ae5e5d794..f9d81ca97 100644 --- a/app/src/main/res/layout/fragment_artist_detail.xml +++ b/app/src/main/res/layout/fragment_artist_detail.xml @@ -71,7 +71,7 @@ tools:text="Artist Name" /> + app:layout_constraintTop_toBottomOf="@+id/song_count" /> + app:layout_constraintTop_toBottomOf="@+id/song_count" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_artist_album.xml b/app/src/main/res/layout/item_artist_album.xml index 765d35f49..754f36f68 100644 --- a/app/src/main/res/layout/item_artist_album.xml +++ b/app/src/main/res/layout/item_artist_album.xml @@ -20,8 +20,8 @@ + + + + + + + + + + + + + + + + + \ 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 5ad27a299..06c439f09 100644 --- a/app/src/main/res/navigation/nav_main.xml +++ b/app/src/main/res/navigation/nav_main.xml @@ -35,7 +35,18 @@ app:launchSingleTop="true" /> + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9d386c1f1..5311820c8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -11,6 +11,7 @@ Retry Grant + Artists Albums Songs