music: allow renaming playlist before import

When you import a playlist, Auxio will now always display the
"New Playlist" dialog so you can change whatever name Auxio has picked
for the imported playlist.

This also prevents the creation of two playlists with the same names.
This commit is contained in:
Alexander Capehart 2024-01-01 16:12:01 -07:00
parent 68584ba426
commit 9ad11ec5aa
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
6 changed files with 53 additions and 13 deletions

View file

@ -471,7 +471,9 @@ class HomeFragment :
is PlaylistDecision.New -> { is PlaylistDecision.New -> {
logD("Creating new playlist") logD("Creating new playlist")
HomeFragmentDirections.newPlaylist( HomeFragmentDirections.newPlaylist(
decision.songs.map { it.uid }.toTypedArray(), decision.reason) decision.songs.map { it.uid }.toTypedArray(),
decision.template,
decision.reason)
} }
is PlaylistDecision.Import -> { is PlaylistDecision.Import -> {
logD("Importing playlist") logD("Importing playlist")

View file

@ -29,7 +29,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeout

View file

@ -51,15 +51,18 @@ constructor(
) : ViewModel(), MusicRepository.UpdateListener, MusicRepository.IndexingListener { ) : ViewModel(), MusicRepository.UpdateListener, MusicRepository.IndexingListener {
private val _indexingState = MutableStateFlow<IndexingState?>(null) private val _indexingState = MutableStateFlow<IndexingState?>(null)
/** The current music loading state, or null if no loading is going on. */ /** The current music loading state, or null if no loading is going on. */
val indexingState: StateFlow<IndexingState?> = _indexingState val indexingState: StateFlow<IndexingState?> = _indexingState
private val _statistics = MutableStateFlow<Statistics?>(null) private val _statistics = MutableStateFlow<Statistics?>(null)
/** [Statistics] about the last completed music load. */ /** [Statistics] about the last completed music load. */
val statistics: StateFlow<Statistics?> val statistics: StateFlow<Statistics?>
get() = _statistics get() = _statistics
private val _playlistDecision = MutableEvent<PlaylistDecision>() private val _playlistDecision = MutableEvent<PlaylistDecision>()
/** /**
* A [PlaylistDecision] command that is awaiting a view capable of responding to it. Null if * A [PlaylistDecision] command that is awaiting a view capable of responding to it. Null if
* none currently. * none currently.
@ -137,7 +140,7 @@ constructor(
} }
} else { } else {
logD("Launching creation dialog for ${songs.size} songs") logD("Launching creation dialog for ${songs.size} songs")
_playlistDecision.put(PlaylistDecision.New(songs, reason)) _playlistDecision.put(PlaylistDecision.New(songs, null, reason))
} }
} }
@ -168,14 +171,14 @@ constructor(
_playlistMessage.put(PlaylistMessage.ImportFailed) _playlistMessage.put(PlaylistMessage.ImportFailed)
return@launch return@launch
} }
// TODO Require the user to name it something else if the name is a duplicate of
// a prior playlist
if (target !== null) { if (target !== null) {
musicRepository.rewritePlaylist(target, songs) musicRepository.rewritePlaylist(target, songs)
_playlistMessage.put(PlaylistMessage.ImportSuccess) _playlistMessage.put(PlaylistMessage.ImportSuccess)
} else { } else {
// TODO: Have to properly propagate the "Playlist Created" message _playlistDecision.put(
createPlaylist(importedPlaylist.name, songs, PlaylistDecision.New.Reason.IMPORT) PlaylistDecision.New(
songs, importedPlaylist.name, PlaylistDecision.New.Reason.IMPORT))
} }
} }
} else { } else {
@ -211,17 +214,27 @@ constructor(
* *
* @param playlist The [Playlist] to rename, * @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 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
*/ */
fun renamePlaylist(playlist: Playlist, name: String? = null) { fun renamePlaylist(
playlist: Playlist,
name: String? = null,
reason: PlaylistDecision.Rename.Reason = PlaylistDecision.Rename.Reason.ACTION
) {
if (name != null) { if (name != null) {
logD("Renaming $playlist to $name") logD("Renaming $playlist to $name")
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
musicRepository.renamePlaylist(playlist, name) musicRepository.renamePlaylist(playlist, name)
_playlistMessage.put(PlaylistMessage.RenameSuccess) val message =
when (reason) {
PlaylistDecision.Rename.Reason.ACTION -> PlaylistMessage.RenameSuccess
PlaylistDecision.Rename.Reason.IMPORT -> PlaylistMessage.ImportSuccess
}
_playlistMessage.put(message)
} }
} else { } else {
logD("Launching rename dialog for $playlist") logD("Launching rename dialog for $playlist")
_playlistDecision.put(PlaylistDecision.Rename(playlist)) _playlistDecision.put(PlaylistDecision.Rename(playlist, reason))
} }
} }
@ -336,9 +349,12 @@ sealed interface PlaylistDecision {
* Navigate to a dialog that allows a user to pick a name for a new [Playlist]. * Navigate to a dialog that allows a user to pick a name for a new [Playlist].
* *
* @param songs The [Song]s to contain in the new [Playlist]. * @param songs The [Song]s to contain in the new [Playlist].
* @param template An existing playlist name that should be editable in the opened dialog. If
* null, a placeholder should be created and shown as a hint instead.
* @param context The context in which this decision is being fulfilled. * @param context The context in which this decision is being fulfilled.
*/ */
data class New(val songs: List<Song>, val reason: Reason) : PlaylistDecision { data class New(val songs: List<Song>, val template: String?, val reason: Reason) :
PlaylistDecision {
enum class Reason { enum class Reason {
NEW, NEW,
ADD, ADD,
@ -359,7 +375,12 @@ sealed interface PlaylistDecision {
* *
* @param playlist The playlist to act on. * @param playlist The playlist to act on.
*/ */
data class Rename(val playlist: Playlist) : PlaylistDecision data class Rename(val playlist: Playlist, val reason: Reason) : PlaylistDecision {
enum class Reason {
ACTION,
IMPORT
}
}
/** /**
* Navigate to a dialog that allows the user to export a [Playlist]. * Navigate to a dialog that allows the user to export a [Playlist].

View file

@ -100,7 +100,7 @@ class AddToPlaylistDialog :
findNavController() findNavController()
.navigateSafe( .navigateSafe(
AddToPlaylistDialogDirections.newPlaylist( AddToPlaylistDialogDirections.newPlaylist(
songs.map { it.uid }.toTypedArray(), PlaylistDecision.New.Reason.ADD)) songs.map { it.uid }.toTypedArray(), null, PlaylistDecision.New.Reason.ADD))
} }
private fun updatePendingSongs(songs: List<Song>?) { private fun updatePendingSongs(songs: List<Song>?) {

View file

@ -19,6 +19,7 @@
package org.oxycblt.auxio.music.decision package org.oxycblt.auxio.music.decision
import android.os.Bundle import android.os.Bundle
import android.text.Editable
import android.view.LayoutInflater import android.view.LayoutInflater
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.widget.addTextChangedListener import androidx.core.widget.addTextChangedListener
@ -47,6 +48,7 @@ class NewPlaylistDialog : ViewBindingMaterialDialogFragment<DialogPlaylistNameBi
// Information about what playlist to name for is initially within the navigation arguments // 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. // as UIDs, as that is the only safe way to parcel playlist information.
private val args: NewPlaylistDialogArgs by navArgs() private val args: NewPlaylistDialogArgs by navArgs()
private var initializedField = false
override fun onConfigDialog(builder: AlertDialog.Builder) { override fun onConfigDialog(builder: AlertDialog.Builder) {
builder builder
@ -83,6 +85,14 @@ class NewPlaylistDialog : ViewBindingMaterialDialogFragment<DialogPlaylistNameBi
// --- VIEWMODEL SETUP --- // --- VIEWMODEL SETUP ---
musicModel.playlistDecision.consume() musicModel.playlistDecision.consume()
pickerModel.setPendingPlaylist(requireContext(), args.songUids, args.reason) pickerModel.setPendingPlaylist(requireContext(), args.songUids, args.reason)
if (!initializedField) {
initializedField = true
// Need to convert args.existingName to an Editable
if (args.template != null) {
binding.playlistName.text = EDITABLE_FACTORY.newEditable(args.template)
}
}
collectImmediately(pickerModel.currentPendingPlaylist, ::updatePendingPlaylist) collectImmediately(pickerModel.currentPendingPlaylist, ::updatePendingPlaylist)
collectImmediately(pickerModel.chosenName, ::updateChosenName) collectImmediately(pickerModel.chosenName, ::updateChosenName)
} }
@ -101,4 +111,8 @@ class NewPlaylistDialog : ViewBindingMaterialDialogFragment<DialogPlaylistNameBi
(dialog as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE)?.isEnabled = (dialog as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE)?.isEnabled =
chosenName is ChosenName.Valid || chosenName is ChosenName.Empty chosenName is ChosenName.Valid || chosenName is ChosenName.Empty
} }
private companion object {
val EDITABLE_FACTORY: Editable.Factory = Editable.Factory.getInstance()
}
} }

View file

@ -413,6 +413,10 @@
<argument <argument
android:name="songUids" android:name="songUids"
app:argType="org.oxycblt.auxio.music.Music$UID[]" /> app:argType="org.oxycblt.auxio.music.Music$UID[]" />
<argument
android:name="template"
app:argType="string"
app:nullable="true" />
<argument <argument
android:name="reason" android:name="reason"
app:argType="org.oxycblt.auxio.music.PlaylistDecision$New$Reason" /> app:argType="org.oxycblt.auxio.music.PlaylistDecision$New$Reason" />