From 21626d8d74a81d2afbcdc21a55917b53b7dc1fac Mon Sep 17 00:00:00 2001 From: OxygenCobalt Date: Sun, 27 Sep 2020 08:24:11 -0600 Subject: [PATCH] Add genres to LibraryFragment Add a genre mode to LibraryFragment. --- AuxioTODO | 40 ++++++++++++ .../oxycblt/auxio/library/LibraryFragment.kt | 16 ++++- .../auxio/library/adapters/GenreAdapter.kt | 50 +++++++++++++++ .../org/oxycblt/auxio/music/MusicUtils.kt | 50 ++++++++------- .../org/oxycblt/auxio/music/MusicViewModel.kt | 3 + .../org/oxycblt/auxio/music/coil/CoilUtils.kt | 37 ++++++++++- ...ArtistImageFetcher.kt => MosaicFetcher.kt} | 2 +- .../org/oxycblt/auxio/music/models/Artist.kt | 6 +- .../org/oxycblt/auxio/music/models/Genre.kt | 9 +++ .../auxio/music/processing/MusicSorter.kt | 23 +++++-- app/src/main/res/drawable/ic_genre.xml | 11 ++++ app/src/main/res/layout/item_genre.xml | 63 +++++++++++++++++++ app/src/main/res/values/strings.xml | 10 ++- 13 files changed, 283 insertions(+), 37 deletions(-) create mode 100644 AuxioTODO create mode 100644 app/src/main/java/org/oxycblt/auxio/library/adapters/GenreAdapter.kt rename app/src/main/java/org/oxycblt/auxio/music/coil/{ArtistImageFetcher.kt => MosaicFetcher.kt} (97%) create mode 100644 app/src/main/res/drawable/ic_genre.xml create mode 100644 app/src/main/res/layout/item_genre.xml diff --git a/AuxioTODO b/AuxioTODO new file mode 100644 index 000000000..553e26b0d --- /dev/null +++ b/AuxioTODO @@ -0,0 +1,40 @@ +TODO: + +? - Could do with some more research. +! - Tried to do, but is completely broken. + +/detail/ + +- Add genre detail +- ? Implement Toolbar update functionality ? +- ! Implement shared element transitions ! + +/music/ + +- Add option to show all genres +- ! Move genres to songs ! +- ! Remove lists from music models ! +- ! Dont determine track/album/artist counts on the fly ! + +/songs/ + +- ? Sorting ? +- ? Search ? +- ? Fast Scrolling ? + +/library/ + +- Add genres +- ? Move into ViewPager ? +- Sorting +- Search + - ? Show Artists, Albums, and Songs in search ? +- Exit functionality + +/other/ + +- Remove binding adapters + +To be added: +/prefs/ +/playback/ \ No newline at end of file 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 46e3868ee..efd628235 100644 --- a/app/src/main/java/org/oxycblt/auxio/library/LibraryFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/library/LibraryFragment.kt @@ -12,17 +12,20 @@ import org.oxycblt.auxio.MainFragmentDirections 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.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.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 libraryMode = SHOW_GENRES private val musicModel: MusicViewModel by activityViewModels() private val libraryModel: LibraryViewModel by activityViewModels() @@ -42,12 +45,21 @@ class LibraryFragment : Fragment() { } ) - else -> AlbumAdapter( + SHOW_ALBUMS -> AlbumAdapter( musicModel.albums.value!!, ClickListener { navToAlbum(it) } ) + + SHOW_GENRES -> GenreAdapter( + musicModel.genres.value!!, + ClickListener { + Log.d(this::class.simpleName, it.name) + } + ) + + else -> null } binding.libraryRecycler.applyDivider() 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 new file mode 100644 index 000000000..9f7695c00 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/library/adapters/GenreAdapter.kt @@ -0,0 +1,50 @@ +package org.oxycblt.auxio.library.adapters + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import org.oxycblt.auxio.databinding.ItemGenreBinding +import org.oxycblt.auxio.music.models.Genre +import org.oxycblt.auxio.recycler.ClickListener + +class GenreAdapter( + private val data: List, + private val listener: ClickListener +) : RecyclerView.Adapter() { + + override fun getItemCount(): Int = data.size + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + return ViewHolder( + ItemGenreBinding.inflate(LayoutInflater.from(parent.context)) + ) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(data[position]) + } + + // Generic ViewHolder for an artist + inner class ViewHolder( + private val binding: ItemGenreBinding + ) : 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(genre: Genre) { + binding.genre = genre + + binding.root.setOnClickListener { listener.onClick(genre) } + + // Force-update the layout so ellipsizing works. + binding.artistName.requestLayout() + binding.executePendingBindings() + } + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicUtils.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicUtils.kt index c42702f1d..473886e14 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicUtils.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicUtils.kt @@ -10,6 +10,7 @@ import androidx.databinding.BindingAdapter 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.Genre // List of ID3 genres + Winamp extensions, each index corresponds to their int value. // There are a lot more int-genre extensions as far as Im aware, but this works for most cases. @@ -70,7 +71,7 @@ fun Long.toAlbumArtURI(): Uri { ) } -// Convert a string into its duration +// Convert seconds into its string duration fun Long.toDuration(): String { val durationString = DateUtils.formatElapsedTime(this) @@ -99,37 +100,36 @@ fun getAlbumSongCount(album: Album, context: Context): String { ) } -// Format the amount of songs in an album -@BindingAdapter("songCount") -fun TextView.bindAlbumSongs(album: Album) { - text = getAlbumSongCount(album, context) -} +@BindingAdapter("genreCounts") +fun TextView.bindGenreCounts(genre: Genre) { + val artists = context.resources.getQuantityString( + R.plurals.format_artist_count, genre.numArtists, genre.numArtists + ) -@BindingAdapter("artistCounts") -fun TextView.bindArtistCounts(artist: Artist) { val albums = context.resources.getQuantityString( - R.plurals.format_albums, artist.numAlbums, artist.numAlbums - ) - val songs = context.resources.getQuantityString( - R.plurals.format_song_count, artist.numSongs, artist.numSongs + R.plurals.format_album_count, genre.numAlbums, genre.numAlbums ) - text = context.getString(R.string.format_double_counts, albums, songs) + text = context.getString(R.string.format_double_counts, artists, albums) } // Get the artist genre. // TODO: Add option to list all genres @BindingAdapter("artistGenre") fun TextView.bindArtistGenre(artist: Artist) { - // If there are multiple genres, then pick the most "Prominent" one, - // Otherwise just pick the first one - if (artist.genres.keys.size > 1) { - text = artist.genres.keys.sortedByDescending { - artist.genres[it]?.size - }[0] - } else { - text = artist.genres.keys.first() - } + text = artist.genres[0].name +} + +@BindingAdapter("artistCounts") +fun TextView.bindArtistCounts(artist: Artist) { + val albums = context.resources.getQuantityString( + R.plurals.format_album_count, artist.numAlbums, artist.numAlbums + ) + val songs = context.resources.getQuantityString( + R.plurals.format_song_count, artist.numSongs, artist.numSongs + ) + + text = context.getString(R.string.format_double_counts, albums, songs) } // Get a bunch of miscellaneous album information [Year, Songs, Duration] and combine them @@ -142,3 +142,9 @@ fun TextView.bindAlbumDetails(album: Album) { album.totalDuration ) } + +// Format the amount of songs in an album +@BindingAdapter("songCount") +fun TextView.bindAlbumSongs(album: Album) { + text = getAlbumSongCount(album, context) +} diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt index c7d322d2e..21b117fb6 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt @@ -21,6 +21,9 @@ import org.oxycblt.auxio.music.processing.MusicLoaderResponse import org.oxycblt.auxio.music.processing.MusicSorter // ViewModel for music storage. May also be a god object. +// FIXME: This class can be improved in multiple ways +// - Remove lists/parents from models so that they can be parcelable +// - Move genre usage to songs [If there's a way to find songs without a genre] class MusicViewModel(private val app: Application) : ViewModel() { // Coroutine 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 b1359f75e..3f407661f 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 @@ -9,6 +9,7 @@ import coil.request.ImageRequest 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.Genre import org.oxycblt.auxio.music.models.Song // Get the cover art for a song or album @@ -45,7 +46,7 @@ fun ImageView.getArtistImage(artist: Artist) { uris.add(artist.albums[i].coverUri) } - val fetcher = ArtistImageFetcher(context) + val fetcher = MosaicFetcher(context) request = getDefaultRequest(context, this) .data(uris) @@ -70,6 +71,40 @@ fun ImageView.getArtistImage(artist: Artist) { Coil.imageLoader(context).enqueue(request) } +@BindingAdapter("genreImage") +fun ImageView.getGenreImage(genre: Genre) { + val request: ImageRequest + + if (genre.numArtists >= 4) { + val uris = mutableListOf() + + for (i in 0..3) { + uris.add(genre.artists[i].albums[0].coverUri) + } + + val fetcher = MosaicFetcher(context) + + request = getDefaultRequest(context, this) + .data(uris) + .fetcher(fetcher) + .error(R.drawable.ic_genre) + .build() + } else { + if (genre.artists.isNotEmpty()) { + request = getDefaultRequest(context, this) + .data(genre.artists[0].albums[0].coverUri) + .error(R.drawable.ic_genre) + .build() + } else { + setImageResource(R.drawable.ic_genre) + + return + } + } + + Coil.imageLoader(context).enqueue(request) +} + // Get the base request used across the app. private fun getDefaultRequest(context: Context, imageView: ImageView): ImageRequest.Builder { return ImageRequest.Builder(context) diff --git a/app/src/main/java/org/oxycblt/auxio/music/coil/ArtistImageFetcher.kt b/app/src/main/java/org/oxycblt/auxio/music/coil/MosaicFetcher.kt similarity index 97% rename from app/src/main/java/org/oxycblt/auxio/music/coil/ArtistImageFetcher.kt rename to app/src/main/java/org/oxycblt/auxio/music/coil/MosaicFetcher.kt index 6b101e58e..7d0cdba6e 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/coil/ArtistImageFetcher.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/coil/MosaicFetcher.kt @@ -20,7 +20,7 @@ import java.io.InputStream const val MOSAIC_BITMAP_SIZE = 512 -class ArtistImageFetcher(private val context: Context) : Fetcher> { +class MosaicFetcher(private val context: Context) : Fetcher> { override suspend fun fetch( pool: BitmapPool, data: List, diff --git a/app/src/main/java/org/oxycblt/auxio/music/models/Artist.kt b/app/src/main/java/org/oxycblt/auxio/music/models/Artist.kt index b97bd7717..594441f46 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/models/Artist.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/models/Artist.kt @@ -7,7 +7,7 @@ data class Artist( val givenGenres: MutableList = mutableListOf() ) { val albums = mutableListOf() - lateinit var genres: Map> + val genres = mutableListOf() val numAlbums: Int get() = albums.size val numSongs: Int @@ -18,8 +18,4 @@ data class Artist( } return num } - - fun finalizeGenres() { - genres = givenGenres.groupBy { it.name } - } } 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 8dcc9c917..3666b27c6 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 @@ -5,4 +5,13 @@ data class Genre( var name: String, ) { val artists = mutableListOf() + + val numArtists: Int get() = artists.size + val numAlbums: Int get() { + var num = 0 + artists.forEach { + num += it.numAlbums + } + 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 cec217eb0..ed1d57377 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 @@ -7,7 +7,7 @@ import org.oxycblt.auxio.music.models.Genre import org.oxycblt.auxio.music.models.Song class MusicSorter( - val genres: MutableList, + var genres: MutableList, val artists: MutableList, val albums: MutableList, val songs: MutableList, @@ -81,6 +81,18 @@ class MusicSorter( artist.albums.add(album) } + // Then group the artist's genres and sort them by "Prominence" + // A.K.A Who has the most map entries + val groupedGenres = artist.givenGenres.groupBy { it.name } + + groupedGenres.keys.sortedByDescending { key -> + groupedGenres[key]?.size + }.forEach { key -> + groupedGenres[key]?.get(0)?.let { + artist.genres.add(it) + } + } + unknownAlbums.removeAll(artistAlbums) } @@ -127,7 +139,7 @@ class MusicSorter( if (unknownArtists.size > 0) { // Reuse an existing unknown genre if one is found - val unknownGenre = Genre( + val unknownGenre = genres.find { it.name == genrePlaceholder } ?: Genre( name = genrePlaceholder ) @@ -146,8 +158,11 @@ class MusicSorter( // Finalize music private fun finalizeMusic() { - // Finalize the genre for each artist - artists.forEach { it.finalizeGenres() } + // Remove genre duplicates now, as there's a risk duplicates could be added during the + // sorting process. + genres = genres.distinctBy { + it.name + }.toMutableList() // Then finally sort the music genres.sortWith( diff --git a/app/src/main/res/drawable/ic_genre.xml b/app/src/main/res/drawable/ic_genre.xml new file mode 100644 index 000000000..3a4918b43 --- /dev/null +++ b/app/src/main/res/drawable/ic_genre.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_genre.xml b/app/src/main/res/layout/item_genre.xml new file mode 100644 index 000000000..098d37bb8 --- /dev/null +++ b/app/src/main/res/layout/item_genre.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + \ 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 f1f6b8218..9d386c1f1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -15,7 +15,8 @@ Songs Album Cover for %s - Artist Cover for %s + Artist Image for %s + Genre Image for %s Track %s Error Change Sorting Mode @@ -34,8 +35,13 @@ %s Songs - + %s Album %s Albums + + + %s Artist + %s Artists + \ No newline at end of file