detail: split off detail list into generator
This commit is contained in:
parent
f4e1681044
commit
26f27d0edd
6 changed files with 395 additions and 298 deletions
216
app/src/main/java/org/oxycblt/auxio/detail/DetailGenerator.kt
Normal file
216
app/src/main/java/org/oxycblt/auxio/detail/DetailGenerator.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
30
app/src/main/java/org/oxycblt/auxio/detail/DetailModule.kt
Normal file
30
app/src/main/java/org/oxycblt/auxio/detail/DetailModule.kt
Normal 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
|
||||||
|
}
|
|
@ -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))
|
|
||||||
list.addAll(entry.value)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Album only has one disc, don't add any redundant headers
|
|
||||||
list.addAll(songs)
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
// Intentional so that the header item isn't replaced with the songs
|
|
||||||
instructions = UpdateInstructions.Replace(list.size)
|
|
||||||
}
|
|
||||||
list.addAll(artistSongSort.songs(artist.songs))
|
|
||||||
}
|
|
||||||
|
|
||||||
logD("Updating artist list to ${list.size} items with $instructions")
|
|
||||||
_artistSongInstructions.put(instructions)
|
|
||||||
_artistSongList.value = list.toList()
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
) {
|
) {
|
||||||
logD("Refreshing playlist list")
|
if (detail == null) {
|
||||||
val list = mutableListOf<Item>()
|
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
|
||||||
|
}
|
||||||
|
|
||||||
val songs = editedPlaylist.value ?: playlist.songs
|
is DetailSection.Discs -> {
|
||||||
if (songs.isNotEmpty()) {
|
val header = BasicHeader(section.stringRes)
|
||||||
|
newList.add(Divider(header))
|
||||||
|
newList.add(header)
|
||||||
|
section.discs.flatMap {
|
||||||
|
listOf(DiscHeader(it.key)) + it.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 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
|
||||||
|
newInstructions = UpdateInstructions.Replace(newList.size)
|
||||||
|
}
|
||||||
|
newList.addAll(items)
|
||||||
|
}
|
||||||
|
parent.value = detail.parent
|
||||||
|
list.value = newList
|
||||||
|
instructions.put(newInstructions)
|
||||||
|
}
|
||||||
|
|
||||||
|
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>()
|
||||||
|
if (edited.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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
if (music.isEmpty()) {
|
||||||
deviceLibrary.artists.forEach {
|
return
|
||||||
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 (changes.userLibrary && userLibrary != null) {
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
is Artist -> {
|
entry.value.map { it.toMediaItem(context, null, header(discString)) }
|
||||||
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)) } +
|
else -> error("Unknown section type: $section")
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue