diff --git a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt index 4877ee253..106ba2885 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt @@ -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( diff --git a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt index 3f08beaab..42d6bb341 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt @@ -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( diff --git a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt index 046c52f29..4578f664d 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt @@ -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( diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt index fcbbe4a33..d2e87f6ed 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt @@ -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) diff --git a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt index eb08d08a1..2028c3610 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt @@ -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( diff --git a/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt b/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt index 615f256e4..5ba78ea8f 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt @@ -67,7 +67,7 @@ class SongDetailDialog : ViewBindingDialogFragment() { 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) } diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt index df53b581d..75527b6d8 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt @@ -48,13 +48,18 @@ constructor( val statistics: StateFlow get() = _statistics - private val _newPlaylistSongs = MutableEvent?>() + private val _newPlaylistSongs = MutableEvent>() /** Flag for opening a dialog to create a playlist of the given [Song]s. */ - val newPlaylistSongs: Event?> = _newPlaylistSongs + val newPlaylistSongs: Event> = _newPlaylistSongs - private val _songsToAdd = MutableEvent?>() + private val _songsToAdd = MutableEvent>() /** Flag for opening a dialog to add the given [Song]s to a playlist. */ - val songsToAdd: Event?> = _songsToAdd + val songsToAdd: Event> = _songsToAdd + + private val _playlistToDelete = MutableEvent() + /** Flag for opening a dialog to confirm deletion of the given [Playlist]. */ + val playlistToDelete: Event + 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) + } } /** diff --git a/app/src/main/java/org/oxycblt/auxio/music/picker/AddToPlaylistDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/picker/AddToPlaylistDialog.kt index dc3ceb556..66660bb6d 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/picker/AddToPlaylistDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/picker/AddToPlaylistDialog.kt @@ -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?) { diff --git a/app/src/main/java/org/oxycblt/auxio/music/picker/DeletePlaylistDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/picker/DeletePlaylistDialog.kt new file mode 100644 index 000000000..155846606 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/music/picker/DeletePlaylistDialog.kt @@ -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 . + */ + +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() { + 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())) + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/music/picker/PlaylistPickerViewModel.kt b/app/src/main/java/org/oxycblt/auxio/music/picker/PlaylistPickerViewModel.kt index c5508b2e0..750886f90 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/picker/PlaylistPickerViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/picker/PlaylistPickerViewModel.kt @@ -48,14 +48,18 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M val chosenName: StateFlow get() = _chosenName - private val _currentPendingSongs = MutableStateFlow?>(null) - val currentPendingSongs: StateFlow?> - get() = _currentPendingSongs + private val _currentSongsToAdd = MutableStateFlow?>(null) + val currentSongsToAdd: StateFlow?> + get() = _currentSongsToAdd private val _playlistChoices = MutableStateFlow>(listOf()) val playlistChoices: StateFlow> get() = _playlistChoices + private val _currentPlaylistToDelete = MutableStateFlow(null) + val currentPlaylistToDelete: StateFlow + 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) { - if (currentPendingSongs.value?.map { it.uid } == songUids) return + fun setSongsToAdd(songUids: Array) { 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) + } } /** diff --git a/app/src/main/res/layout/dialog_delete_playlist.xml b/app/src/main/res/layout/dialog_delete_playlist.xml new file mode 100644 index 000000000..4987c3290 --- /dev/null +++ b/app/src/main/res/layout/dialog_delete_playlist.xml @@ -0,0 +1,11 @@ + + \ No newline at end of file diff --git a/app/src/main/res/navigation/nav_main.xml b/app/src/main/res/navigation/nav_main.xml index 511e35393..a11e936ac 100644 --- a/app/src/main/res/navigation/nav_main.xml +++ b/app/src/main/res/navigation/nav_main.xml @@ -23,6 +23,9 @@ + @@ -67,6 +70,16 @@ app:destination="@id/new_playlist_dialog" /> + + + + Playlists New playlist Delete + Delete playlist? Search @@ -395,6 +396,7 @@ %d Hz Loading your music library… (%1$d/%2$d) + Delete %s? This cannot be undone. Songs loaded: %d Albums loaded: %d