music: simplify playlist decision events

Simplify the 4 stateflows controlling when playlist decision dialogs
must be opened to just one enum.

This is like the detail view, and makes the amount of observers I have
to spin up much smaller.

Eventually, most of even these observer calls will be collapsed into
the menu itself.
This commit is contained in:
Alexander Capehart 2023-06-27 19:43:15 -06:00
parent 07e9ca8ef6
commit 9b0e39919b
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
9 changed files with 229 additions and 80 deletions

View file

@ -62,8 +62,6 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
* high-level navigation features.
*
* @author Alexander Capehart (OxygenCobalt)
*
* TODO: Break up the god navigation setup going on here
*/
@AndroidEntryPoint
class MainFragment :

View file

@ -46,6 +46,7 @@ import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.music.PlaylistDecision
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.info.Disc
import org.oxycblt.auxio.playback.PlaybackViewModel
@ -124,6 +125,7 @@ class AlbumDetailFragment :
collectImmediately(detailModel.albumList, ::updateList)
collect(detailModel.toShow.flow, ::handleShow)
collectImmediately(selectionModel.selected, ::updateSelection)
collect(musicModel.playlistDecision.flow, ::handleDecision)
collectImmediately(
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
collect(playbackModel.artistPickerSong.flow, ::handlePlayFromArtist)
@ -298,6 +300,36 @@ class AlbumDetailFragment :
}
}
private fun updateSelection(selected: List<Music>) {
albumListAdapter.setSelected(selected.toSet())
val binding = requireBinding()
if (selected.isNotEmpty()) {
binding.detailSelectionToolbar.title = getString(R.string.fmt_selected, selected.size)
binding.detailToolbar.setVisible(R.id.detail_selection_toolbar)
} else {
binding.detailToolbar.setVisible(R.id.detail_normal_toolbar)
}
}
private fun handleDecision(decision: PlaylistDecision?) {
when (decision) {
is PlaylistDecision.Add ->{
logD("Adding ${decision.songs.size} songs to a playlist")
findNavController().navigateSafe(
AlbumDetailFragmentDirections.addToPlaylist(
decision.songs.map { it.uid }.toTypedArray())
)
musicModel.playlistDecision.consume()
}
is PlaylistDecision.New, is PlaylistDecision.Rename, is PlaylistDecision.Delete ->
error("Unexpected decision $decision")
null -> {}
}
}
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
albumListAdapter.setPlaying(
song.takeIf { parent == detailModel.currentAlbum.value }, isPlaying)
@ -352,16 +384,4 @@ class AlbumDetailFragment :
}
}
}
private fun updateSelection(selected: List<Music>) {
albumListAdapter.setSelected(selected.toSet())
val binding = requireBinding()
if (selected.isNotEmpty()) {
binding.detailSelectionToolbar.title = getString(R.string.fmt_selected, selected.size)
binding.detailToolbar.setVisible(R.id.detail_selection_toolbar)
} else {
binding.detailToolbar.setVisible(R.id.detail_normal_toolbar)
}
}
}

View file

@ -46,6 +46,7 @@ import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.music.PlaylistDecision
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.util.collect
@ -124,11 +125,12 @@ class ArtistDetailFragment :
collectImmediately(detailModel.currentArtist, ::updateArtist)
collectImmediately(detailModel.artistList, ::updateList)
collect(detailModel.toShow.flow, ::handleShow)
collectImmediately(selectionModel.selected, ::updateSelection)
collect(musicModel.playlistDecision.flow, ::handleDecision)
collectImmediately(
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
collect(playbackModel.artistPickerSong.flow, ::handlePlayFromArtist)
collect(playbackModel.genrePickerSong.flow, ::handlePlayFromGenre)
collectImmediately(selectionModel.selected, ::updateSelection)
}
override fun onDestroyBinding(binding: FragmentDetailBinding) {
@ -308,6 +310,36 @@ class ArtistDetailFragment :
}
}
private fun updateSelection(selected: List<Music>) {
artistListAdapter.setSelected(selected.toSet())
val binding = requireBinding()
if (selected.isNotEmpty()) {
binding.detailSelectionToolbar.title = getString(R.string.fmt_selected, selected.size)
binding.detailToolbar.setVisible(R.id.detail_selection_toolbar)
} else {
binding.detailToolbar.setVisible(R.id.detail_normal_toolbar)
}
}
private fun handleDecision(decision: PlaylistDecision?) {
when (decision) {
is PlaylistDecision.Add ->{
logD("Adding ${decision.songs.size} songs to a playlist")
findNavController().navigateSafe(
ArtistDetailFragmentDirections.addToPlaylist(
decision.songs.map { it.uid }.toTypedArray())
)
musicModel.playlistDecision.consume()
}
is PlaylistDecision.New, is PlaylistDecision.Rename, is PlaylistDecision.Delete ->
error("Unexpected decision $decision")
null -> {}
}
}
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
val currentArtist = unlikelyToBeNull(detailModel.currentArtist.value)
val playingItem =
@ -334,16 +366,4 @@ class ArtistDetailFragment :
logD("Launching play from genre dialog for $song")
findNavController().navigateSafe(AlbumDetailFragmentDirections.playFromGenre(song.uid))
}
private fun updateSelection(selected: List<Music>) {
artistListAdapter.setSelected(selected.toSet())
val binding = requireBinding()
if (selected.isNotEmpty()) {
binding.detailSelectionToolbar.title = getString(R.string.fmt_selected, selected.size)
binding.detailToolbar.setVisible(R.id.detail_selection_toolbar)
} else {
binding.detailToolbar.setVisible(R.id.detail_normal_toolbar)
}
}
}

View file

@ -46,6 +46,7 @@ import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.music.PlaylistDecision
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.util.collect
@ -122,11 +123,12 @@ class GenreDetailFragment :
collectImmediately(detailModel.currentGenre, ::updatePlaylist)
collectImmediately(detailModel.genreList, ::updateList)
collect(detailModel.toShow.flow, ::handleShow)
collectImmediately(selectionModel.selected, ::updateSelection)
collect(musicModel.playlistDecision.flow, ::handleDecision)
collectImmediately(
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
collect(playbackModel.artistPickerSong.flow, ::handlePlayFromArtist)
collect(playbackModel.genrePickerSong.flow, ::handlePlayFromGenre)
collectImmediately(selectionModel.selected, ::updateSelection)
}
override fun onDestroyBinding(binding: FragmentDetailBinding) {
@ -295,6 +297,36 @@ class GenreDetailFragment :
}
}
private fun updateSelection(selected: List<Music>) {
genreListAdapter.setSelected(selected.toSet())
val binding = requireBinding()
if (selected.isNotEmpty()) {
binding.detailSelectionToolbar.title = getString(R.string.fmt_selected, selected.size)
binding.detailToolbar.setVisible(R.id.detail_selection_toolbar)
} else {
binding.detailToolbar.setVisible(R.id.detail_normal_toolbar)
}
}
private fun handleDecision(decision: PlaylistDecision?) {
when (decision) {
is PlaylistDecision.Add ->{
logD("Adding ${decision.songs.size} songs to a playlist")
findNavController().navigateSafe(
GenreDetailFragmentDirections.addToPlaylist(
decision.songs.map { it.uid }.toTypedArray())
)
musicModel.playlistDecision.consume()
}
is PlaylistDecision.New, is PlaylistDecision.Rename, is PlaylistDecision.Delete ->
error("Unexpected decision $decision")
null -> {}
}
}
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
val currentGenre = unlikelyToBeNull(detailModel.currentGenre.value)
val playingItem =
@ -321,16 +353,4 @@ class GenreDetailFragment :
logD("Launching play from genre dialog for $song")
findNavController().navigateSafe(AlbumDetailFragmentDirections.playFromGenre(song.uid))
}
private fun updateSelection(selected: List<Music>) {
genreListAdapter.setSelected(selected.toSet())
val binding = requireBinding()
if (selected.isNotEmpty()) {
binding.detailSelectionToolbar.title = getString(R.string.fmt_selected, selected.size)
binding.detailToolbar.setVisible(R.id.detail_selection_toolbar)
} else {
binding.detailToolbar.setVisible(R.id.detail_normal_toolbar)
}
}
}

View file

@ -48,6 +48,7 @@ import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.music.Playlist
import org.oxycblt.auxio.music.PlaylistDecision
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.util.collect
@ -137,11 +138,12 @@ class PlaylistDetailFragment :
collectImmediately(detailModel.playlistList, ::updateList)
collectImmediately(detailModel.editedPlaylist, ::updateEditedList)
collect(detailModel.toShow.flow, ::handleShow)
collectImmediately(selectionModel.selected, ::updateSelection)
collect(musicModel.playlistDecision.flow, ::handleDecision)
collectImmediately(
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
collect(playbackModel.artistPickerSong.flow, ::handlePlayFromArtist)
collect(playbackModel.genrePickerSong.flow, ::handlePlayFromGenre)
collectImmediately(selectionModel.selected, ::updateSelection)
}
override fun onStart() {
@ -342,6 +344,38 @@ class PlaylistDetailFragment :
}
}
private fun updateSelection(selected: List<Music>) {
playlistListAdapter.setSelected(selected.toSet())
val binding = requireBinding()
if (selected.isNotEmpty()) {
binding.detailSelectionToolbar.title = getString(R.string.fmt_selected, selected.size)
}
updateMultiToolbar()
}
private fun handleDecision(decision: PlaylistDecision?) {
if (decision == null) return
when (decision) {
is PlaylistDecision.Rename -> {
logD("Renaming ${decision.playlist}")
findNavController().navigateSafe(
PlaylistDetailFragmentDirections.renamePlaylist(decision.playlist.uid)
)
}
is PlaylistDecision.Delete -> {
logD("Deleting ${decision.playlist}")
findNavController().navigateSafe(
PlaylistDetailFragmentDirections.deletePlaylist(decision.playlist.uid)
)
}
is PlaylistDecision.Add, is PlaylistDecision.New -> error("Unexpected decision $decision")
}
musicModel.playlistDecision.consume()
}
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
// Prefer songs that are playing from this playlist.
playlistListAdapter.setPlaying(
@ -359,17 +393,6 @@ class PlaylistDetailFragment :
logD("Launching play from genre dialog for $song")
findNavController().navigateSafe(AlbumDetailFragmentDirections.playFromGenre(song.uid))
}
private fun updateSelection(selected: List<Music>) {
playlistListAdapter.setSelected(selected.toSet())
val binding = requireBinding()
if (selected.isNotEmpty()) {
binding.detailSelectionToolbar.title = getString(R.string.fmt_selected, selected.size)
}
updateMultiToolbar()
}
private fun updateMultiToolbar() {
val id =
when {

View file

@ -130,6 +130,7 @@ class SongDetailDialog : ViewBindingDialogFragment<DialogSongDetailBinding>() {
if (show == null) return
if (show is Show.SongDetails) {
logD("Navigated to this song")
detailModel.toShow.consume()
} else {
error("Unexpected show command $show")
}

View file

@ -65,6 +65,7 @@ import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.music.NoAudioPermissionException
import org.oxycblt.auxio.music.NoMusicException
import org.oxycblt.auxio.music.PERMISSION_READ_AUDIO
import org.oxycblt.auxio.music.PlaylistDecision
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.util.collect
@ -85,9 +86,9 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
@AndroidEntryPoint
class HomeFragment :
SelectionFragment<FragmentHomeBinding>(), AppBarLayout.OnOffsetChangedListener {
override val playbackModel: PlaybackViewModel by activityViewModels()
override val selectionModel: SelectionViewModel by activityViewModels()
override val musicModel: MusicViewModel by activityViewModels()
override val playbackModel: PlaybackViewModel by activityViewModels()
private val homeModel: HomeViewModel by activityViewModels()
private val detailModel: DetailViewModel by activityViewModels()
private var storagePermissionLauncher: ActivityResultLauncher<String>? = null
@ -171,9 +172,10 @@ class HomeFragment :
collect(homeModel.recreateTabs.flow, ::handleRecreate)
collectImmediately(homeModel.currentTabMode, ::updateCurrentTab)
collectImmediately(homeModel.songsList, homeModel.isFastScrolling, ::updateFab)
collectImmediately(musicModel.indexingState, ::updateIndexerState)
collect(detailModel.toShow.flow, ::handleShow)
collectImmediately(selectionModel.selected, ::updateSelection)
collectImmediately(musicModel.indexingState, ::updateIndexerState)
collect(musicModel.playlistDecision.flow, ::handleDecision)
collect(detailModel.toShow.flow, ::handleShow)
}
override fun onSaveInstanceState(outState: Bundle) {
@ -479,6 +481,39 @@ class HomeFragment :
}
}
private fun handleDecision(decision: PlaylistDecision?) {
if (decision == null) return
when (decision) {
is PlaylistDecision.New -> {
logD("Creating new playlist")
findNavController().navigateSafe(
HomeFragmentDirections.newPlaylist(decision.songs.map { it.uid }.toTypedArray()))
}
is PlaylistDecision.Rename -> {
logD("Renaming ${decision.playlist}")
findNavController().navigateSafe(
HomeFragmentDirections.renamePlaylist(decision.playlist.uid)
)
}
is PlaylistDecision.Delete -> {
logD("Deleting ${decision.playlist}")
findNavController().navigateSafe(
HomeFragmentDirections.deletePlaylist(decision.playlist.uid)
)
}
is PlaylistDecision.Add -> {
logD("Adding ${decision.songs.size} to a playlist")
findNavController().navigateSafe(
HomeFragmentDirections.addToPlaylist(decision.songs.map { it.uid }.toTypedArray())
)
}
}
musicModel.playlistDecision.consume()
}
private fun updateFab(songs: List<Song>, isFastScrolling: Boolean) {
val binding = requireBinding()
// If there are no songs, it's likely that the library has not been loaded, so

View file

@ -52,23 +52,8 @@ constructor(
val statistics: StateFlow<Statistics?>
get() = _statistics
private val _newPlaylistSongs = MutableEvent<List<Song>>()
/** Flag for opening a dialog to create a playlist of the given [Song]s. */
val newPlaylistSongs: Event<List<Song>> = _newPlaylistSongs
private val _playlistToRename = MutableEvent<Playlist?>()
/** Flag for opening a dialog to rename the given [Playlist]. */
val playlistToRename: Event<Playlist?>
get() = _playlistToRename
private val _playlistToDelete = MutableEvent<Playlist>()
/** Flag for opening a dialog to confirm deletion of the given [Playlist]. */
val playlistToDelete: Event<Playlist>
get() = _playlistToDelete
private val _songsToAdd = MutableEvent<List<Song>>()
/** Flag for opening a dialog to add the given [Song]s to a playlist. */
val songsToAdd: Event<List<Song>> = _songsToAdd
private val _playlistDecision = MutableEvent<PlaylistDecision>()
val playlistDecision: Event<PlaylistDecision> get() = _playlistDecision
init {
musicRepository.addUpdateListener(this)
@ -121,7 +106,7 @@ constructor(
viewModelScope.launch(Dispatchers.IO) { musicRepository.createPlaylist(name, songs) }
} else {
logD("Launching creation dialog for ${songs.size} songs")
_newPlaylistSongs.put(songs)
_playlistDecision.put(PlaylistDecision.New(songs))
}
}
@ -137,7 +122,7 @@ constructor(
viewModelScope.launch(Dispatchers.IO) { musicRepository.renamePlaylist(playlist, name) }
} else {
logD("Launching rename dialog for $playlist")
_playlistToRename.put(playlist)
_playlistDecision.put(PlaylistDecision.Rename(playlist))
}
}
@ -154,7 +139,7 @@ constructor(
viewModelScope.launch(Dispatchers.IO) { musicRepository.deletePlaylist(playlist) }
} else {
logD("Launching deletion dialog for $playlist")
_playlistToDelete.put(playlist)
_playlistDecision.put(PlaylistDecision.Delete(playlist))
}
}
@ -214,7 +199,7 @@ constructor(
viewModelScope.launch(Dispatchers.IO) { musicRepository.addToPlaylist(songs, playlist) }
} else {
logD("Launching addition dialog for songs=${songs.size}")
_songsToAdd.put(songs)
_playlistDecision.put(PlaylistDecision.Add(songs))
}
}
@ -235,3 +220,10 @@ constructor(
val durationMs: Long
)
}
sealed interface PlaylistDecision {
data class New(val songs: List<Song>) : PlaylistDecision
data class Rename(val playlist: Playlist) : PlaylistDecision
data class Delete(val playlist: Playlist) : PlaylistDecision
data class Add(val songs: List<Song>) : PlaylistDecision
}

View file

@ -34,8 +34,10 @@ import com.google.android.material.transition.MaterialSharedAxis
import dagger.hilt.android.AndroidEntryPoint
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentSearchBinding
import org.oxycblt.auxio.detail.ArtistDetailFragmentDirections
import org.oxycblt.auxio.detail.DetailViewModel
import org.oxycblt.auxio.detail.Show
import org.oxycblt.auxio.home.HomeFragmentDirections
import org.oxycblt.auxio.list.Divider
import org.oxycblt.auxio.list.Header
import org.oxycblt.auxio.list.Item
@ -48,6 +50,7 @@ import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.music.Playlist
import org.oxycblt.auxio.music.PlaylistDecision
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.util.collect
@ -136,10 +139,11 @@ class SearchFragment : ListFragment<Music, FragmentSearchBinding>() {
// --- VIEWMODEL SETUP ---
collectImmediately(searchModel.searchResults, ::updateSearchResults)
collectImmediately(selectionModel.selected, ::updateSelection)
collect(musicModel.playlistDecision.flow, ::handleDecision)
collectImmediately(
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
collect(detailModel.toShow.flow, ::handleShow)
collectImmediately(selectionModel.selected, ::updateSelection)
}
override fun onDestroyBinding(binding: FragmentSearchBinding) {
@ -200,9 +204,7 @@ class SearchFragment : ListFragment<Music, FragmentSearchBinding>() {
}
}
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
searchAdapter.setPlaying(parent ?: song, isPlaying)
}
private fun handleShow(show: Show?) {
when (show) {
@ -257,6 +259,44 @@ class SearchFragment : ListFragment<Music, FragmentSearchBinding>() {
hideKeyboard()
}
private fun handleDecision(decision: PlaylistDecision?) {
if (decision == null) return
when (decision) {
is PlaylistDecision.New -> {
logD("Creating new playlist")
findNavController().navigateSafe(
HomeFragmentDirections.newPlaylist(decision.songs.map { it.uid }.toTypedArray()))
}
is PlaylistDecision.Rename -> {
logD("Renaming ${decision.playlist}")
findNavController().navigateSafe(
HomeFragmentDirections.renamePlaylist(decision.playlist.uid)
)
}
is PlaylistDecision.Delete -> {
logD("Deleting ${decision.playlist}")
findNavController().navigateSafe(
SearchFragmentDirections.deletePlaylist(decision.playlist.uid)
)
}
is PlaylistDecision.Add -> {
logD("Adding ${decision.songs.size} to a playlist")
findNavController().navigateSafe(
HomeFragmentDirections.addToPlaylist(decision.songs.map { it.uid }.toTypedArray())
)
}
}
musicModel.playlistDecision.consume()
}
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
searchAdapter.setPlaying(parent ?: song, isPlaying)
}
private fun updateSelection(selected: List<Music>) {
searchAdapter.setSelected(selected.toSet())
val binding = requireBinding()