From 26f27d0edd810b7ad48c82dcca916c376cab9ae6 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Wed, 18 Sep 2024 14:50:53 -0600 Subject: [PATCH] detail: split off detail list into generator --- .../oxycblt/auxio/detail/DetailGenerator.kt | 216 ++++++++++++ .../org/oxycblt/auxio/detail/DetailModule.kt | 30 ++ .../oxycblt/auxio/detail/DetailViewModel.kt | 320 +++++------------- .../org/oxycblt/auxio/list/ListSettings.kt | 10 +- .../music/service/MediaItemTranslation.kt | 4 + .../auxio/music/service/MusicBrowser.kt | 113 +++---- 6 files changed, 395 insertions(+), 298 deletions(-) create mode 100644 app/src/main/java/org/oxycblt/auxio/detail/DetailGenerator.kt create mode 100644 app/src/main/java/org/oxycblt/auxio/detail/DetailModule.kt diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailGenerator.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailGenerator.kt new file mode 100644 index 000000000..bd5356df5 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailGenerator.kt @@ -0,0 +1,216 @@ +package org.oxycblt.auxio.detail + +import android.content.Context +import androidx.annotation.StringRes +import org.oxycblt.auxio.R +import org.oxycblt.auxio.detail.list.DiscHeader +import org.oxycblt.auxio.list.ListSettings +import org.oxycblt.auxio.list.sort.Sort +import org.oxycblt.auxio.music.Album +import org.oxycblt.auxio.music.Artist +import org.oxycblt.auxio.music.Genre +import org.oxycblt.auxio.music.Music +import org.oxycblt.auxio.music.MusicParent +import org.oxycblt.auxio.music.MusicRepository +import org.oxycblt.auxio.music.MusicType +import org.oxycblt.auxio.music.Playlist +import org.oxycblt.auxio.music.Song +import org.oxycblt.auxio.music.info.Disc +import org.oxycblt.auxio.music.info.ReleaseType +import org.oxycblt.auxio.util.logD +import java.util.SortedMap +import javax.inject.Inject + +interface DetailGenerator { + fun any(uid: Music.UID): Detail? + fun album(uid: Music.UID): Detail? + fun artist(uid: Music.UID): Detail? + fun genre(uid: Music.UID): Detail? + fun playlist(uid: Music.UID): Detail? + fun release() + + interface Factory { + fun create(invalidator: Invalidator): DetailGenerator + } + + interface Invalidator { + fun invalidate(type: MusicType, replace: Int?) + } +} + +class DetailGeneratorFactoryImpl @Inject constructor( + private val listSettings: ListSettings, + private val musicRepository: MusicRepository +) : DetailGenerator.Factory { + override fun create(invalidator: DetailGenerator.Invalidator): DetailGenerator = + DetailGeneratorImpl(invalidator, listSettings, musicRepository) +} + +private class DetailGeneratorImpl( + private val invalidator: DetailGenerator.Invalidator, + private val listSettings: ListSettings, + private val musicRepository: MusicRepository +) : DetailGenerator, MusicRepository.UpdateListener, ListSettings.Listener { + init { + listSettings.registerListener(this) + musicRepository.addUpdateListener(this) + } + + override fun onAlbumSongSortChanged() { + super.onAlbumSongSortChanged() + invalidator.invalidate(MusicType.ALBUMS, -1) + } + + override fun onArtistSongSortChanged() { + super.onArtistSongSortChanged() + invalidator.invalidate(MusicType.ARTISTS, -1) + } + + override fun onGenreSongSortChanged() { + super.onGenreSongSortChanged() + invalidator.invalidate(MusicType.GENRES, -1) + } + + override fun onMusicChanges(changes: MusicRepository.Changes) { + if (changes.deviceLibrary) { + invalidator.invalidate(MusicType.ALBUMS, null) + invalidator.invalidate(MusicType.ARTISTS, null) + invalidator.invalidate(MusicType.GENRES, null) + } + if (changes.userLibrary) { + invalidator.invalidate(MusicType.PLAYLISTS, null) + } + } + + override fun release() { + listSettings.unregisterListener(this) + musicRepository.removeUpdateListener(this) + } + + override fun any(uid: Music.UID): Detail? { + val music = musicRepository.find(uid) ?: return null + return when (music) { + is Album -> album(uid) + is Artist -> artist(uid) + is Genre -> genre(uid) + is Playlist -> playlist(uid) + else -> null + } + } + + override fun album(uid: Music.UID): Detail? { + val album = musicRepository.deviceLibrary?.findAlbum(uid) ?: return null + val songs = listSettings.albumSongSort.songs(album.songs) + val discs = songs.groupBy { it.disc } + val section = if (discs.size > 1 || discs.keys.first() != null) { + DetailSection.Discs(discs) + } else { + DetailSection.Songs(songs) + } + return Detail(album, listOf(section)) + } + + override fun artist(uid: Music.UID): Detail? { + val artist = musicRepository.deviceLibrary?.findArtist(uid) ?: return null + val grouping = + artist.explicitAlbums.groupByTo(sortedMapOf()) { + // Remap the complicated ReleaseType data structure into detail sections + when (it.releaseType.refinement) { + ReleaseType.Refinement.LIVE -> DetailSection.Albums.Category.LIVE + ReleaseType.Refinement.REMIX -> DetailSection.Albums.Category.REMIXES + null -> + when (it.releaseType) { + is ReleaseType.Album -> DetailSection.Albums.Category.ALBUMS + is ReleaseType.EP -> DetailSection.Albums.Category.EPS + is ReleaseType.Single -> DetailSection.Albums.Category.SINGLES + is ReleaseType.Compilation -> DetailSection.Albums.Category.COMPILATIONS + is ReleaseType.Soundtrack -> DetailSection.Albums.Category.SOUNDTRACKS + is ReleaseType.Mix -> DetailSection.Albums.Category.DJ_MIXES + is ReleaseType.Mixtape -> DetailSection.Albums.Category.MIXTAPES + is ReleaseType.Demo -> DetailSection.Albums.Category.DEMOS + } + } + } + + if (artist.implicitAlbums.isNotEmpty()) { + // groupByTo normally returns a mapping to a MutableList mapping. Since MutableList + // inherits list, we can cast upwards and save a copy by directly inserting the + // implicit album list into the mapping. + logD("Implicit albums present, adding to list") + @Suppress("UNCHECKED_CAST") + (grouping as MutableMap>)[DetailSection.Albums.Category.APPEARANCES] = + artist.implicitAlbums + } + + val sections = grouping.mapTo(mutableListOf()) { (category, albums) -> + DetailSection.Albums(category, ARTIST_ALBUM_SORT.albums(albums)) + } + val songs = DetailSection.Songs(listSettings.artistSongSort.songs(artist.songs)) + sections.add(songs) + return Detail(artist, sections) + } + + override fun genre(uid: Music.UID): Detail? { + val genre = musicRepository.deviceLibrary?.findGenre(uid) ?: return null + val artists = DetailSection.Artists(GENRE_ARTIST_SORT.artists(genre.artists)) + val songs = DetailSection.Songs(listSettings.genreSongSort.songs(genre.songs)) + return Detail(genre, listOf(artists, songs)) + } + + override fun playlist(uid: Music.UID): Detail? { + val playlist = musicRepository.userLibrary?.findPlaylist(uid) ?: return null + val songs = DetailSection.Songs(playlist.songs) + return Detail(playlist, listOf(songs)) + } + + private companion object { + val ARTIST_ALBUM_SORT = Sort(Sort.Mode.ByDate, Sort.Direction.DESCENDING) + val GENRE_ARTIST_SORT = Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING) + } +} + +data class Detail

(val parent: P, val sections: List) + +sealed interface DetailSection { + val order: Int + val stringRes: Int + + abstract class PlainSection : DetailSection { + abstract val items: List + } + + data class Artists(override val items: List) : PlainSection() { + override val order = 0 + override val stringRes = R.string.lbl_songs + } + + data class Albums(val category: Category, override val items: List) : PlainSection() { + override val order = 1 + category.ordinal + override val stringRes = category.stringRes + + enum class Category(@StringRes val stringRes: Int) { + ALBUMS(R.string.lbl_albums), + EPS(R.string.lbl_eps), + SINGLES(R.string.lbl_singles), + COMPILATIONS(R.string.lbl_compilations), + SOUNDTRACKS(R.string.lbl_soundtracks), + DJ_MIXES(R.string.lbl_mixes), + MIXTAPES(R.string.lbl_mixtapes), + DEMOS(R.string.lbl_demos), + APPEARANCES(R.string.lbl_appears_on), + LIVE(R.string.lbl_live_group), + REMIXES(R.string.lbl_remix_group) + } + } + + + data class Songs(override val items: List) : PlainSection() { + override val order = 12 + override val stringRes = R.string.lbl_songs + } + + data class Discs(val discs: Map>) : DetailSection { + override val order = 13 + override val stringRes = R.string.lbl_songs + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailModule.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailModule.kt new file mode 100644 index 000000000..4d41529db --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailModule.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2024 Auxio Project + * HomeModule.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.auxio.detail + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +interface DetailModule { + @Binds fun detailGeneratorFactory(factory: DetailGeneratorFactoryImpl): DetailGenerator.Factory +} 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 3613d96c6..03390b2a1 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt @@ -18,7 +18,6 @@ package org.oxycblt.auxio.detail -import androidx.annotation.StringRes import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel @@ -43,10 +42,11 @@ import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Music +import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicRepository +import org.oxycblt.auxio.music.MusicType import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.music.info.ReleaseType import org.oxycblt.auxio.music.metadata.AudioProperties import org.oxycblt.auxio.playback.PlaySong import org.oxycblt.auxio.playback.PlaybackSettings @@ -69,8 +69,12 @@ constructor( private val listSettings: ListSettings, private val musicRepository: MusicRepository, private val audioPropertiesFactory: AudioProperties.Factory, - private val playbackSettings: PlaybackSettings -) : ViewModel(), MusicRepository.UpdateListener { + private val playbackSettings: PlaybackSettings, + detailGeneratorFactory: DetailGenerator.Factory +) : ViewModel(), DetailGenerator.Invalidator { + private val detailGenerator = detailGeneratorFactory.create(this) + + private val _toShow = MutableEvent() /** * A [Show] command that is awaiting a view capable of responding to it. Null if none currently. @@ -133,13 +137,8 @@ constructor( get() = _artistSongInstructions /** The current [Sort] used for [Song]s in [artistSongList]. */ - var artistSongSort: Sort + val artistSongSort: Sort get() = listSettings.artistSongSort - set(value) { - listSettings.artistSongSort = value - // Refresh the artist list to reflect the new sort. - currentArtist.value?.let { refreshArtistList(it, true) } - } /** The [PlaySong] instructions to use when playing a [Song] from [Artist] details. */ val playInArtistWith @@ -162,13 +161,8 @@ constructor( get() = _genreSongInstructions /** The current [Sort] used for [Song]s in [genreSongList]. */ - var genreSongSort: Sort + val genreSongSort: Sort get() = listSettings.genreSongSort - set(value) { - listSettings.genreSongSort = value - // Refresh the genre list to reflect the new sort. - currentGenre.value?.let { refreshGenreList(it, true) } - } /** The [PlaySong] instructions to use when playing a [Song] from [Genre] details. */ val playInGenreWith @@ -204,54 +198,32 @@ constructor( playbackSettings.inParentPlaybackMode ?: PlaySong.FromPlaylist(unlikelyToBeNull(currentPlaylist.value)) - init { - musicRepository.addUpdateListener(this) - } - override fun onCleared() { - musicRepository.removeUpdateListener(this) + detailGenerator.release() } - override fun onMusicChanges(changes: MusicRepository.Changes) { - // If we are showing any item right now, we will need to refresh it (and any information - // related to it) with the new library in order to prevent stale items from showing up - // in the UI. - val deviceLibrary = musicRepository.deviceLibrary - if (changes.deviceLibrary && deviceLibrary != null) { - val song = currentSong.value - if (song != null) { - _currentSong.value = deviceLibrary.findSong(song.uid)?.also(::refreshAudioInfo) - logD("Updated song to ${currentSong.value}") + override fun invalidate(type: MusicType, replace: Int?) { + when (type) { + MusicType.ALBUMS -> { + val album = detailGenerator.album(currentAlbum.value?.uid ?: return) + refreshDetail(album, _currentAlbum, _albumSongList, _albumSongInstructions, replace) } - val album = currentAlbum.value - if (album != null) { - _currentAlbum.value = deviceLibrary.findAlbum(album.uid)?.also(::refreshAlbumList) - logD("Updated album to ${currentAlbum.value}") + MusicType.ARTISTS -> { + val artist = detailGenerator.artist(currentArtist.value?.uid ?: return) + refreshDetail(artist, _currentArtist, _artistSongList, _artistSongInstructions, replace) } - val artist = currentArtist.value - if (artist != null) { - _currentArtist.value = - deviceLibrary.findArtist(artist.uid)?.also(::refreshArtistList) - logD("Updated artist to ${currentArtist.value}") + MusicType.GENRES -> { + val genre = detailGenerator.genre(currentGenre.value?.uid ?: return) + refreshDetail(genre, _currentGenre, _genreSongList, _genreSongInstructions, replace) } - val genre = currentGenre.value - if (genre != null) { - _currentGenre.value = deviceLibrary.findGenre(genre.uid)?.also(::refreshGenreList) - logD("Updated genre to ${currentGenre.value}") + MusicType.PLAYLISTS -> { + refreshPlaylist(currentPlaylist.value?.uid ?: return) } - } - val userLibrary = musicRepository.userLibrary - if (changes.userLibrary && userLibrary != null) { - val playlist = currentPlaylist.value - if (playlist != null) { - _currentPlaylist.value = - userLibrary.findPlaylist(playlist.uid)?.also(::refreshPlaylistList) - logD("Updated playlist to ${currentPlaylist.value}") - } + else -> error("Unexpected music type $type") } } @@ -356,8 +328,11 @@ constructor( */ fun setAlbum(uid: Music.UID) { logD("Opening album $uid") - _currentAlbum.value = - musicRepository.deviceLibrary?.findAlbum(uid)?.also(::refreshAlbumList) + if (uid === _currentAlbum.value?.uid) { + return + } + val album = detailGenerator.album(uid) + refreshDetail(album, _currentAlbum, _albumSongList, _albumSongInstructions, null) if (_currentAlbum.value == null) { logW("Given album UID was invalid") } @@ -370,7 +345,6 @@ constructor( */ fun applyAlbumSongSort(sort: Sort) { listSettings.albumSongSort = sort - _currentAlbum.value?.let { refreshAlbumList(it, true) } } /** @@ -381,11 +355,11 @@ constructor( */ fun setArtist(uid: Music.UID) { logD("Opening artist $uid") - _currentArtist.value = - musicRepository.deviceLibrary?.findArtist(uid)?.also(::refreshArtistList) - if (_currentArtist.value == null) { - logW("Given artist UID was invalid") + if (uid === _currentArtist.value?.uid) { + return } + val artist = detailGenerator.artist(uid) + refreshDetail(artist, _currentArtist, _artistSongList, _artistSongInstructions, null) } /** @@ -395,7 +369,6 @@ constructor( */ fun applyArtistSongSort(sort: Sort) { listSettings.artistSongSort = sort - _currentArtist.value?.let { refreshArtistList(it, true) } } /** @@ -406,11 +379,11 @@ constructor( */ fun setGenre(uid: Music.UID) { logD("Opening genre $uid") - _currentGenre.value = - musicRepository.deviceLibrary?.findGenre(uid)?.also(::refreshGenreList) - if (_currentGenre.value == null) { - logW("Given genre UID was invalid") + if (uid === _currentGenre.value?.uid) { + return } + val genre = detailGenerator.genre(uid) + refreshDetail(genre, _currentGenre, _genreSongList, _genreSongInstructions, null) } /** @@ -420,7 +393,6 @@ constructor( */ fun applyGenreSongSort(sort: Sort) { listSettings.genreSongSort = sort - _currentGenre.value?.let { refreshGenreList(it, true) } } /** @@ -431,11 +403,10 @@ constructor( */ fun setPlaylist(uid: Music.UID) { logD("Opening playlist $uid") - _currentPlaylist.value = - musicRepository.userLibrary?.findPlaylist(uid)?.also(::refreshPlaylistList) - if (_currentPlaylist.value == null) { - logW("Given playlist UID was invalid") + if (uid === _currentPlaylist.value?.uid) { + return } + refreshPlaylist(uid) } /** Start a playlist editing session. Does nothing if a playlist is not being shown. */ @@ -443,7 +414,7 @@ constructor( val playlist = _currentPlaylist.value ?: return logD("Starting playlist edit") _editedPlaylist.value = playlist.songs - refreshPlaylistList(playlist) + refreshPlaylist(playlist.uid) } /** @@ -474,9 +445,8 @@ constructor( // Nothing to do. return false } - logD("Discarding playlist edits") _editedPlaylist.value = null - refreshPlaylistList(playlist) + refreshPlaylist(playlist.uid) return true } @@ -488,7 +458,7 @@ constructor( fun applyPlaylistSongSort(sort: Sort) { val playlist = _currentPlaylist.value ?: return _editedPlaylist.value = sort.songs(_editedPlaylist.value ?: return) - refreshPlaylistList(playlist, UpdateInstructions.Replace(2)) + refreshPlaylist(playlist.uid, UpdateInstructions.Replace(2)) } /** @@ -509,7 +479,7 @@ constructor( logD("Moving playlist song from $realFrom [$from] to $realTo [$to]") editedPlaylist.add(realFrom, editedPlaylist.removeAt(realTo)) _editedPlaylist.value = editedPlaylist - refreshPlaylistList(playlist, UpdateInstructions.Move(from, to)) + refreshPlaylist(playlist.uid, UpdateInstructions.Move(from, to)) return true } @@ -528,8 +498,8 @@ constructor( logD("Removing playlist song at $realAt [$at]") editedPlaylist.removeAt(realAt) _editedPlaylist.value = editedPlaylist - refreshPlaylistList( - playlist, + refreshPlaylist( + playlist.uid, if (editedPlaylist.isNotEmpty()) { UpdateInstructions.Remove(at, 1) } else { @@ -552,173 +522,69 @@ constructor( } } - private fun refreshAlbumList(album: Album, replace: Boolean = false) { - logD("Refreshing album list") - val list = mutableListOf() - val header = SortHeader(R.string.lbl_songs) - list.add(Divider(header)) - list.add(header) - val instructions = - if (replace) { - // Intentional so that the header item isn't replaced with the songs - UpdateInstructions.Replace(list.size) - } else { - UpdateInstructions.Diff - } - // To create a good user experience regarding disc numbers, we group the album's - // songs up by disc and then delimit the groups by a disc header. - val songs = albumSongSort.songs(album.songs) - val byDisc = songs.groupBy { it.disc } - if (byDisc.size > 1) { - logD("Album has more than one disc, interspersing headers") - for (entry in byDisc.entries) { - list.add(DiscHeader(entry.key)) - list.addAll(entry.value) - } - } else { - // Album only has one disc, don't add any redundant headers - list.addAll(songs) + private fun refreshDetail( + detail: Detail?, + parent: MutableStateFlow, + list: MutableStateFlow>, + instructions: MutableEvent, + replace: Int? + ) { + if (detail == null) { + parent.value = null + return } + val newList = mutableListOf() + var newInstructions: UpdateInstructions = UpdateInstructions.Diff + for ((i, section) in detail.sections.withIndex()) { + val items = when (section) { + is DetailSection.PlainSection<*> -> { + val header = if (section is DetailSection.Songs) + SortHeader(section.stringRes) else BasicHeader(section.stringRes) + newList.add(Divider(header)) + newList.add(header) + section.items + } - logD("Update album list to ${list.size} items with $instructions") - _albumSongInstructions.put(instructions) - _albumSongList.value = list - } - - private fun refreshArtistList(artist: Artist, replace: Boolean = false) { - logD("Refreshing artist list") - val list = mutableListOf() - - val grouping = - artist.explicitAlbums.groupByTo(sortedMapOf()) { - // Remap the complicated ReleaseType data structure into an easier - // "AlbumGrouping" enum that will automatically group and sort - // the artist's albums. - when (it.releaseType.refinement) { - ReleaseType.Refinement.LIVE -> AlbumGrouping.LIVE - ReleaseType.Refinement.REMIX -> AlbumGrouping.REMIXES - null -> - when (it.releaseType) { - is ReleaseType.Album -> AlbumGrouping.ALBUMS - is ReleaseType.EP -> AlbumGrouping.EPS - is ReleaseType.Single -> AlbumGrouping.SINGLES - is ReleaseType.Compilation -> AlbumGrouping.COMPILATIONS - is ReleaseType.Soundtrack -> AlbumGrouping.SOUNDTRACKS - is ReleaseType.Mix -> AlbumGrouping.DJMIXES - is ReleaseType.Mixtape -> AlbumGrouping.MIXTAPES - is ReleaseType.Demo -> AlbumGrouping.DEMOS - } + is DetailSection.Discs -> { + val header = BasicHeader(section.stringRes) + newList.add(Divider(header)) + newList.add(header) + section.discs.flatMap { + listOf(DiscHeader(it.key)) + it.value + } } } - - if (artist.implicitAlbums.isNotEmpty()) { - // groupByTo normally returns a mapping to a MutableList mapping. Since MutableList - // inherits list, we can cast upwards and save a copy by directly inserting the - // implicit album list into the mapping. - logD("Implicit albums present, adding to list") - @Suppress("UNCHECKED_CAST") - (grouping as MutableMap>)[AlbumGrouping.APPEARANCES] = - artist.implicitAlbums - } - - logD("Release groups for this artist: ${grouping.keys}") - - for (entry in grouping.entries) { - val header = BasicHeader(entry.key.headerTitleRes) - list.add(Divider(header)) - list.add(header) - list.addAll(ARTIST_ALBUM_SORT.albums(entry.value)) - } - - // Artists may not be linked to any songs, only include a header entry if we have any. - var instructions: UpdateInstructions = UpdateInstructions.Diff - if (artist.songs.isNotEmpty()) { - logD("Songs present in this artist, adding header") - val header = SortHeader(R.string.lbl_songs) - list.add(Divider(header)) - list.add(header) - if (replace) { + // Currently only the final section (songs, which can be sorted) are invalidatable + // and thus need to be replaced. + if (replace == -1 && i == detail.sections.lastIndex) { // Intentional so that the header item isn't replaced with the songs - instructions = UpdateInstructions.Replace(list.size) + newInstructions = UpdateInstructions.Replace(newList.size) } - list.addAll(artistSongSort.songs(artist.songs)) + newList.addAll(items) } - - logD("Updating artist list to ${list.size} items with $instructions") - _artistSongInstructions.put(instructions) - _artistSongList.value = list.toList() + parent.value = detail.parent + list.value = newList + instructions.put(newInstructions) } - private fun refreshGenreList(genre: Genre, replace: Boolean = false) { - logD("Refreshing genre list") - val list = mutableListOf() - // Genre is guaranteed to always have artists and songs. - val artistHeader = BasicHeader(R.string.lbl_artists) - list.add(Divider(artistHeader)) - list.add(artistHeader) - list.addAll(GENRE_ARTIST_SORT.artists(genre.artists)) - - val songHeader = SortHeader(R.string.lbl_songs) - list.add(Divider(songHeader)) - list.add(songHeader) - val instructions = - if (replace) { - // Intentional so that the header item isn't replaced alongside the songs - UpdateInstructions.Replace(list.size) - } else { - UpdateInstructions.Diff - } - list.addAll(genreSongSort.songs(genre.songs)) - - logD("Updating genre list to ${list.size} items with $instructions") - _genreSongInstructions.put(instructions) - _genreSongList.value = list - } - - private fun refreshPlaylistList( - playlist: Playlist, - instructions: UpdateInstructions = UpdateInstructions.Diff - ) { + private fun refreshPlaylist(uid: Music.UID, instructions: UpdateInstructions = UpdateInstructions.Diff) { logD("Refreshing playlist list") + val edited = editedPlaylist.value + if (edited == null) { + val playlist = detailGenerator.playlist(uid) + refreshDetail(playlist, _currentPlaylist, _playlistSongList, _playlistSongInstructions, null) + return + } val list = mutableListOf() - - val songs = editedPlaylist.value ?: playlist.songs - if (songs.isNotEmpty()) { + if (edited.isNotEmpty()) { val header = EditHeader(R.string.lbl_songs) list.add(Divider(header)) list.add(header) - list.addAll(songs) + list.addAll(edited) } - - logD("Updating playlist list to ${list.size} items with $instructions") - _playlistSongInstructions.put(instructions) _playlistSongList.value = list - } - - /** - * A simpler mapping of [ReleaseType] used for grouping and sorting songs. - * - * @param headerTitleRes The title string resource to use for a header created out of an - * instance of this enum. - */ - private enum class AlbumGrouping(@StringRes val headerTitleRes: Int) { - ALBUMS(R.string.lbl_albums), - EPS(R.string.lbl_eps), - SINGLES(R.string.lbl_singles), - COMPILATIONS(R.string.lbl_compilations), - SOUNDTRACKS(R.string.lbl_soundtracks), - DJMIXES(R.string.lbl_mixes), - MIXTAPES(R.string.lbl_mixtapes), - DEMOS(R.string.lbl_demos), - APPEARANCES(R.string.lbl_appears_on), - LIVE(R.string.lbl_live_group), - REMIXES(R.string.lbl_remix_group), - } - - private companion object { - val ARTIST_ALBUM_SORT = Sort(Sort.Mode.ByDate, Sort.Direction.DESCENDING) - val GENRE_ARTIST_SORT = Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING) + _playlistSongInstructions.put(instructions) } } diff --git a/app/src/main/java/org/oxycblt/auxio/list/ListSettings.kt b/app/src/main/java/org/oxycblt/auxio/list/ListSettings.kt index c817dcf0e..37cb2cc23 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/ListSettings.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/ListSettings.kt @@ -46,13 +46,12 @@ interface ListSettings : Settings { interface Listener { fun onSongSortChanged() {} - fun onAlbumSortChanged() {} - + fun onAlbumSongSortChanged() {} fun onArtistSortChanged() {} - + fun onArtistSongSortChanged() {} fun onGenreSortChanged() {} - + fun onGenreSongSortChanged() {} fun onPlaylistSortChanged() {} } } @@ -162,8 +161,11 @@ class ListSettingsImpl @Inject constructor(@ApplicationContext val context: Cont when (key) { getString(R.string.set_key_songs_sort) -> listener.onSongSortChanged() getString(R.string.set_key_albums_sort) -> listener.onAlbumSortChanged() + getString(R.string.set_key_album_songs_sort) -> listener.onAlbumSongSortChanged() getString(R.string.set_key_artists_sort) -> listener.onArtistSortChanged() + getString(R.string.set_key_artist_songs_sort) -> listener.onArtistSongSortChanged() getString(R.string.set_key_genres_sort) -> listener.onGenreSortChanged() + getString(R.string.set_key_genre_songs_sort) -> listener.onGenreSongSortChanged() getString(R.string.set_key_playlists_sort) -> listener.onPlaylistSortChanged() } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/service/MediaItemTranslation.kt b/app/src/main/java/org/oxycblt/auxio/music/service/MediaItemTranslation.kt index 624247f8e..1d3c8ff02 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/service/MediaItemTranslation.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/service/MediaItemTranslation.kt @@ -130,6 +130,10 @@ fun header(@StringRes nameRes: Int): Sugar = { MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, it.getString(nameRes)) } +fun header(name: String): Sugar = { + putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, name) +} + private fun style(style: Int): Sugar = { putInt(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_SINGLE_ITEM, style) } diff --git a/app/src/main/java/org/oxycblt/auxio/music/service/MusicBrowser.kt b/app/src/main/java/org/oxycblt/auxio/music/service/MusicBrowser.kt index ecb53daa9..2a109204a 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/service/MusicBrowser.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/service/MusicBrowser.kt @@ -22,7 +22,12 @@ import android.content.Context import android.support.v4.media.MediaBrowserCompat.MediaItem import javax.inject.Inject import org.oxycblt.auxio.R +import org.oxycblt.auxio.detail.DetailGenerator +import org.oxycblt.auxio.detail.DetailSection +import org.oxycblt.auxio.detail.list.SortHeader import org.oxycblt.auxio.home.HomeGenerator +import org.oxycblt.auxio.list.BasicHeader +import org.oxycblt.auxio.list.Divider import org.oxycblt.auxio.list.ListSettings import org.oxycblt.auxio.list.adapter.UpdateInstructions import org.oxycblt.auxio.list.sort.Sort @@ -42,17 +47,17 @@ private constructor( private val invalidator: Invalidator, private val musicRepository: MusicRepository, private val searchEngine: SearchEngine, - private val listSettings: ListSettings, - homeGeneratorFactory: HomeGenerator.Factory -) : MusicRepository.UpdateListener, HomeGenerator.Invalidator { + homeGeneratorFactory: HomeGenerator.Factory, + detailGeneratorFactory: DetailGenerator.Factory +) : HomeGenerator.Invalidator, DetailGenerator.Invalidator { class Factory @Inject constructor( private val musicRepository: MusicRepository, private val searchEngine: SearchEngine, - private val listSettings: ListSettings, - private val homeGeneratorFactory: HomeGenerator.Factory + private val homeGeneratorFactory: HomeGenerator.Factory, + private val detailGeneratorFactory: DetailGenerator.Factory ) { fun create(context: Context, invalidator: Invalidator): MusicBrowser = MusicBrowser( @@ -60,8 +65,8 @@ private constructor( invalidator, musicRepository, searchEngine, - listSettings, - homeGeneratorFactory) + homeGeneratorFactory, + detailGeneratorFactory) } interface Invalidator { @@ -69,13 +74,11 @@ private constructor( } private val homeGenerator = homeGeneratorFactory.create(this) - - init { - musicRepository.addUpdateListener(this) - } + private val detailGenerator = detailGeneratorFactory.create(this) fun release() { - musicRepository.removeUpdateListener(this) + homeGenerator.release() + detailGenerator.release() } override fun invalidateMusic(type: MusicType, instructions: UpdateInstructions) { @@ -92,36 +95,21 @@ private constructor( } } - override fun onMusicChanges(changes: MusicRepository.Changes) { - val deviceLibrary = musicRepository.deviceLibrary - val invalidate = mutableSetOf() - if (changes.deviceLibrary && deviceLibrary != null) { - deviceLibrary.albums.forEach { - val id = MediaSessionUID.SingleItem(it.uid).toString() - invalidate.add(id) - } - - deviceLibrary.artists.forEach { - val id = MediaSessionUID.SingleItem(it.uid).toString() - invalidate.add(id) - } - - deviceLibrary.genres.forEach { - val id = MediaSessionUID.SingleItem(it.uid).toString() - invalidate.add(id) - } + override fun invalidate(type: MusicType, replace: Int?) { + val deviceLibrary = musicRepository.deviceLibrary ?: return + val userLibrary = musicRepository.userLibrary ?: return + val music = when (type) { + MusicType.ALBUMS -> deviceLibrary.albums + MusicType.ARTISTS -> deviceLibrary.artists + MusicType.GENRES -> deviceLibrary.genres + MusicType.PLAYLISTS -> userLibrary.playlists + else -> return } - val userLibrary = musicRepository.userLibrary - if (changes.userLibrary && userLibrary != null) { - userLibrary.playlists.forEach { - val id = MediaSessionUID.SingleItem(it.uid).toString() - invalidate.add(id) - } - } - - if (invalidate.isNotEmpty()) { - invalidator?.invalidateMusic(invalidate) + if (music.isEmpty()) { + return } + val ids = music.map { MediaSessionUID.SingleItem(it.uid).toString() }.toSet() + invalidator.invalidateMusic(ids) } fun getItem(mediaId: String): MediaItem? { @@ -235,34 +223,25 @@ private constructor( } private fun getChildMediaItems(uid: Music.UID): List? { - return when (val item = musicRepository.find(uid)) { - is Album -> { - val songs = listSettings.albumSongSort.songs(item.songs) - songs.map { it.toMediaItem(context, item, header(R.string.lbl_songs)) } + val detail = detailGenerator.any(uid) ?: return null + return detail.sections.flatMap { section -> + when (section) { + is DetailSection.Songs -> section.items.map { it.toMediaItem(context, null, header(section.stringRes)) } + is DetailSection.Albums -> section.items.map { it.toMediaItem(context, null, header(section.stringRes)) } + is DetailSection.Artists -> section.items.map { it.toMediaItem(context, header(section.stringRes)) } + is DetailSection.Discs -> section.discs.flatMap { + section.discs.flatMap { entry -> + val disc = entry.key + val discString = if (disc != null) { + context.getString(R.string.fmt_disc_no, disc.number) + } else { + context.getString(R.string.def_disc) + } + entry.value.map { it.toMediaItem(context, null, header(discString)) } + } + } + else -> error("Unknown section type: $section") } - is Artist -> { - val albums = ARTIST_ALBUMS_SORT.albums(item.explicitAlbums + item.implicitAlbums) - val songs = listSettings.artistSongSort.songs(item.songs) - albums.map { it.toMediaItem(context, null, header(R.string.lbl_songs)) } + - songs.map { it.toMediaItem(context, item, header(R.string.lbl_songs)) } - } - is Genre -> { - val artists = GENRE_ARTISTS_SORT.artists(item.artists) - val songs = listSettings.genreSongSort.songs(item.songs) - artists.map { it.toMediaItem(context, header(R.string.lbl_songs)) } + - songs.map { it.toMediaItem(context, null, header(R.string.lbl_songs)) } - } - is Playlist -> { - item.songs.map { it.toMediaItem(context, null, header(R.string.lbl_songs)) } - } - is Song, - null -> return null } } - - private companion object { - // TODO: Rely on detail item gen logic? - val ARTIST_ALBUMS_SORT = Sort(Sort.Mode.ByDate, Sort.Direction.DESCENDING) - val GENRE_ARTISTS_SORT = Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING) - } }