music: add more playlist messages

Add more types of playlist messages corresponding to other actions, so
they can be indicated in the UI only when the process is complete.

This is somewhat incomplete. It does not include indicating errors for
other playlist operations (Which I want to do), and neither does it
handle situations in which some playlist operations and up reducing
to others (i.e import -> create). I need to do that later.
This commit is contained in:
Alexander Capehart 2023-12-23 22:00:38 -07:00
parent 21970349cc
commit c5a3f72b99
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
13 changed files with 128 additions and 54 deletions

View file

@ -44,6 +44,7 @@ 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.PlaylistMessage
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.info.Disc
import org.oxycblt.auxio.playback.PlaybackDecision
@ -55,6 +56,7 @@ import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.navigateSafe
import org.oxycblt.auxio.util.overrideOnOverflowMenuClick
import org.oxycblt.auxio.util.setFullWidthLookup
import org.oxycblt.auxio.util.showToast
import org.oxycblt.auxio.util.unlikelyToBeNull
/**
@ -126,6 +128,7 @@ class AlbumDetailFragment :
collect(listModel.menu.flow, ::handleMenu)
collectImmediately(listModel.selected, ::updateSelection)
collect(musicModel.playlistDecision.flow, ::handlePlaylistDecision)
collect(musicModel.playlistMessage.flow, ::handlePlaylistMessage)
collectImmediately(
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
collect(playbackModel.playbackDecision.flow, ::handlePlaybackDecision)
@ -281,6 +284,12 @@ class AlbumDetailFragment :
findNavController().navigateSafe(directions)
}
private fun handlePlaylistMessage(message: PlaylistMessage?) {
if (message == null) return
requireContext().showToast(message.stringRes)
musicModel.playlistMessage.consume()
}
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
albumListAdapter.setPlaying(
song.takeIf { parent == detailModel.currentAlbum.value }, isPlaying)

View file

@ -45,6 +45,7 @@ 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.PlaylistMessage
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.PlaybackDecision
import org.oxycblt.auxio.playback.PlaybackViewModel
@ -54,6 +55,7 @@ import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.navigateSafe
import org.oxycblt.auxio.util.overrideOnOverflowMenuClick
import org.oxycblt.auxio.util.setFullWidthLookup
import org.oxycblt.auxio.util.showToast
import org.oxycblt.auxio.util.unlikelyToBeNull
/**
@ -128,6 +130,7 @@ class ArtistDetailFragment :
collect(listModel.menu.flow, ::handleMenu)
collectImmediately(listModel.selected, ::updateSelection)
collect(musicModel.playlistDecision.flow, ::handlePlaylistDecision)
collect(musicModel.playlistMessage.flow, ::handlePlaylistMessage)
collectImmediately(
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
collect(playbackModel.playbackDecision.flow, ::handlePlaybackDecision)
@ -284,6 +287,12 @@ class ArtistDetailFragment :
findNavController().navigateSafe(directions)
}
private fun handlePlaylistMessage(message: PlaylistMessage?) {
if (message == null) return
requireContext().showToast(message.stringRes)
musicModel.playlistMessage.consume()
}
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
val currentArtist = unlikelyToBeNull(detailModel.currentArtist.value)
val playingItem =

View file

@ -45,6 +45,7 @@ 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.PlaylistMessage
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.PlaybackDecision
import org.oxycblt.auxio.playback.PlaybackViewModel
@ -54,6 +55,7 @@ import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.navigateSafe
import org.oxycblt.auxio.util.overrideOnOverflowMenuClick
import org.oxycblt.auxio.util.setFullWidthLookup
import org.oxycblt.auxio.util.showToast
import org.oxycblt.auxio.util.unlikelyToBeNull
/**
@ -125,7 +127,8 @@ class GenreDetailFragment :
collect(detailModel.toShow.flow, ::handleShow)
collect(listModel.menu.flow, ::handleMenu)
collectImmediately(listModel.selected, ::updateSelection)
collect(musicModel.playlistDecision.flow, ::handleDecision)
collect(musicModel.playlistDecision.flow, ::handlePlaylistDecision)
collect(musicModel.playlistMessage.flow, ::handlePlaylistMessage)
collectImmediately(
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
collect(playbackModel.playbackDecision.flow, ::handlePlaybackDecision)
@ -259,7 +262,7 @@ class GenreDetailFragment :
}
}
private fun handleDecision(decision: PlaylistDecision?) {
private fun handlePlaylistDecision(decision: PlaylistDecision?) {
if (decision == null) return
val directions =
when (decision) {
@ -277,6 +280,12 @@ class GenreDetailFragment :
findNavController().navigateSafe(directions)
}
private fun handlePlaylistMessage(message: PlaylistMessage?) {
if (message == null) return
requireContext().showToast(message.stringRes)
musicModel.playlistMessage.consume()
}
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
val currentGenre = unlikelyToBeNull(detailModel.currentGenre.value)
val playingItem =

View file

@ -49,6 +49,7 @@ 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.PlaylistMessage
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.external.M3U
import org.oxycblt.auxio.playback.PlaybackDecision
@ -61,6 +62,7 @@ import org.oxycblt.auxio.util.logW
import org.oxycblt.auxio.util.navigateSafe
import org.oxycblt.auxio.util.overrideOnOverflowMenuClick
import org.oxycblt.auxio.util.setFullWidthLookup
import org.oxycblt.auxio.util.showToast
import org.oxycblt.auxio.util.unlikelyToBeNull
/**
@ -159,7 +161,8 @@ class PlaylistDetailFragment :
collect(detailModel.toShow.flow, ::handleShow)
collect(listModel.menu.flow, ::handleMenu)
collectImmediately(listModel.selected, ::updateSelection)
collect(musicModel.playlistDecision.flow, ::handleDecision)
collect(musicModel.playlistDecision.flow, ::handlePlaylistDecision)
collect(musicModel.playlistMessage.flow, ::handlePlaylistMessage)
collectImmediately(
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
collect(playbackModel.playbackDecision.flow, ::handlePlaybackDecision)
@ -333,7 +336,7 @@ class PlaylistDetailFragment :
updateMultiToolbar()
}
private fun handleDecision(decision: PlaylistDecision?) {
private fun handlePlaylistDecision(decision: PlaylistDecision?) {
if (decision == null) return
val directions =
when (decision) {
@ -369,6 +372,12 @@ class PlaylistDetailFragment :
findNavController().navigateSafe(directions)
}
private fun handlePlaylistMessage(message: PlaylistMessage?) {
if (message == null) return
requireContext().showToast(message.stringRes)
musicModel.playlistMessage.consume()
}
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
// Prefer songs that are playing from this playlist.
playlistListAdapter.setPlaying(

View file

@ -70,7 +70,7 @@ import org.oxycblt.auxio.music.NoMusicException
import org.oxycblt.auxio.music.PERMISSION_READ_AUDIO
import org.oxycblt.auxio.music.Playlist
import org.oxycblt.auxio.music.PlaylistDecision
import org.oxycblt.auxio.music.PlaylistError
import org.oxycblt.auxio.music.PlaylistMessage
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.external.M3U
import org.oxycblt.auxio.playback.PlaybackViewModel
@ -211,7 +211,7 @@ class HomeFragment :
collectImmediately(listModel.selected, ::updateSelection)
collectImmediately(musicModel.indexingState, ::updateIndexerState)
collect(musicModel.playlistDecision.flow, ::handleDecision)
collectImmediately(musicModel.playlistError.flow, ::handlePlaylistError)
collectImmediately(musicModel.playlistMessage.flow, ::handlePlaylistMessage)
collect(detailModel.toShow.flow, ::handleShow)
}
@ -503,19 +503,10 @@ class HomeFragment :
findNavController().navigateSafe(directions)
}
private fun handlePlaylistError(error: PlaylistError?) {
when (error) {
is PlaylistError.ImportFailed -> {
requireContext().showToast(R.string.err_import_failed)
musicModel.importError.consume()
}
is PlaylistError.ExportFailed -> {
requireContext().showToast(R.string.err_export_failed)
musicModel.importError.consume()
}
null -> {}
}
musicModel.playlistError.consume()
private fun handlePlaylistMessage(message: PlaylistMessage?) {
if (message == null) return
requireContext().showToast(message.stringRes)
musicModel.playlistMessage.consume()
}
private fun updateFab(songs: List<Song>, isFastScrolling: Boolean) {

View file

@ -27,6 +27,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import org.oxycblt.auxio.R
import org.oxycblt.auxio.list.ListSettings
import org.oxycblt.auxio.music.external.ExportConfig
import org.oxycblt.auxio.music.external.ExternalPlaylistManager
@ -65,19 +66,9 @@ constructor(
val playlistDecision: Event<PlaylistDecision>
get() = _playlistDecision
private val _playlistError = MutableEvent<PlaylistError>()
val playlistError: Event<PlaylistError>
get() = _playlistError
private val _importError = MutableEvent<Unit>()
/** Flag for when playlist importing failed. Consume this and show an error if active. */
val importError: Event<Unit>
get() = _importError
private val _exportError = MutableEvent<Unit>()
/** Flag for when playlist exporting failed. Consume this and show an error if active. */
val exportError: Event<Unit>
get() = _exportError
private val _playlistMessage = MutableEvent<PlaylistMessage>()
val playlistMessage: Event<PlaylistMessage>
get() = _playlistMessage
init {
musicRepository.addUpdateListener(this)
@ -127,7 +118,10 @@ constructor(
fun createPlaylist(name: String? = null, songs: List<Song> = listOf()) {
if (name != null) {
logD("Creating $name with ${songs.size} songs]")
viewModelScope.launch(Dispatchers.IO) { musicRepository.createPlaylist(name, songs) }
viewModelScope.launch(Dispatchers.IO) {
musicRepository.createPlaylist(name, songs)
_playlistMessage.put(PlaylistMessage.NewPlaylistSuccess)
}
} else {
logD("Launching creation dialog for ${songs.size} songs")
_playlistDecision.put(PlaylistDecision.New(songs))
@ -148,7 +142,7 @@ constructor(
viewModelScope.launch(Dispatchers.IO) {
val importedPlaylist = externalPlaylistManager.import(uri)
if (importedPlaylist == null) {
_playlistError.put(PlaylistError.ImportFailed)
_playlistMessage.put(PlaylistMessage.ImportFailed)
return@launch
}
@ -156,14 +150,16 @@ constructor(
val songs = importedPlaylist.paths.mapNotNull(deviceLibrary::findSongByPath)
if (songs.isEmpty()) {
_playlistError.put(PlaylistError.ImportFailed)
_playlistMessage.put(PlaylistMessage.ImportFailed)
return@launch
}
// TODO Require the user to name it something else if the name is a duplicate of
// a prior playlist
if (target !== null) {
musicRepository.rewritePlaylist(target, songs)
_playlistMessage.put(PlaylistMessage.ImportSuccess)
} else {
// TODO: Have to properly propagate the "Playlist Created" message
createPlaylist(importedPlaylist.name, songs)
}
}
@ -183,8 +179,10 @@ constructor(
if (uri != null && config != null) {
logD("Exporting playlist to $uri")
viewModelScope.launch(Dispatchers.IO) {
if (!externalPlaylistManager.export(playlist, uri, config)) {
_playlistError.put(PlaylistError.ExportFailed)
if (externalPlaylistManager.export(playlist, uri, config)) {
_playlistMessage.put(PlaylistMessage.ExportSuccess)
} else {
_playlistMessage.put(PlaylistMessage.ExportFailed)
}
}
} else {
@ -202,7 +200,10 @@ constructor(
fun renamePlaylist(playlist: Playlist, name: String? = null) {
if (name != null) {
logD("Renaming $playlist to $name")
viewModelScope.launch(Dispatchers.IO) { musicRepository.renamePlaylist(playlist, name) }
viewModelScope.launch(Dispatchers.IO) {
musicRepository.renamePlaylist(playlist, name)
_playlistMessage.put(PlaylistMessage.RenameSuccess)
}
} else {
logD("Launching rename dialog for $playlist")
_playlistDecision.put(PlaylistDecision.Rename(playlist))
@ -219,7 +220,10 @@ constructor(
fun deletePlaylist(playlist: Playlist, rude: Boolean = false) {
if (rude) {
logD("Deleting $playlist")
viewModelScope.launch(Dispatchers.IO) { musicRepository.deletePlaylist(playlist) }
viewModelScope.launch(Dispatchers.IO) {
musicRepository.deletePlaylist(playlist)
_playlistMessage.put(PlaylistMessage.DeleteSuccess)
}
} else {
logD("Launching deletion dialog for $playlist")
_playlistDecision.put(PlaylistDecision.Delete(playlist))
@ -279,7 +283,10 @@ constructor(
fun addToPlaylist(songs: List<Song>, playlist: Playlist? = null) {
if (playlist != null) {
logD("Adding ${songs.size} songs to $playlist")
viewModelScope.launch(Dispatchers.IO) { musicRepository.addToPlaylist(songs, playlist) }
viewModelScope.launch(Dispatchers.IO) {
musicRepository.addToPlaylist(songs, playlist)
_playlistMessage.put(PlaylistMessage.AddSuccess)
}
} else {
logD("Launching addition dialog for songs=${songs.size}")
_playlistDecision.put(PlaylistDecision.Add(songs))
@ -354,8 +361,46 @@ sealed interface PlaylistDecision {
data class Add(val songs: List<Song>) : PlaylistDecision
}
sealed interface PlaylistError {
data object ImportFailed : PlaylistError
sealed interface PlaylistMessage {
val stringRes: Int
data object ExportFailed : PlaylistError
data object NewPlaylistSuccess : PlaylistMessage {
override val stringRes: Int
get() = R.string.lng_playlist_created
}
data object ImportSuccess : PlaylistMessage {
override val stringRes: Int
get() = R.string.lng_playlist_imported
}
data object ImportFailed : PlaylistMessage {
override val stringRes: Int
get() = R.string.err_import_failed
}
data object RenameSuccess : PlaylistMessage {
override val stringRes: Int
get() = R.string.lng_playlist_renamed
}
data object DeleteSuccess : PlaylistMessage {
override val stringRes: Int
get() = R.string.lng_playlist_deleted
}
data object AddSuccess : PlaylistMessage {
override val stringRes: Int
get() = R.string.lng_playlist_added
}
data object ExportSuccess : PlaylistMessage {
override val stringRes: Int
get() = R.string.lng_playlist_exported
}
data object ExportFailed : PlaylistMessage {
override val stringRes: Int
get() = R.string.err_export_failed
}
}

View file

@ -37,7 +37,6 @@ import org.oxycblt.auxio.ui.ViewBindingMaterialDialogFragment
import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.navigateSafe
import org.oxycblt.auxio.util.showToast
/**
* A dialog that allows the user to pick a specific playlist to add song(s) to.
@ -86,7 +85,6 @@ class AddToPlaylistDialog :
override fun onClick(item: PlaylistChoice, viewHolder: RecyclerView.ViewHolder) {
musicModel.addToPlaylist(pickerModel.currentSongsToAdd.value ?: return, item.playlist)
requireContext().showToast(R.string.lng_playlist_added)
findNavController().navigateUp()
}

View file

@ -33,7 +33,6 @@ import org.oxycblt.auxio.music.Playlist
import org.oxycblt.auxio.ui.ViewBindingMaterialDialogFragment
import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.showToast
import org.oxycblt.auxio.util.unlikelyToBeNull
/**
@ -56,7 +55,6 @@ class DeletePlaylistDialog : ViewBindingMaterialDialogFragment<DialogDeletePlayl
// Now we can delete the playlist for-real this time.
musicModel.deletePlaylist(
unlikelyToBeNull(pickerModel.currentPlaylistToDelete.value), rude = true)
requireContext().showToast(R.string.lng_playlist_deleted)
}
.setNegativeButton(R.string.lbl_cancel, null)
}

View file

@ -98,7 +98,6 @@ class ExportPlaylistDialog : ViewBindingMaterialDialogFragment<DialogPlaylistExp
binding.exportWindowsPaths.setOnClickListener { _ ->
val current = pickerModel.currentExportConfig.value
logD("change")
pickerModel.setExportConfig(current.copy(windowsPaths = !current.windowsPaths))
}

View file

@ -33,7 +33,6 @@ import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.ui.ViewBindingMaterialDialogFragment
import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.showToast
import org.oxycblt.auxio.util.unlikelyToBeNull
/**
@ -62,7 +61,6 @@ class NewPlaylistDialog : ViewBindingMaterialDialogFragment<DialogPlaylistNameBi
}
// TODO: Navigate to playlist if there are songs in it
musicModel.createPlaylist(name, pendingPlaylist.songs)
requireContext().showToast(R.string.lng_playlist_created)
findNavController().apply {
navigateUp()
// Do an additional navigation away from the playlist addition dialog, if

View file

@ -34,7 +34,6 @@ import org.oxycblt.auxio.music.Playlist
import org.oxycblt.auxio.ui.ViewBindingMaterialDialogFragment
import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.showToast
import org.oxycblt.auxio.util.unlikelyToBeNull
/**
@ -58,7 +57,6 @@ class RenamePlaylistDialog : ViewBindingMaterialDialogFragment<DialogPlaylistNam
val playlist = unlikelyToBeNull(pickerModel.currentPlaylistToRename.value)
val chosenName = pickerModel.chosenName.value as ChosenName.Valid
musicModel.renamePlaylist(playlist, chosenName.value)
requireContext().showToast(R.string.lng_playlist_renamed)
findNavController().navigateUp()
}
.setNegativeButton(R.string.lbl_cancel, null)

View file

@ -52,6 +52,7 @@ 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.PlaylistMessage
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.external.M3U
import org.oxycblt.auxio.playback.PlaybackDecision
@ -64,6 +65,7 @@ import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logW
import org.oxycblt.auxio.util.navigateSafe
import org.oxycblt.auxio.util.setFullWidthLookup
import org.oxycblt.auxio.util.showToast
/**
* The [ListFragment] providing search functionality for the music library.
@ -160,7 +162,8 @@ class SearchFragment : ListFragment<Music, FragmentSearchBinding>() {
collectImmediately(searchModel.searchResults, ::updateSearchResults)
collectImmediately(listModel.selected, ::updateSelection)
collect(listModel.menu.flow, ::handleMenu)
collect(musicModel.playlistDecision.flow, ::handleDecision)
collect(musicModel.playlistDecision.flow, ::handlePlaylistDecision)
collect(musicModel.playlistMessage.flow, ::handlePlaylistMessage)
collectImmediately(
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
collect(playbackModel.playbackDecision.flow, ::handlePlaybackDecision)
@ -302,7 +305,7 @@ class SearchFragment : ListFragment<Music, FragmentSearchBinding>() {
}
}
private fun handleDecision(decision: PlaylistDecision?) {
private fun handlePlaylistDecision(decision: PlaylistDecision?) {
if (decision == null) return
val directions =
when (decision) {
@ -340,6 +343,12 @@ class SearchFragment : ListFragment<Music, FragmentSearchBinding>() {
findNavController().navigateSafe(directions)
}
private fun handlePlaylistMessage(message: PlaylistMessage?) {
if (message == null) return
requireContext().showToast(message.stringRes)
musicModel.playlistMessage.consume()
}
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
searchAdapter.setPlaying(parent ?: song, isPlaying)
}

View file

@ -196,7 +196,9 @@
<string name="lng_observing">Monitoring your music library for changes…</string>
<string name="lng_queue_added">Added to queue</string>
<string name="lng_playlist_created">Playlist created</string>
<string name="lng_playlist_imported">Playlist imported</string>
<string name="lng_playlist_renamed">Playlist renamed</string>
<string name="lng_playlist_exported">Playlist exported</string>
<string name="lng_playlist_deleted">Playlist deleted</string>
<string name="lng_playlist_added">Added to playlist</string>
<string name="lng_author">Developed by Alexander Capehart</string>