Skip to content

Commit

Permalink
music: rename playlist when reimporting
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
OxygenCobalt committed Jan 2, 2024
1 parent 9ad11ec commit 0675ce8
Show file tree
Hide file tree
Showing 10 changed files with 140 additions and 58 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
Expand Down
6 changes: 5 additions & 1 deletion app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
Expand Down
1 change: 1 addition & 0 deletions app/src/main/java/org/oxycblt/auxio/music/Music.kt
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,7 @@ interface Genre : MusicParent {
* @author Alexander Capehart (OxygenCobalt)
*/
interface Playlist : MusicParent {
override val name: Name.Known
override val songs: List<Song>
/** The total duration of the songs in this genre, in milliseconds. */
val durationMs: Long
Expand Down
38 changes: 30 additions & 8 deletions app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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(
Expand All @@ -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.
Expand All @@ -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<Song> = 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
Expand All @@ -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))
}
}

Expand All @@ -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) {
Expand Down Expand Up @@ -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<Song>,
val reason: Reason
) : PlaylistDecision {
enum class Reason {
ACTION,
IMPORT
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -52,9 +53,14 @@ class NewPlaylistDialog : ViewBindingMaterialDialogFragment<DialogPlaylistNameBi

override fun onConfigDialog(builder: AlertDialog.Builder) {
builder
.setTitle(R.string.lbl_new_playlist)
.setTitle(
when (args.reason) {
PlaylistDecision.New.Reason.NEW,
PlaylistDecision.New.Reason.ADD -> 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
Expand Down Expand Up @@ -84,27 +90,29 @@ 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)
}
}
pickerModel.setPendingPlaylist(requireContext(), args.songUids, args.template, args.reason)

collectImmediately(pickerModel.currentPendingPlaylist, ::updatePendingPlaylist)
collectImmediately(pickerModel.currentPendingNewPlaylist, ::updatePendingPlaylist)
collectImmediately(pickerModel.chosenName, ::updateChosenName)
}

private fun updatePendingPlaylist(pendingPlaylist: PendingPlaylist?) {
if (pendingPlaylist == null) {
private fun updatePendingPlaylist(pendingNewPlaylist: PendingNewPlaylist?) {
if (pendingNewPlaylist == null) {
logD("No playlist to create, leaving")
findNavController().navigateUp()
return
}

requireBinding().playlistName.hint = pendingPlaylist.preferredName
val binding = requireBinding()
if (pendingNewPlaylist.template != null) {
if (initializedField) return
initializedField = true
// Need to convert args.existingName to an Editable
if (args.template != null) {
binding.playlistName.text = EDITABLE_FACTORY.newEditable(args.template)
}
} else {
binding.playlistName.hint = pendingNewPlaylist.preferredName
}
}

private fun updateChosenName(chosenName: ChosenName) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,15 @@ import org.oxycblt.auxio.util.logW
@HiltViewModel
class PlaylistPickerViewModel @Inject constructor(private val musicRepository: MusicRepository) :
ViewModel(), MusicRepository.UpdateListener {
private val _currentPendingPlaylist = MutableStateFlow<PendingPlaylist?>(null)
private val _currentPendingNewPlaylist = MutableStateFlow<PendingNewPlaylist?>(null)
/** A new [Playlist] having it's name chosen by the user. Null if none yet. */
val currentPendingPlaylist: StateFlow<PendingPlaylist?>
get() = _currentPendingPlaylist
val currentPendingNewPlaylist: StateFlow<PendingNewPlaylist?>
get() = _currentPendingNewPlaylist

private val _currentPlaylistToRename = MutableStateFlow<Playlist?>(null)
private val _currentPendingRenamePlaylist = MutableStateFlow<PendingRenamePlaylist?>(null)
/** An existing [Playlist] that is being renamed. Null if none yet. */
val currentPlaylistToRename: StateFlow<Playlist?>
get() = _currentPlaylistToRename
val currentPendingRenamePlaylist: StateFlow<PendingRenamePlaylist?>
get() = _currentPendingRenamePlaylist

private val _currentPlaylistToExport = MutableStateFlow<Playlist?>(null)
/** An existing [Playlist] that is being exported. Null if none yet. */
Expand All @@ -71,7 +71,7 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M
get() = _currentPlaylistToDelete

private val _chosenName = MutableStateFlow<ChosenName>(ChosenName.Empty)
/** The users chosen name for [currentPendingPlaylist] or [currentPlaylistToRename]. */
/** The users chosen name for [currentPendingNewPlaylist] or [currentPendingRenamePlaylist]. */
val chosenName: StateFlow<ChosenName>
get() = _chosenName

Expand All @@ -93,14 +93,15 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M
var refreshChoicesWith: List<Song>? = 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 ->
Expand Down Expand Up @@ -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.
Expand All @@ -150,6 +151,7 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M
fun setPendingPlaylist(
context: Context,
songUids: Array<Music.UID>,
template: String?,
reason: PlaylistDecision.New.Reason
) {
logD("Opening ${songUids.size} songs to create a playlist from")
Expand All @@ -173,26 +175,38 @@ 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
}
}

/**
* 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<Music.UID>,
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
}
}

/**
Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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<Song>,
val template: String?,
val reason: PlaylistDecision.New.Reason
)

data class PendingRenamePlaylist(
val playlist: Playlist,
val applySongs: List<Song>,
val template: String?,
val reason: PlaylistDecision.Rename.Reason
)

/**
* Represents the (processed) user input from the playlist naming dialogs.
*
Expand Down
Loading

0 comments on commit 0675ce8

Please sign in to comment.