From 0675ce8a030fe1ea2c64f839b73ba98603202872 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Mon, 1 Jan 2024 20:21:39 -0700 Subject: [PATCH] music: rename playlist when reimporting When reimporting an M3U file into a playlist, if the name differs, then initiate a rename dialog so the user has a choice on whether they want to use the new name or not. This does kinda desecrate the "Rename" decision a bit, but it's still to the user the same. --- .../auxio/detail/PlaylistDetailFragment.kt | 6 +- .../org/oxycblt/auxio/home/HomeFragment.kt | 6 +- .../java/org/oxycblt/auxio/music/Music.kt | 1 + .../org/oxycblt/auxio/music/MusicViewModel.kt | 38 ++++++++--- .../auxio/music/decision/NewPlaylistDialog.kt | 42 +++++++----- .../music/decision/PlaylistPickerViewModel.kt | 68 ++++++++++++------- .../music/decision/RenamePlaylistDialog.kt | 23 ++++--- .../oxycblt/auxio/search/SearchFragment.kt | 6 +- .../res/layout/fragment_playback_panel.xml | 2 +- app/src/main/res/navigation/inner.xml | 10 +++ 10 files changed, 142 insertions(+), 60 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt index 40a03c6e8..5e02cd0a1 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt @@ -352,7 +352,11 @@ class PlaylistDetailFragment : } is PlaylistDecision.Rename -> { logD("Renaming ${decision.playlist}") - PlaylistDetailFragmentDirections.renamePlaylist(decision.playlist.uid) + PlaylistDetailFragmentDirections.renamePlaylist( + decision.playlist.uid, + decision.template, + decision.applySongs.map { it.uid }.toTypedArray(), + decision.reason) } is PlaylistDecision.Export -> { logD("Exporting ${decision.playlist}") diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt index 9ed9870ce..3c8a0dd90 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt @@ -487,7 +487,11 @@ class HomeFragment : } is PlaylistDecision.Rename -> { logD("Renaming ${decision.playlist}") - HomeFragmentDirections.renamePlaylist(decision.playlist.uid) + HomeFragmentDirections.renamePlaylist( + decision.playlist.uid, + decision.template, + decision.applySongs.map { it.uid }.toTypedArray(), + decision.reason) } is PlaylistDecision.Export -> { logD("Exporting ${decision.playlist}") diff --git a/app/src/main/java/org/oxycblt/auxio/music/Music.kt b/app/src/main/java/org/oxycblt/auxio/music/Music.kt index 7d6fae73e..1bf23aaf6 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Music.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Music.kt @@ -348,6 +348,7 @@ interface Genre : MusicParent { * @author Alexander Capehart (OxygenCobalt) */ interface Playlist : MusicParent { + override val name: Name.Known override val songs: List /** The total duration of the songs in this genre, in milliseconds. */ val durationMs: Long 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 ffd629172..97a8fd8f0 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt @@ -145,7 +145,7 @@ constructor( } /** - * Import a playlist from a file [Uri]. Errors pushed to [importError]. + * Import a playlist from a file [Uri]. Errors pushed to [playlistMessage]. * * @param uri The [Uri] of the file to import. If null, the user will be prompted with a file * picker. @@ -173,8 +173,17 @@ constructor( } if (target !== null) { - musicRepository.rewritePlaylist(target, songs) - _playlistMessage.put(PlaylistMessage.ImportSuccess) + if (importedPlaylist.name != null && importedPlaylist.name != target.name.raw) { + _playlistDecision.put( + PlaylistDecision.Rename( + target, + importedPlaylist.name, + songs, + PlaylistDecision.Rename.Reason.IMPORT)) + } else { + musicRepository.rewritePlaylist(target, songs) + _playlistMessage.put(PlaylistMessage.ImportSuccess) + } } else { _playlistDecision.put( PlaylistDecision.New( @@ -188,7 +197,7 @@ constructor( } /** - * Export a [Playlist] to a file [Uri]. Errors pushed to [exportError]. + * Export a [Playlist] to a file [Uri]. Errors pushed to [playlistMessage]. * * @param playlist The [Playlist] to export. * @param uri The [Uri] to export to. If null, the user will be prompted for one. @@ -214,17 +223,24 @@ constructor( * * @param playlist The [Playlist] to rename, * @param name The new name of the [Playlist]. If null, the user will be prompted for a name. - * @param reason The reason why the playlist is being renamed. For all intensive purposes, you + * @param applySongs The songs to apply to the playlist after renaming. If empty, no songs will + * be applied. This argument is internal and does not need to be specified in normal use. + * @param reason The reason why the playlist is being renamed. This argument is internal and + * does not need to be specified in normal use. */ fun renamePlaylist( playlist: Playlist, name: String? = null, + applySongs: List = listOf(), reason: PlaylistDecision.Rename.Reason = PlaylistDecision.Rename.Reason.ACTION ) { if (name != null) { logD("Renaming $playlist to $name") viewModelScope.launch(Dispatchers.IO) { musicRepository.renamePlaylist(playlist, name) + if (applySongs.isNotEmpty()) { + musicRepository.rewritePlaylist(playlist, applySongs) + } val message = when (reason) { PlaylistDecision.Rename.Reason.ACTION -> PlaylistMessage.RenameSuccess @@ -234,7 +250,7 @@ constructor( } } else { logD("Launching rename dialog for $playlist") - _playlistDecision.put(PlaylistDecision.Rename(playlist, reason)) + _playlistDecision.put(PlaylistDecision.Rename(playlist, null, applySongs, reason)) } } @@ -243,7 +259,8 @@ constructor( * * @param playlist The playlist to delete. * @param rude Whether to immediately delete the playlist or prompt the user first. This should - * be false at almost all times. + * be false at almost all times. This argument is internal and does not need to be specified + * in normal use. */ fun deletePlaylist(playlist: Playlist, rude: Boolean = false) { if (rude) { @@ -375,7 +392,12 @@ sealed interface PlaylistDecision { * * @param playlist The playlist to act on. */ - data class Rename(val playlist: Playlist, val reason: Reason) : PlaylistDecision { + data class Rename( + val playlist: Playlist, + val template: String?, + val applySongs: List, + val reason: Reason + ) : PlaylistDecision { enum class Reason { ACTION, IMPORT diff --git a/app/src/main/java/org/oxycblt/auxio/music/decision/NewPlaylistDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/decision/NewPlaylistDialog.kt index 54ea142ff..ac3b82ec0 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/decision/NewPlaylistDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/decision/NewPlaylistDialog.kt @@ -31,6 +31,7 @@ import dagger.hilt.android.AndroidEntryPoint import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.DialogPlaylistNameBinding import org.oxycblt.auxio.music.MusicViewModel +import org.oxycblt.auxio.music.PlaylistDecision import org.oxycblt.auxio.ui.ViewBindingMaterialDialogFragment import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.logD @@ -52,9 +53,14 @@ class NewPlaylistDialog : ViewBindingMaterialDialogFragment R.string.lbl_new_playlist + PlaylistDecision.New.Reason.IMPORT -> R.string.lbl_import_playlist + }) .setPositiveButton(R.string.lbl_ok) { _, _ -> - val pendingPlaylist = unlikelyToBeNull(pickerModel.currentPendingPlaylist.value) + val pendingPlaylist = unlikelyToBeNull(pickerModel.currentPendingNewPlaylist.value) val name = when (val chosenName = pickerModel.chosenName.value) { is ChosenName.Valid -> chosenName.value @@ -84,27 +90,29 @@ class NewPlaylistDialog : ViewBindingMaterialDialogFragment(null) + private val _currentPendingNewPlaylist = MutableStateFlow(null) /** A new [Playlist] having it's name chosen by the user. Null if none yet. */ - val currentPendingPlaylist: StateFlow - get() = _currentPendingPlaylist + val currentPendingNewPlaylist: StateFlow + get() = _currentPendingNewPlaylist - private val _currentPlaylistToRename = MutableStateFlow(null) + private val _currentPendingRenamePlaylist = MutableStateFlow(null) /** An existing [Playlist] that is being renamed. Null if none yet. */ - val currentPlaylistToRename: StateFlow - get() = _currentPlaylistToRename + val currentPendingRenamePlaylist: StateFlow + get() = _currentPendingRenamePlaylist private val _currentPlaylistToExport = MutableStateFlow(null) /** An existing [Playlist] that is being exported. Null if none yet. */ @@ -71,7 +71,7 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M get() = _currentPlaylistToDelete private val _chosenName = MutableStateFlow(ChosenName.Empty) - /** The users chosen name for [currentPendingPlaylist] or [currentPlaylistToRename]. */ + /** The users chosen name for [currentPendingNewPlaylist] or [currentPendingRenamePlaylist]. */ val chosenName: StateFlow get() = _chosenName @@ -93,14 +93,15 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M var refreshChoicesWith: List? = null val deviceLibrary = musicRepository.deviceLibrary if (changes.deviceLibrary && deviceLibrary != null) { - _currentPendingPlaylist.value = - _currentPendingPlaylist.value?.let { pendingPlaylist -> - PendingPlaylist( + _currentPendingNewPlaylist.value = + _currentPendingNewPlaylist.value?.let { pendingPlaylist -> + PendingNewPlaylist( pendingPlaylist.preferredName, pendingPlaylist.songs.mapNotNull { deviceLibrary.findSong(it.uid) }, + pendingPlaylist.template, pendingPlaylist.reason) } - logD("Updated pending playlist: ${_currentPendingPlaylist.value?.preferredName}") + logD("Updated pending playlist: ${_currentPendingNewPlaylist.value?.preferredName}") _currentSongsToAdd.value = _currentSongsToAdd.value?.let { pendingSongs -> @@ -141,7 +142,7 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M } /** - * Set a new [currentPendingPlaylist] from a new batch of pending [Song] [Music.UID]s. + * Set a new [currentPendingNewPlaylist] 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. @@ -150,6 +151,7 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M fun setPendingPlaylist( context: Context, songUids: Array, + template: String?, reason: PlaylistDecision.New.Reason ) { logD("Opening ${songUids.size} songs to create a playlist from") @@ -173,9 +175,9 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M possibleName } - _currentPendingPlaylist.value = + _currentPendingNewPlaylist.value = if (possibleName != null && songs != null) { - PendingPlaylist(possibleName, songs, reason) + PendingNewPlaylist(possibleName, songs, template, reason) } else { logW("Given song UIDs to create were invalid") null @@ -183,16 +185,28 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M } /** - * Set a new [currentPlaylistToRename] from a [Playlist] [Music.UID]. + * Set a new [currentPendingRenamePlaylist] from a [Playlist] [Music.UID]. * * @param playlistUid The [Music.UID]s of the [Playlist] to rename. */ - fun setPlaylistToRename(playlistUid: Music.UID) { + fun setPlaylistToRename( + playlistUid: Music.UID, + applySongUids: Array, + template: String?, + reason: PlaylistDecision.Rename.Reason + ) { logD("Opening playlist $playlistUid to rename") - _currentPlaylistToRename.value = musicRepository.userLibrary?.findPlaylist(playlistUid) - if (_currentPlaylistToDelete.value == null) { - logW("Given playlist UID to rename was invalid") - } + val playlist = musicRepository.userLibrary?.findPlaylist(playlistUid) + val applySongs = + musicRepository.deviceLibrary?.let { applySongUids.mapNotNull(it::findSong) } + + _currentPendingRenamePlaylist.value = + if (playlist != null && applySongs != null) { + PendingRenamePlaylist(playlist, applySongs, template, reason) + } else { + logW("Given playlist UID to rename was invalid") + null + } } /** @@ -223,7 +237,7 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M } /** - * Set a new [currentPendingPlaylist] from a new [Playlist] [Music.UID]. + * Set a new [currentPendingNewPlaylist] from a new [Playlist] [Music.UID]. * * @param playlistUid The [Music.UID] of the [Playlist] to delete. */ @@ -301,16 +315,24 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M * Represents a playlist that will be created as soon as a name is chosen. * * @param preferredName The name to be used by default if no other name is chosen. - * @param songs The [Song]s to be contained in the [PendingPlaylist] + * @param songs The [Song]s to be contained in the [PendingNewPlaylist] * @param reason The reason the playlist is being created. * @author Alexander Capehart (OxygenCobalt) */ -data class PendingPlaylist( +data class PendingNewPlaylist( val preferredName: String, val songs: List, + val template: String?, val reason: PlaylistDecision.New.Reason ) +data class PendingRenamePlaylist( + val playlist: Playlist, + val applySongs: List, + val template: String?, + val reason: PlaylistDecision.Rename.Reason +) + /** * Represents the (processed) user input from the playlist naming dialogs. * diff --git a/app/src/main/java/org/oxycblt/auxio/music/decision/RenamePlaylistDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/decision/RenamePlaylistDialog.kt index 6c39855b8..da5265daf 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/decision/RenamePlaylistDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/decision/RenamePlaylistDialog.kt @@ -30,7 +30,6 @@ import dagger.hilt.android.AndroidEntryPoint import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.DialogPlaylistNameBinding import org.oxycblt.auxio.music.MusicViewModel -import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.ui.ViewBindingMaterialDialogFragment import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.logD @@ -54,9 +53,14 @@ class RenamePlaylistDialog : ViewBindingMaterialDialogFragment - val playlist = unlikelyToBeNull(pickerModel.currentPlaylistToRename.value) + val pendingRenamePlaylist = + unlikelyToBeNull(pickerModel.currentPendingRenamePlaylist.value) val chosenName = pickerModel.chosenName.value as ChosenName.Valid - musicModel.renamePlaylist(playlist, chosenName.value) + musicModel.renamePlaylist( + pendingRenamePlaylist.playlist, + chosenName.value, + pendingRenamePlaylist.applySongs, + pendingRenamePlaylist.reason) findNavController().navigateUp() } .setNegativeButton(R.string.lbl_cancel, null) @@ -73,20 +77,23 @@ class RenamePlaylistDialog : ViewBindingMaterialDialogFragment() { } is PlaylistDecision.Rename -> { logD("Renaming ${decision.playlist}") - SearchFragmentDirections.renamePlaylist(decision.playlist.uid) + SearchFragmentDirections.renamePlaylist( + decision.playlist.uid, + decision.template, + decision.applySongs.map { it.uid }.toTypedArray(), + decision.reason) } is PlaylistDecision.Delete -> { logD("Deleting ${decision.playlist}") diff --git a/app/src/main/res/layout/fragment_playback_panel.xml b/app/src/main/res/layout/fragment_playback_panel.xml index 573c443b7..050d2b86b 100644 --- a/app/src/main/res/layout/fragment_playback_panel.xml +++ b/app/src/main/res/layout/fragment_playback_panel.xml @@ -72,7 +72,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" app:layout_constraintBottom_toTopOf="@+id/playback_controls_container" - app:layout_constraintEnd_toEndOf="@+id/playback_cover_pager" + app:layout_constraintEnd_toEndOf="@+id/playback_info_container" app:layout_constraintHorizontal_bias="0.0" app:layout_constraintStart_toStartOf="parent" /> diff --git a/app/src/main/res/navigation/inner.xml b/app/src/main/res/navigation/inner.xml index 9d00b047c..c688464cb 100644 --- a/app/src/main/res/navigation/inner.xml +++ b/app/src/main/res/navigation/inner.xml @@ -430,6 +430,16 @@ + + +