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.
This commit is contained in:
parent
9ad11ec5aa
commit
0675ce8a03
10 changed files with 142 additions and 60 deletions
|
@ -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}")
|
||||
|
|
|
@ -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}")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
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<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
|
||||
|
@ -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<Song>,
|
||||
val reason: Reason
|
||||
) : PlaylistDecision {
|
||||
enum class Reason {
|
||||
ACTION,
|
||||
IMPORT
|
||||
|
|
|
@ -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<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
|
||||
|
@ -84,27 +90,29 @@ class NewPlaylistDialog : ViewBindingMaterialDialogFragment<DialogPlaylistNameBi
|
|||
|
||||
// --- VIEWMODEL SETUP ---
|
||||
musicModel.playlistDecision.consume()
|
||||
pickerModel.setPendingPlaylist(requireContext(), args.songUids, args.reason)
|
||||
if (!initializedField) {
|
||||
pickerModel.setPendingPlaylist(requireContext(), args.songUids, args.template, args.reason)
|
||||
|
||||
collectImmediately(pickerModel.currentPendingNewPlaylist, ::updatePendingPlaylist)
|
||||
collectImmediately(pickerModel.chosenName, ::updateChosenName)
|
||||
}
|
||||
|
||||
private fun updatePendingPlaylist(pendingNewPlaylist: PendingNewPlaylist?) {
|
||||
if (pendingNewPlaylist == null) {
|
||||
logD("No playlist to create, leaving")
|
||||
findNavController().navigateUp()
|
||||
return
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
collectImmediately(pickerModel.currentPendingPlaylist, ::updatePendingPlaylist)
|
||||
collectImmediately(pickerModel.chosenName, ::updateChosenName)
|
||||
}
|
||||
|
||||
private fun updatePendingPlaylist(pendingPlaylist: PendingPlaylist?) {
|
||||
if (pendingPlaylist == null) {
|
||||
logD("No playlist to create, leaving")
|
||||
findNavController().navigateUp()
|
||||
return
|
||||
}
|
||||
|
||||
requireBinding().playlistName.hint = pendingPlaylist.preferredName
|
||||
}
|
||||
|
||||
private fun updateChosenName(chosenName: ChosenName) {
|
||||
|
|
|
@ -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. */
|
||||
|
@ -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
|
||||
|
||||
|
@ -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 ->
|
||||
|
@ -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<Music.UID>,
|
||||
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,15 +185,27 @@ 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<Music.UID>,
|
||||
template: String?,
|
||||
reason: PlaylistDecision.Rename.Reason
|
||||
) {
|
||||
logD("Opening playlist $playlistUid to rename")
|
||||
_currentPlaylistToRename.value = musicRepository.userLibrary?.findPlaylist(playlistUid)
|
||||
if (_currentPlaylistToDelete.value == null) {
|
||||
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<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.
|
||||
*
|
||||
|
|
|
@ -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<DialogPlaylistNam
|
|||
builder
|
||||
.setTitle(R.string.lbl_rename_playlist)
|
||||
.setPositiveButton(R.string.lbl_ok) { _, _ ->
|
||||
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<DialogPlaylistNam
|
|||
|
||||
// --- VIEWMODEL SETUP ---
|
||||
musicModel.playlistDecision.consume()
|
||||
pickerModel.setPlaylistToRename(args.playlistUid)
|
||||
collectImmediately(pickerModel.currentPlaylistToRename, ::updatePlaylistToRename)
|
||||
pickerModel.setPlaylistToRename(
|
||||
args.playlistUid, args.applySongUids, args.template, args.reason)
|
||||
collectImmediately(pickerModel.currentPendingRenamePlaylist, ::updatePlaylistToRename)
|
||||
collectImmediately(pickerModel.chosenName, ::updateChosenName)
|
||||
}
|
||||
|
||||
private fun updatePlaylistToRename(playlist: Playlist?) {
|
||||
if (playlist == null) {
|
||||
private fun updatePlaylistToRename(pendingRenamePlaylist: PendingRenamePlaylist?) {
|
||||
if (pendingRenamePlaylist == null) {
|
||||
// Nothing to rename anymore.
|
||||
findNavController().navigateUp()
|
||||
return
|
||||
}
|
||||
|
||||
if (!initializedField) {
|
||||
val default = playlist.name.resolve(requireContext())
|
||||
val default =
|
||||
pendingRenamePlaylist.template
|
||||
?: pendingRenamePlaylist.playlist.name.resolve(requireContext())
|
||||
logD("Name input is not initialized, setting to $default")
|
||||
requireBinding().playlistName.setText(default)
|
||||
initializedField = true
|
||||
|
|
|
@ -321,7 +321,11 @@ class SearchFragment : ListFragment<Music, FragmentSearchBinding>() {
|
|||
}
|
||||
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}")
|
||||
|
|
|
@ -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" />
|
||||
|
||||
|
|
|
@ -430,6 +430,16 @@
|
|||
<argument
|
||||
android:name="playlistUid"
|
||||
app:argType="org.oxycblt.auxio.music.Music$UID" />
|
||||
<argument
|
||||
android:name="template"
|
||||
app:argType="string"
|
||||
app:nullable="true" />
|
||||
<argument
|
||||
android:name="applySongUids"
|
||||
app:argType="org.oxycblt.auxio.music.Music$UID[]" />
|
||||
<argument
|
||||
android:name="reason"
|
||||
app:argType="org.oxycblt.auxio.music.PlaylistDecision$Rename$Reason" />
|
||||
</dialog>
|
||||
|
||||
<dialog
|
||||
|
|
Loading…
Reference in a new issue