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 -> {
logD("Creating new playlist")
HomeFragmentDirections.newPlaylist(
decision.songs.map { it.uid }.toTypedArray(), decision.reason)
decision.songs.map { it.uid }.toTypedArray(),
decision.template,
decision.reason)
}
is PlaylistDecision.Import -> {
logD("Importing playlist")

View file

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

View file

@ -51,15 +51,18 @@ constructor(
) : ViewModel(), MusicRepository.UpdateListener, MusicRepository.IndexingListener {
private val _indexingState = MutableStateFlow<IndexingState?>(null)
/** The current music loading state, or null if no loading is going on. */
val indexingState: StateFlow<IndexingState?> = _indexingState
private val _statistics = MutableStateFlow<Statistics?>(null)
/** [Statistics] about the last completed music load. */
val statistics: StateFlow<Statistics?>
get() = _statistics
private val _playlistDecision = MutableEvent<PlaylistDecision>()
/**
* A [PlaylistDecision] command that is awaiting a view capable of responding to it. Null if
* none currently.
@ -137,7 +140,7 @@ constructor(
}
} else {
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)
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, PlaylistDecision.New.Reason.IMPORT)
_playlistDecision.put(
PlaylistDecision.New(
songs, importedPlaylist.name, PlaylistDecision.New.Reason.IMPORT))
}
}
} else {
@ -211,17 +214,27 @@ 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
*/
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) {
logD("Renaming $playlist to $name")
viewModelScope.launch(Dispatchers.IO) {
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 {
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].
*
* @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.
*/
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 {
NEW,
ADD,
@ -359,7 +375,12 @@ sealed interface PlaylistDecision {
*
* @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].

View file

@ -100,7 +100,7 @@ class AddToPlaylistDialog :
findNavController()
.navigateSafe(
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>?) {

View file

@ -19,6 +19,7 @@
package org.oxycblt.auxio.music.decision
import android.os.Bundle
import android.text.Editable
import android.view.LayoutInflater
import androidx.appcompat.app.AlertDialog
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
// as UIDs, as that is the only safe way to parcel playlist information.
private val args: NewPlaylistDialogArgs by navArgs()
private var initializedField = false
override fun onConfigDialog(builder: AlertDialog.Builder) {
builder
@ -83,6 +85,14 @@ class NewPlaylistDialog : ViewBindingMaterialDialogFragment<DialogPlaylistNameBi
// --- VIEWMODEL SETUP ---
musicModel.playlistDecision.consume()
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.chosenName, ::updateChosenName)
}
@ -101,4 +111,8 @@ class NewPlaylistDialog : ViewBindingMaterialDialogFragment<DialogPlaylistNameBi
(dialog as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE)?.isEnabled =
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
android:name="songUids"
app:argType="org.oxycblt.auxio.music.Music$UID[]" />
<argument
android:name="template"
app:argType="string"
app:nullable="true" />
<argument
android:name="reason"
app:argType="org.oxycblt.auxio.music.PlaylistDecision$New$Reason" />