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
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue