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
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<Show>()
/**
* 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<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
// 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 <T : MusicParent> refreshDetail(
detail: Detail<T>?,
parent: MutableStateFlow<T?>,
list: MutableStateFlow<List<Item>>,
instructions: MutableEvent<UpdateInstructions>,
replace: Int?
) {
if (detail == null) {
parent.value = null
return
}
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")
_albumSongInstructions.put(instructions)
_albumSongList.value = list
}
private fun refreshArtistList(artist: Artist, replace: Boolean = false) {
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
}
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, 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) {
// 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<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
) {
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<Item>()
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)
}
}

View file

@ -46,13 +46,12 @@ interface ListSettings : Settings<ListSettings.Listener> {
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()
}
}

View file

@ -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)
}

View file

@ -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<String>()
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<MediaItem>? {
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)
}
}