detail: split off detail list into generator

This commit is contained in:
Alexander Capehart 2024-09-18 14:50:53 -06:00
parent f4e1681044
commit 26f27d0edd
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
6 changed files with 395 additions and 298 deletions

View file

@ -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<out MusicParent>?
fun album(uid: Music.UID): Detail<Album>?
fun artist(uid: Music.UID): Detail<Artist>?
fun genre(uid: Music.UID): Detail<Genre>?
fun playlist(uid: Music.UID): Detail<Playlist>?
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<out MusicParent>? {
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<Album>? {
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<Artist>? {
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, Collection<Album>>)[DetailSection.Albums.Category.APPEARANCES] =
artist.implicitAlbums
}
val sections = grouping.mapTo(mutableListOf<DetailSection>()) { (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<Genre>? {
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<Playlist>? {
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<P : MusicParent>(val parent: P, val sections: List<DetailSection>)
sealed interface DetailSection {
val order: Int
val stringRes: Int
abstract class PlainSection<T : Music> : DetailSection {
abstract val items: List<T>
}
data class Artists(override val items: List<Artist>) : PlainSection<Artist>() {
override val order = 0
override val stringRes = R.string.lbl_songs
}
data class Albums(val category: Category, override val items: List<Album>) : PlainSection<Album>() {
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<Song>) : PlainSection<Song>() {
override val order = 12
override val stringRes = R.string.lbl_songs
}
data class Discs(val discs: Map<Disc?, List<Song>>) : DetailSection {
override val order = 13
override val stringRes = R.string.lbl_songs
}
}

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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
}

View file

@ -18,7 +18,6 @@
package org.oxycblt.auxio.detail package org.oxycblt.auxio.detail
import androidx.annotation.StringRes
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel 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.Artist
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.MusicRepository import org.oxycblt.auxio.music.MusicRepository
import org.oxycblt.auxio.music.MusicType
import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.Playlist
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.info.ReleaseType
import org.oxycblt.auxio.music.metadata.AudioProperties import org.oxycblt.auxio.music.metadata.AudioProperties
import org.oxycblt.auxio.playback.PlaySong import org.oxycblt.auxio.playback.PlaySong
import org.oxycblt.auxio.playback.PlaybackSettings import org.oxycblt.auxio.playback.PlaybackSettings
@ -69,8 +69,12 @@ constructor(
private val listSettings: ListSettings, private val listSettings: ListSettings,
private val musicRepository: MusicRepository, private val musicRepository: MusicRepository,
private val audioPropertiesFactory: AudioProperties.Factory, private val audioPropertiesFactory: AudioProperties.Factory,
private val playbackSettings: PlaybackSettings private val playbackSettings: PlaybackSettings,
) : ViewModel(), MusicRepository.UpdateListener { detailGeneratorFactory: DetailGenerator.Factory
) : ViewModel(), DetailGenerator.Invalidator {
private val detailGenerator = detailGeneratorFactory.create(this)
private val _toShow = MutableEvent<Show>() private val _toShow = MutableEvent<Show>()
/** /**
* A [Show] command that is awaiting a view capable of responding to it. Null if none currently. * A [Show] command that is awaiting a view capable of responding to it. Null if none currently.
@ -133,13 +137,8 @@ constructor(
get() = _artistSongInstructions get() = _artistSongInstructions
/** The current [Sort] used for [Song]s in [artistSongList]. */ /** The current [Sort] used for [Song]s in [artistSongList]. */
var artistSongSort: Sort val artistSongSort: Sort
get() = listSettings.artistSongSort 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. */ /** The [PlaySong] instructions to use when playing a [Song] from [Artist] details. */
val playInArtistWith val playInArtistWith
@ -162,13 +161,8 @@ constructor(
get() = _genreSongInstructions get() = _genreSongInstructions
/** The current [Sort] used for [Song]s in [genreSongList]. */ /** The current [Sort] used for [Song]s in [genreSongList]. */
var genreSongSort: Sort val genreSongSort: Sort
get() = listSettings.genreSongSort 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. */ /** The [PlaySong] instructions to use when playing a [Song] from [Genre] details. */
val playInGenreWith val playInGenreWith
@ -204,54 +198,32 @@ constructor(
playbackSettings.inParentPlaybackMode playbackSettings.inParentPlaybackMode
?: PlaySong.FromPlaylist(unlikelyToBeNull(currentPlaylist.value)) ?: PlaySong.FromPlaylist(unlikelyToBeNull(currentPlaylist.value))
init {
musicRepository.addUpdateListener(this)
}
override fun onCleared() { override fun onCleared() {
musicRepository.removeUpdateListener(this) detailGenerator.release()
} }
override fun onMusicChanges(changes: MusicRepository.Changes) { override fun invalidate(type: MusicType, replace: Int?) {
// If we are showing any item right now, we will need to refresh it (and any information when (type) {
// related to it) with the new library in order to prevent stale items from showing up MusicType.ALBUMS -> {
// in the UI. val album = detailGenerator.album(currentAlbum.value?.uid ?: return)
val deviceLibrary = musicRepository.deviceLibrary refreshDetail(album, _currentAlbum, _albumSongList, _albumSongInstructions, replace)
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}")
} }
val album = currentAlbum.value MusicType.ARTISTS -> {
if (album != null) { val artist = detailGenerator.artist(currentArtist.value?.uid ?: return)
_currentAlbum.value = deviceLibrary.findAlbum(album.uid)?.also(::refreshAlbumList) refreshDetail(artist, _currentArtist, _artistSongList, _artistSongInstructions, replace)
logD("Updated album to ${currentAlbum.value}")
} }
val artist = currentArtist.value MusicType.GENRES -> {
if (artist != null) { val genre = detailGenerator.genre(currentGenre.value?.uid ?: return)
_currentArtist.value = refreshDetail(genre, _currentGenre, _genreSongList, _genreSongInstructions, replace)
deviceLibrary.findArtist(artist.uid)?.also(::refreshArtistList)
logD("Updated artist to ${currentArtist.value}")
} }
val genre = currentGenre.value MusicType.PLAYLISTS -> {
if (genre != null) { refreshPlaylist(currentPlaylist.value?.uid ?: return)
_currentGenre.value = deviceLibrary.findGenre(genre.uid)?.also(::refreshGenreList)
logD("Updated genre to ${currentGenre.value}")
} }
}
val userLibrary = musicRepository.userLibrary else -> error("Unexpected music type $type")
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}")
}
} }
} }
@ -356,8 +328,11 @@ constructor(
*/ */
fun setAlbum(uid: Music.UID) { fun setAlbum(uid: Music.UID) {
logD("Opening album $uid") logD("Opening album $uid")
_currentAlbum.value = if (uid === _currentAlbum.value?.uid) {
musicRepository.deviceLibrary?.findAlbum(uid)?.also(::refreshAlbumList) return
}
val album = detailGenerator.album(uid)
refreshDetail(album, _currentAlbum, _albumSongList, _albumSongInstructions, null)
if (_currentAlbum.value == null) { if (_currentAlbum.value == null) {
logW("Given album UID was invalid") logW("Given album UID was invalid")
} }
@ -370,7 +345,6 @@ constructor(
*/ */
fun applyAlbumSongSort(sort: Sort) { fun applyAlbumSongSort(sort: Sort) {
listSettings.albumSongSort = sort listSettings.albumSongSort = sort
_currentAlbum.value?.let { refreshAlbumList(it, true) }
} }
/** /**
@ -381,11 +355,11 @@ constructor(
*/ */
fun setArtist(uid: Music.UID) { fun setArtist(uid: Music.UID) {
logD("Opening artist $uid") logD("Opening artist $uid")
_currentArtist.value = if (uid === _currentArtist.value?.uid) {
musicRepository.deviceLibrary?.findArtist(uid)?.also(::refreshArtistList) return
if (_currentArtist.value == null) {
logW("Given artist UID was invalid")
} }
val artist = detailGenerator.artist(uid)
refreshDetail(artist, _currentArtist, _artistSongList, _artistSongInstructions, null)
} }
/** /**
@ -395,7 +369,6 @@ constructor(
*/ */
fun applyArtistSongSort(sort: Sort) { fun applyArtistSongSort(sort: Sort) {
listSettings.artistSongSort = sort listSettings.artistSongSort = sort
_currentArtist.value?.let { refreshArtistList(it, true) }
} }
/** /**
@ -406,11 +379,11 @@ constructor(
*/ */
fun setGenre(uid: Music.UID) { fun setGenre(uid: Music.UID) {
logD("Opening genre $uid") logD("Opening genre $uid")
_currentGenre.value = if (uid === _currentGenre.value?.uid) {
musicRepository.deviceLibrary?.findGenre(uid)?.also(::refreshGenreList) return
if (_currentGenre.value == null) {
logW("Given genre UID was invalid")
} }
val genre = detailGenerator.genre(uid)
refreshDetail(genre, _currentGenre, _genreSongList, _genreSongInstructions, null)
} }
/** /**
@ -420,7 +393,6 @@ constructor(
*/ */
fun applyGenreSongSort(sort: Sort) { fun applyGenreSongSort(sort: Sort) {
listSettings.genreSongSort = sort listSettings.genreSongSort = sort
_currentGenre.value?.let { refreshGenreList(it, true) }
} }
/** /**
@ -431,11 +403,10 @@ constructor(
*/ */
fun setPlaylist(uid: Music.UID) { fun setPlaylist(uid: Music.UID) {
logD("Opening playlist $uid") logD("Opening playlist $uid")
_currentPlaylist.value = if (uid === _currentPlaylist.value?.uid) {
musicRepository.userLibrary?.findPlaylist(uid)?.also(::refreshPlaylistList) return
if (_currentPlaylist.value == null) {
logW("Given playlist UID was invalid")
} }
refreshPlaylist(uid)
} }
/** Start a playlist editing session. Does nothing if a playlist is not being shown. */ /** Start a playlist editing session. Does nothing if a playlist is not being shown. */
@ -443,7 +414,7 @@ constructor(
val playlist = _currentPlaylist.value ?: return val playlist = _currentPlaylist.value ?: return
logD("Starting playlist edit") logD("Starting playlist edit")
_editedPlaylist.value = playlist.songs _editedPlaylist.value = playlist.songs
refreshPlaylistList(playlist) refreshPlaylist(playlist.uid)
} }
/** /**
@ -474,9 +445,8 @@ constructor(
// Nothing to do. // Nothing to do.
return false return false
} }
logD("Discarding playlist edits")
_editedPlaylist.value = null _editedPlaylist.value = null
refreshPlaylistList(playlist) refreshPlaylist(playlist.uid)
return true return true
} }
@ -488,7 +458,7 @@ constructor(
fun applyPlaylistSongSort(sort: Sort) { fun applyPlaylistSongSort(sort: Sort) {
val playlist = _currentPlaylist.value ?: return val playlist = _currentPlaylist.value ?: return
_editedPlaylist.value = sort.songs(_editedPlaylist.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]") logD("Moving playlist song from $realFrom [$from] to $realTo [$to]")
editedPlaylist.add(realFrom, editedPlaylist.removeAt(realTo)) editedPlaylist.add(realFrom, editedPlaylist.removeAt(realTo))
_editedPlaylist.value = editedPlaylist _editedPlaylist.value = editedPlaylist
refreshPlaylistList(playlist, UpdateInstructions.Move(from, to)) refreshPlaylist(playlist.uid, UpdateInstructions.Move(from, to))
return true return true
} }
@ -528,8 +498,8 @@ constructor(
logD("Removing playlist song at $realAt [$at]") logD("Removing playlist song at $realAt [$at]")
editedPlaylist.removeAt(realAt) editedPlaylist.removeAt(realAt)
_editedPlaylist.value = editedPlaylist _editedPlaylist.value = editedPlaylist
refreshPlaylistList( refreshPlaylist(
playlist, playlist.uid,
if (editedPlaylist.isNotEmpty()) { if (editedPlaylist.isNotEmpty()) {
UpdateInstructions.Remove(at, 1) UpdateInstructions.Remove(at, 1)
} else { } else {
@ -552,173 +522,69 @@ constructor(
} }
} }
private fun refreshAlbumList(album: Album, replace: Boolean = false) {
logD("Refreshing album list")
val list = mutableListOf<Item>()
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 private fun <T : MusicParent> refreshDetail(
// songs up by disc and then delimit the groups by a disc header. detail: Detail<T>?,
val songs = albumSongSort.songs(album.songs) parent: MutableStateFlow<T?>,
val byDisc = songs.groupBy { it.disc } list: MutableStateFlow<List<Item>>,
if (byDisc.size > 1) { instructions: MutableEvent<UpdateInstructions>,
logD("Album has more than one disc, interspersing headers") replace: Int?
for (entry in byDisc.entries) { ) {
list.add(DiscHeader(entry.key)) if (detail == null) {
list.addAll(entry.value) parent.value = null
} return
} else {
// Album only has one disc, don't add any redundant headers
list.addAll(songs)
} }
val newList = mutableListOf<Item>()
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") is DetailSection.Discs -> {
_albumSongInstructions.put(instructions) val header = BasicHeader(section.stringRes)
_albumSongList.value = list newList.add(Divider(header))
} newList.add(header)
section.discs.flatMap {
private fun refreshArtistList(artist: Artist, replace: Boolean = false) { listOf(DiscHeader(it.key)) + it.value
logD("Refreshing artist list") }
val list = mutableListOf<Item>()
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
}
} }
} }
// Currently only the final section (songs, which can be sorted) are invalidatable
if (artist.implicitAlbums.isNotEmpty()) { // and thus need to be replaced.
// groupByTo normally returns a mapping to a MutableList mapping. Since MutableList if (replace == -1 && i == detail.sections.lastIndex) {
// 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, Collection<Album>>)[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) {
// Intentional so that the header item isn't replaced with the songs // 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)
} }
parent.value = detail.parent
logD("Updating artist list to ${list.size} items with $instructions") list.value = newList
_artistSongInstructions.put(instructions) instructions.put(newInstructions)
_artistSongList.value = list.toList()
} }
private fun refreshGenreList(genre: Genre, replace: Boolean = false) { private fun refreshPlaylist(uid: Music.UID, instructions: UpdateInstructions = UpdateInstructions.Diff) {
logD("Refreshing genre list")
val list = mutableListOf<Item>()
// 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
) {
logD("Refreshing playlist list") 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<Item>() val list = mutableListOf<Item>()
if (edited.isNotEmpty()) {
val songs = editedPlaylist.value ?: playlist.songs
if (songs.isNotEmpty()) {
val header = EditHeader(R.string.lbl_songs) val header = EditHeader(R.string.lbl_songs)
list.add(Divider(header)) list.add(Divider(header))
list.add(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 _playlistSongList.value = list
} _playlistSongInstructions.put(instructions)
/**
* 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)
} }
} }

View file

@ -46,13 +46,12 @@ interface ListSettings : Settings<ListSettings.Listener> {
interface Listener { interface Listener {
fun onSongSortChanged() {} fun onSongSortChanged() {}
fun onAlbumSortChanged() {} fun onAlbumSortChanged() {}
fun onAlbumSongSortChanged() {}
fun onArtistSortChanged() {} fun onArtistSortChanged() {}
fun onArtistSongSortChanged() {}
fun onGenreSortChanged() {} fun onGenreSortChanged() {}
fun onGenreSongSortChanged() {}
fun onPlaylistSortChanged() {} fun onPlaylistSortChanged() {}
} }
} }
@ -162,8 +161,11 @@ class ListSettingsImpl @Inject constructor(@ApplicationContext val context: Cont
when (key) { when (key) {
getString(R.string.set_key_songs_sort) -> listener.onSongSortChanged() getString(R.string.set_key_songs_sort) -> listener.onSongSortChanged()
getString(R.string.set_key_albums_sort) -> listener.onAlbumSortChanged() 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_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_genres_sort) -> listener.onGenreSortChanged()
getString(R.string.set_key_genre_songs_sort) -> listener.onGenreSongSortChanged()
getString(R.string.set_key_playlists_sort) -> listener.onPlaylistSortChanged() getString(R.string.set_key_playlists_sort) -> listener.onPlaylistSortChanged()
} }
} }

View file

@ -130,6 +130,10 @@ fun header(@StringRes nameRes: Int): Sugar = {
MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, it.getString(nameRes)) 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 = { private fun style(style: Int): Sugar = {
putInt(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_SINGLE_ITEM, style) putInt(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_SINGLE_ITEM, style)
} }

View file

@ -22,7 +22,12 @@ import android.content.Context
import android.support.v4.media.MediaBrowserCompat.MediaItem import android.support.v4.media.MediaBrowserCompat.MediaItem
import javax.inject.Inject import javax.inject.Inject
import org.oxycblt.auxio.R 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.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.ListSettings
import org.oxycblt.auxio.list.adapter.UpdateInstructions import org.oxycblt.auxio.list.adapter.UpdateInstructions
import org.oxycblt.auxio.list.sort.Sort import org.oxycblt.auxio.list.sort.Sort
@ -42,17 +47,17 @@ private constructor(
private val invalidator: Invalidator, private val invalidator: Invalidator,
private val musicRepository: MusicRepository, private val musicRepository: MusicRepository,
private val searchEngine: SearchEngine, private val searchEngine: SearchEngine,
private val listSettings: ListSettings, homeGeneratorFactory: HomeGenerator.Factory,
homeGeneratorFactory: HomeGenerator.Factory detailGeneratorFactory: DetailGenerator.Factory
) : MusicRepository.UpdateListener, HomeGenerator.Invalidator { ) : HomeGenerator.Invalidator, DetailGenerator.Invalidator {
class Factory class Factory
@Inject @Inject
constructor( constructor(
private val musicRepository: MusicRepository, private val musicRepository: MusicRepository,
private val searchEngine: SearchEngine, 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 = fun create(context: Context, invalidator: Invalidator): MusicBrowser =
MusicBrowser( MusicBrowser(
@ -60,8 +65,8 @@ private constructor(
invalidator, invalidator,
musicRepository, musicRepository,
searchEngine, searchEngine,
listSettings, homeGeneratorFactory,
homeGeneratorFactory) detailGeneratorFactory)
} }
interface Invalidator { interface Invalidator {
@ -69,13 +74,11 @@ private constructor(
} }
private val homeGenerator = homeGeneratorFactory.create(this) private val homeGenerator = homeGeneratorFactory.create(this)
private val detailGenerator = detailGeneratorFactory.create(this)
init {
musicRepository.addUpdateListener(this)
}
fun release() { fun release() {
musicRepository.removeUpdateListener(this) homeGenerator.release()
detailGenerator.release()
} }
override fun invalidateMusic(type: MusicType, instructions: UpdateInstructions) { override fun invalidateMusic(type: MusicType, instructions: UpdateInstructions) {
@ -92,36 +95,21 @@ private constructor(
} }
} }
override fun onMusicChanges(changes: MusicRepository.Changes) { override fun invalidate(type: MusicType, replace: Int?) {
val deviceLibrary = musicRepository.deviceLibrary val deviceLibrary = musicRepository.deviceLibrary ?: return
val invalidate = mutableSetOf<String>() val userLibrary = musicRepository.userLibrary ?: return
if (changes.deviceLibrary && deviceLibrary != null) { val music = when (type) {
deviceLibrary.albums.forEach { MusicType.ALBUMS -> deviceLibrary.albums
val id = MediaSessionUID.SingleItem(it.uid).toString() MusicType.ARTISTS -> deviceLibrary.artists
invalidate.add(id) MusicType.GENRES -> deviceLibrary.genres
} MusicType.PLAYLISTS -> userLibrary.playlists
else -> return
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)
}
} }
val userLibrary = musicRepository.userLibrary if (music.isEmpty()) {
if (changes.userLibrary && userLibrary != null) { return
userLibrary.playlists.forEach {
val id = MediaSessionUID.SingleItem(it.uid).toString()
invalidate.add(id)
}
}
if (invalidate.isNotEmpty()) {
invalidator?.invalidateMusic(invalidate)
} }
val ids = music.map { MediaSessionUID.SingleItem(it.uid).toString() }.toSet()
invalidator.invalidateMusic(ids)
} }
fun getItem(mediaId: String): MediaItem? { fun getItem(mediaId: String): MediaItem? {
@ -235,34 +223,25 @@ private constructor(
} }
private fun getChildMediaItems(uid: Music.UID): List<MediaItem>? { private fun getChildMediaItems(uid: Music.UID): List<MediaItem>? {
return when (val item = musicRepository.find(uid)) { val detail = detailGenerator.any(uid) ?: return null
is Album -> { return detail.sections.flatMap { section ->
val songs = listSettings.albumSongSort.songs(item.songs) when (section) {
songs.map { it.toMediaItem(context, item, header(R.string.lbl_songs)) } 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)
}
} }