music: add playlist deletion dialog
Add a dialog that allows the user to confirm playlist deletion, instead of it happening immediately.
This commit is contained in:
parent
d1f9200bf9
commit
97e144058a
13 changed files with 180 additions and 41 deletions
|
@ -41,6 +41,7 @@ import org.oxycblt.auxio.databinding.FragmentMainBinding
|
|||
import org.oxycblt.auxio.list.selection.SelectionViewModel
|
||||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.music.MusicViewModel
|
||||
import org.oxycblt.auxio.music.Playlist
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.navigation.MainNavigationAction
|
||||
import org.oxycblt.auxio.navigation.NavigationViewModel
|
||||
|
@ -136,6 +137,7 @@ class MainFragment :
|
|||
collect(navModel.exploreArtistNavigationItem.flow, ::handleArtistNavigationPicker)
|
||||
collect(musicModel.newPlaylistSongs.flow, ::handleNewPlaylist)
|
||||
collect(musicModel.songsToAdd.flow, ::handleAddToPlaylist)
|
||||
collect(musicModel.playlistToDelete.flow, ::handleDeletePlaylist)
|
||||
collectImmediately(playbackModel.song, ::updateSong)
|
||||
collect(playbackModel.artistPickerSong.flow, ::handlePlaybackArtistPicker)
|
||||
collect(playbackModel.genrePickerSong.flow, ::handlePlaybackGenrePicker)
|
||||
|
@ -322,6 +324,13 @@ class MainFragment :
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleDeletePlaylist(playlist: Playlist?) {
|
||||
if (playlist != null) {
|
||||
findNavController()
|
||||
.navigateSafe(MainFragmentDirections.actionDeletePlaylist(playlist.uid))
|
||||
musicModel.playlistToDelete.consume()
|
||||
}
|
||||
}
|
||||
private fun handlePlaybackArtistPicker(song: Song?) {
|
||||
if (song != null) {
|
||||
navModel.mainNavigateTo(
|
||||
|
|
|
@ -99,7 +99,7 @@ class AlbumDetailFragment :
|
|||
|
||||
// -- VIEWMODEL SETUP ---
|
||||
// DetailViewModel handles most initialization from the navigation argument.
|
||||
detailModel.setAlbumUid(args.albumUid)
|
||||
detailModel.setAlbum(args.albumUid)
|
||||
collectImmediately(detailModel.currentAlbum, ::updateAlbum)
|
||||
collectImmediately(detailModel.albumList, ::updateList)
|
||||
collectImmediately(
|
||||
|
|
|
@ -98,7 +98,7 @@ class ArtistDetailFragment :
|
|||
|
||||
// --- VIEWMODEL SETUP ---
|
||||
// DetailViewModel handles most initialization from the navigation argument.
|
||||
detailModel.setArtistUid(args.artistUid)
|
||||
detailModel.setArtist(args.artistUid)
|
||||
collectImmediately(detailModel.currentArtist, ::updateArtist)
|
||||
collectImmediately(detailModel.artistList, ::updateList)
|
||||
collectImmediately(
|
||||
|
|
|
@ -224,47 +224,47 @@ constructor(
|
|||
}
|
||||
|
||||
/**
|
||||
* Set a new [currentSong] from it's [Music.UID]. If the [Music.UID] differs, [currentSong] and
|
||||
* [songAudioProperties] will be updated to align with the new [Song].
|
||||
* Set a new [currentSong] from it's [Music.UID]. [currentSong] and [songAudioProperties] will
|
||||
* be updated to align with the new [Song].
|
||||
*
|
||||
* @param uid The UID of the [Song] to load. Must be valid.
|
||||
*/
|
||||
fun setSongUid(uid: Music.UID) {
|
||||
fun setSong(uid: Music.UID) {
|
||||
logD("Opening Song [uid: $uid]")
|
||||
_currentSong.value = musicRepository.deviceLibrary?.findSong(uid)?.also(::refreshAudioInfo)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a new [currentAlbum] from it's [Music.UID]. If the [Music.UID] differs, [currentAlbum]
|
||||
* and [albumList] will be updated to align with the new [Album].
|
||||
* Set a new [currentAlbum] from it's [Music.UID]. [currentAlbum] and [albumList] will be
|
||||
* updated to align with the new [Album].
|
||||
*
|
||||
* @param uid The [Music.UID] of the [Album] to update [currentAlbum] to. Must be valid.
|
||||
*/
|
||||
fun setAlbumUid(uid: Music.UID) {
|
||||
fun setAlbum(uid: Music.UID) {
|
||||
logD("Opening Album [uid: $uid]")
|
||||
_currentAlbum.value =
|
||||
musicRepository.deviceLibrary?.findAlbum(uid)?.also(::refreshAlbumList)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a new [currentArtist] from it's [Music.UID]. If the [Music.UID] differs, [currentArtist]
|
||||
* and [artistList] will be updated to align with the new [Artist].
|
||||
* Set a new [currentArtist] from it's [Music.UID]. [currentArtist] and [artistList] will be
|
||||
* updated to align with the new [Artist].
|
||||
*
|
||||
* @param uid The [Music.UID] of the [Artist] to update [currentArtist] to. Must be valid.
|
||||
*/
|
||||
fun setArtistUid(uid: Music.UID) {
|
||||
fun setArtist(uid: Music.UID) {
|
||||
logD("Opening Artist [uid: $uid]")
|
||||
_currentArtist.value =
|
||||
musicRepository.deviceLibrary?.findArtist(uid)?.also(::refreshArtistList)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a new [currentGenre] from it's [Music.UID]. If the [Music.UID] differs, [currentGenre]
|
||||
* and [genreList] will be updated to align with the new album.
|
||||
* Set a new [currentGenre] from it's [Music.UID]. [currentGenre] and [genreList] will be
|
||||
* updated to align with the new album.
|
||||
*
|
||||
* @param uid The [Music.UID] of the [Genre] to update [currentGenre] to. Must be valid.
|
||||
*/
|
||||
fun setGenreUid(uid: Music.UID) {
|
||||
fun setGenre(uid: Music.UID) {
|
||||
logD("Opening Genre [uid: $uid]")
|
||||
_currentGenre.value =
|
||||
musicRepository.deviceLibrary?.findGenre(uid)?.also(::refreshGenreList)
|
||||
|
|
|
@ -91,7 +91,7 @@ class GenreDetailFragment :
|
|||
|
||||
// --- VIEWMODEL SETUP ---
|
||||
// DetailViewModel handles most initialization from the navigation argument.
|
||||
detailModel.setGenreUid(args.genreUid)
|
||||
detailModel.setGenre(args.genreUid)
|
||||
collectImmediately(detailModel.currentGenre, ::updatePlaylist)
|
||||
collectImmediately(detailModel.genreList, ::updateList)
|
||||
collectImmediately(
|
||||
|
|
|
@ -67,7 +67,7 @@ class SongDetailDialog : ViewBindingDialogFragment<DialogSongDetailBinding>() {
|
|||
super.onBindingCreated(binding, savedInstanceState)
|
||||
binding.detailProperties.adapter = detailAdapter
|
||||
// DetailViewModel handles most initialization from the navigation argument.
|
||||
detailModel.setSongUid(args.songUid)
|
||||
detailModel.setSong(args.songUid)
|
||||
collectImmediately(detailModel.currentSong, detailModel.songAudioProperties, ::updateSong)
|
||||
}
|
||||
|
||||
|
|
|
@ -48,13 +48,18 @@ constructor(
|
|||
val statistics: StateFlow<Statistics?>
|
||||
get() = _statistics
|
||||
|
||||
private val _newPlaylistSongs = MutableEvent<List<Song>?>()
|
||||
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
|
||||
val newPlaylistSongs: Event<List<Song>> = _newPlaylistSongs
|
||||
|
||||
private val _songsToAdd = MutableEvent<List<Song>?>()
|
||||
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
|
||||
val songsToAdd: Event<List<Song>> = _songsToAdd
|
||||
|
||||
private val _playlistToDelete = MutableEvent<Playlist>()
|
||||
/** Flag for opening a dialog to confirm deletion of the given [Playlist]. */
|
||||
val playlistToDelete: Event<Playlist>
|
||||
get() = _playlistToDelete
|
||||
|
||||
init {
|
||||
musicRepository.addUpdateListener(this)
|
||||
|
@ -110,11 +115,15 @@ constructor(
|
|||
* Delete a [Playlist].
|
||||
*
|
||||
* @param playlist The playlist to delete.
|
||||
*
|
||||
* TODO: Prompt the user before deleting.
|
||||
* @param rude Whether to immediately delete the playlist or prompt the user first. This should
|
||||
* be false at almost all times.
|
||||
*/
|
||||
fun deletePlaylist(playlist: Playlist) {
|
||||
musicRepository.deletePlaylist(playlist)
|
||||
fun deletePlaylist(playlist: Playlist, rude: Boolean = false) {
|
||||
if (rude) {
|
||||
musicRepository.deletePlaylist(playlist)
|
||||
} else {
|
||||
_playlistToDelete.put(playlist)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -71,8 +71,8 @@ class AddToPlaylistDialog :
|
|||
}
|
||||
|
||||
// --- VIEWMODEL SETUP ---
|
||||
pickerModel.setPendingSongs(args.songUids)
|
||||
collectImmediately(pickerModel.currentPendingSongs, ::updatePendingSongs)
|
||||
pickerModel.setSongsToAdd(args.songUids)
|
||||
collectImmediately(pickerModel.currentSongsToAdd, ::updatePendingSongs)
|
||||
collectImmediately(pickerModel.playlistChoices, ::updatePlaylistChoices)
|
||||
}
|
||||
|
||||
|
@ -82,13 +82,13 @@ class AddToPlaylistDialog :
|
|||
}
|
||||
|
||||
override fun onClick(item: PlaylistChoice, viewHolder: RecyclerView.ViewHolder) {
|
||||
musicModel.addToPlaylist(pickerModel.currentPendingSongs.value ?: return, item.playlist)
|
||||
musicModel.addToPlaylist(pickerModel.currentSongsToAdd.value ?: return, item.playlist)
|
||||
requireContext().showToast(R.string.lng_playlist_added)
|
||||
findNavController().navigateUp()
|
||||
}
|
||||
|
||||
override fun onNewPlaylist() {
|
||||
musicModel.createPlaylist(songs = pickerModel.currentPendingSongs.value ?: return)
|
||||
musicModel.createPlaylist(songs = pickerModel.currentSongsToAdd.value ?: return)
|
||||
}
|
||||
|
||||
private fun updatePendingSongs(songs: List<Song>?) {
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Auxio Project
|
||||
* DeletePlaylistDialog.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.music.picker
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.DialogDeletePlaylistBinding
|
||||
import org.oxycblt.auxio.music.MusicViewModel
|
||||
import org.oxycblt.auxio.music.Playlist
|
||||
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
|
||||
import org.oxycblt.auxio.util.collectImmediately
|
||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||
|
||||
/**
|
||||
* A [ViewBindingDialogFragment] that asks the user to confirm the deletion of a [Playlist].
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class DeletePlaylistDialog : ViewBindingDialogFragment<DialogDeletePlaylistBinding>() {
|
||||
private val pickerModel: PlaylistPickerViewModel by viewModels()
|
||||
private val musicModel: MusicViewModel by activityViewModels()
|
||||
// Information about what playlist to name for is initially within the navigation arguments
|
||||
// as UIDs, as that is the only safe way to parcel playlist information.
|
||||
private val args: DeletePlaylistDialogArgs by navArgs()
|
||||
|
||||
override fun onConfigDialog(builder: AlertDialog.Builder) {
|
||||
builder
|
||||
.setTitle(R.string.lbl_confirm_delete_playlist)
|
||||
.setPositiveButton(R.string.lbl_delete) { _, _ ->
|
||||
// Now we can delete the playlist for-real this time.
|
||||
musicModel.deletePlaylist(
|
||||
unlikelyToBeNull(pickerModel.currentPlaylistToDelete.value), rude = true)
|
||||
}
|
||||
.setNegativeButton(R.string.lbl_cancel, null)
|
||||
}
|
||||
|
||||
override fun onCreateBinding(inflater: LayoutInflater) =
|
||||
DialogDeletePlaylistBinding.inflate(inflater)
|
||||
|
||||
override fun onBindingCreated(
|
||||
binding: DialogDeletePlaylistBinding,
|
||||
savedInstanceState: Bundle?
|
||||
) {
|
||||
super.onBindingCreated(binding, savedInstanceState)
|
||||
|
||||
// --- VIEWMODEL SETUP ---
|
||||
pickerModel.setPlaylistToDelete(args.playlistUid)
|
||||
collectImmediately(pickerModel.currentPlaylistToDelete, ::updatePlaylistToDelete)
|
||||
}
|
||||
|
||||
private fun updatePlaylistToDelete(playlist: Playlist?) {
|
||||
if (playlist == null) {
|
||||
// Playlist does not exist anymore, leave
|
||||
findNavController().navigateUp()
|
||||
return
|
||||
}
|
||||
|
||||
requireBinding().deletionInfo.text =
|
||||
getString(R.string.fmt_deletion_info, playlist.name.resolve(requireContext()))
|
||||
}
|
||||
}
|
|
@ -48,14 +48,18 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M
|
|||
val chosenName: StateFlow<ChosenName>
|
||||
get() = _chosenName
|
||||
|
||||
private val _currentPendingSongs = MutableStateFlow<List<Song>?>(null)
|
||||
val currentPendingSongs: StateFlow<List<Song>?>
|
||||
get() = _currentPendingSongs
|
||||
private val _currentSongsToAdd = MutableStateFlow<List<Song>?>(null)
|
||||
val currentSongsToAdd: StateFlow<List<Song>?>
|
||||
get() = _currentSongsToAdd
|
||||
|
||||
private val _playlistChoices = MutableStateFlow<List<PlaylistChoice>>(listOf())
|
||||
val playlistChoices: StateFlow<List<PlaylistChoice>>
|
||||
get() = _playlistChoices
|
||||
|
||||
private val _currentPlaylistToDelete = MutableStateFlow<Playlist?>(null)
|
||||
val currentPlaylistToDelete: StateFlow<Playlist?>
|
||||
get() = _currentPlaylistToDelete
|
||||
|
||||
init {
|
||||
musicRepository.addUpdateListener(this)
|
||||
}
|
||||
|
@ -70,8 +74,8 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M
|
|||
pendingPlaylist.preferredName,
|
||||
pendingPlaylist.songs.mapNotNull { deviceLibrary.findSong(it.uid) })
|
||||
}
|
||||
_currentPendingSongs.value =
|
||||
_currentPendingSongs.value?.let { pendingSongs ->
|
||||
_currentSongsToAdd.value =
|
||||
_currentSongsToAdd.value?.let { pendingSongs ->
|
||||
pendingSongs
|
||||
.mapNotNull { deviceLibrary.findSong(it.uid) }
|
||||
.ifEmpty { null }
|
||||
|
@ -88,7 +92,7 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M
|
|||
// Nothing to do.
|
||||
}
|
||||
}
|
||||
refreshChoicesWith = refreshChoicesWith ?: _currentPendingSongs.value
|
||||
refreshChoicesWith = refreshChoicesWith ?: _currentSongsToAdd.value
|
||||
}
|
||||
|
||||
refreshChoicesWith?.let(::refreshPlaylistChoices)
|
||||
|
@ -99,7 +103,7 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M
|
|||
}
|
||||
|
||||
/**
|
||||
* Update the current [PendingPlaylist]. Will do nothing if already equal.
|
||||
* Set a new [currentPendingPlaylist] from a new batch of pending [Song] [Music.UID]s.
|
||||
*
|
||||
* @param context [Context] required to generate a playlist name.
|
||||
* @param songUids The [Music.UID]s of songs to be present in the playlist.
|
||||
|
@ -121,7 +125,7 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M
|
|||
}
|
||||
|
||||
/**
|
||||
* Update the current [ChosenName] based on new user input.
|
||||
* Update the current [chosenName] based on new user input.
|
||||
*
|
||||
* @param name The new user-inputted name, or null if not present.
|
||||
*/
|
||||
|
@ -143,16 +147,14 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M
|
|||
}
|
||||
|
||||
/**
|
||||
* Update the current [Song]s that to show playlist add choices for. Will do nothing if already
|
||||
* equal.
|
||||
* Set a new [currentSongsToAdd] from a new batch of pending [Song] [Music.UID]s.
|
||||
*
|
||||
* @param songUids The [Music.UID]s of songs to add to a playlist.
|
||||
*/
|
||||
fun setPendingSongs(songUids: Array<Music.UID>) {
|
||||
if (currentPendingSongs.value?.map { it.uid } == songUids) return
|
||||
fun setSongsToAdd(songUids: Array<Music.UID>) {
|
||||
val deviceLibrary = musicRepository.deviceLibrary ?: return
|
||||
val songs = songUids.mapNotNull(deviceLibrary::findSong)
|
||||
_currentPendingSongs.value = songs
|
||||
_currentSongsToAdd.value = songs
|
||||
refreshPlaylistChoices(songs)
|
||||
}
|
||||
|
||||
|
@ -164,6 +166,15 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M
|
|||
PlaylistChoice(it, songs.all(songSet::contains))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a new [currentPendingPlaylist] from a new [Playlist] [Music.UID].
|
||||
*
|
||||
* @param playlistUid The [Music.UID] of the [Playlist] to delete.
|
||||
*/
|
||||
fun setPlaylistToDelete(playlistUid: Music.UID) {
|
||||
_currentPlaylistToDelete.value = musicRepository.userLibrary?.findPlaylist(playlistUid)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
11
app/src/main/res/layout/dialog_delete_playlist.xml
Normal file
11
app/src/main/res/layout/dialog_delete_playlist.xml
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/deletion_info"
|
||||
android:layout_width="match_parent"
|
||||
android:paddingTop="@dimen/spacing_medium"
|
||||
android:paddingEnd="@dimen/spacing_large"
|
||||
android:paddingStart="@dimen/spacing_large"
|
||||
android:layout_height="match_parent"
|
||||
android:textAppearance="@style/TextAppearance.Auxio.BodyLarge"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:text="Delete Playlist 16? This cannot be undone."/>
|
|
@ -23,6 +23,9 @@
|
|||
<action
|
||||
android:id="@+id/action_add_to_playlist"
|
||||
app:destination="@id/add_to_playlist_dialog" />
|
||||
<action
|
||||
android:id="@+id/action_delete_playlist"
|
||||
app:destination="@id/delete_playlist_dialog" />
|
||||
<action
|
||||
android:id="@+id/action_pick_navigation_artist"
|
||||
app:destination="@id/navigate_to_artist_dialog" />
|
||||
|
@ -67,6 +70,16 @@
|
|||
app:destination="@id/new_playlist_dialog" />
|
||||
</dialog>
|
||||
|
||||
<dialog
|
||||
android:id="@+id/delete_playlist_dialog"
|
||||
android:name="org.oxycblt.auxio.music.picker.DeletePlaylistDialog"
|
||||
android:label="delete_playlist_dialog"
|
||||
tools:layout="@layout/dialog_playlist_name">
|
||||
<argument
|
||||
android:name="playlistUid"
|
||||
app:argType="org.oxycblt.auxio.music.Music$UID" />
|
||||
</dialog>
|
||||
|
||||
<dialog
|
||||
android:id="@+id/navigate_to_artist_dialog"
|
||||
android:name="org.oxycblt.auxio.navigation.picker.NavigateToArtistDialog"
|
||||
|
|
|
@ -80,6 +80,7 @@
|
|||
<string name="lbl_playlists">Playlists</string>
|
||||
<string name="lbl_new_playlist">New playlist</string>
|
||||
<string name="lbl_delete">Delete</string>
|
||||
<string name="lbl_confirm_delete_playlist">Delete playlist?</string>
|
||||
|
||||
<!-- Search for music -->
|
||||
<string name="lbl_search">Search</string>
|
||||
|
@ -395,6 +396,7 @@
|
|||
<string name="fmt_sample_rate">%d Hz</string>
|
||||
|
||||
<string name="fmt_indexing">Loading your music library… (%1$d/%2$d)</string>
|
||||
<string name="fmt_deletion_info">Delete %s? This cannot be undone.</string>
|
||||
|
||||
<string name="fmt_lib_song_count">Songs loaded: %d</string>
|
||||
<string name="fmt_lib_album_count">Albums loaded: %d</string>
|
||||
|
|
Loading…
Reference in a new issue