music: clean up playlist name dialog

Cleanup the playlist naming dialog to have nicer UX/implementation.
This commit is contained in:
Alexander Capehart 2023-05-12 15:42:30 -06:00
parent 97705a37e4
commit e2104c58b8
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
8 changed files with 78 additions and 105 deletions

View file

@ -42,7 +42,7 @@ import org.oxycblt.auxio.list.selection.SelectionViewModel
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.dialog.PendingName
import org.oxycblt.auxio.music.dialog.PendingPlaylist
import org.oxycblt.auxio.navigation.MainNavigationAction
import org.oxycblt.auxio.navigation.NavigationViewModel
import org.oxycblt.auxio.playback.PlaybackBottomSheetBehavior
@ -304,9 +304,10 @@ class MainFragment :
}
}
private fun handlePlaylistNaming(args: PendingName.Args?) {
if (args != null) {
findNavController().navigateSafe(MainFragmentDirections.actionNewPlaylist(args))
private fun handlePlaylistNaming(pendingPlaylist: PendingPlaylist?) {
if (pendingPlaylist != null) {
findNavController()
.navigateSafe(MainFragmentDirections.actionNewPlaylist(pendingPlaylist))
musicModel.pendingNewPlaylist.consume()
}
}

View file

@ -25,7 +25,7 @@ import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.dialog.PendingName
import org.oxycblt.auxio.music.dialog.PendingPlaylist
import org.oxycblt.auxio.util.Event
import org.oxycblt.auxio.util.MutableEvent
@ -47,8 +47,8 @@ class MusicViewModel @Inject constructor(private val musicRepository: MusicRepos
val statistics: StateFlow<Statistics?>
get() = _statistics
private val _pendingNewPlaylist = MutableEvent<PendingName.Args?>()
val pendingNewPlaylist: Event<PendingName.Args?> = _pendingNewPlaylist
private val _pendingNewPlaylist = MutableEvent<PendingPlaylist?>()
val pendingNewPlaylist: Event<PendingPlaylist?> = _pendingNewPlaylist
init {
musicRepository.addUpdateListener(this)
@ -115,7 +115,7 @@ class MusicViewModel @Inject constructor(private val musicRepository: MusicRepos
*/
fun createPlaylist(name: String, songs: List<Song> = listOf()) {
// TODO: Attempt to unify playlist creation flow with dialog model
_pendingNewPlaylist.put(PendingName.Args(name, songs.map { it.uid }))
_pendingNewPlaylist.put(PendingPlaylist(name, songs.map { it.uid }))
}
/**

View file

@ -23,13 +23,13 @@ import android.view.LayoutInflater
import androidx.appcompat.app.AlertDialog
import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import dagger.hilt.android.AndroidEntryPoint
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogPlaylistNameBinding
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.unlikelyToBeNull
/**
* A dialog allowing the name of a new/existing playlist to be edited.
@ -44,7 +44,6 @@ class NewPlaylistDialog : ViewBindingDialogFragment<DialogPlaylistNameBinding>()
// 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 initializedInput = false
override fun onConfigDialog(builder: AlertDialog.Builder) {
builder
@ -59,26 +58,20 @@ class NewPlaylistDialog : ViewBindingDialogFragment<DialogPlaylistNameBinding>()
override fun onBindingCreated(binding: DialogPlaylistNameBinding, savedInstanceState: Bundle?) {
super.onBindingCreated(binding, savedInstanceState)
binding.playlistName.addTextChangedListener {
dialogModel.updatePendingName(it?.toString())
binding.playlistName.apply {
hint = args.pendingPlaylist.name
addTextChangedListener {
dialogModel.updatePendingName(
(if (it.isNullOrEmpty()) unlikelyToBeNull(hint) else it).toString())
}
}
dialogModel.setPendingName(args.pendingName)
collectImmediately(dialogModel.currentPendingName, ::updatePendingName)
dialogModel.setPendingName(args.pendingPlaylist)
collectImmediately(dialogModel.pendingPlaylistValid, ::updateValid)
}
private fun updatePendingName(pendingName: PendingName?) {
if (pendingName == null) {
findNavController().navigateUp()
return
}
// Make sure we initialize the TextView with the preferred name if we haven't already.
if (!initializedInput) {
requireBinding().playlistName.setText(pendingName.name)
initializedInput = true
}
private fun updateValid(valid: Boolean) {
// Disable the OK button if the name is invalid (empty or whitespace)
(dialog as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE)?.isEnabled =
pendingName.valid
(dialog as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE)?.isEnabled = valid
}
}

View file

@ -28,6 +28,7 @@ import kotlinx.parcelize.Parcelize
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicRepository
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.util.logD
/**
* A [ViewModel] managing the state of the playlist editing dialogs.
@ -37,33 +38,18 @@ import org.oxycblt.auxio.music.Song
@HiltViewModel
class PlaylistDialogViewModel @Inject constructor(private val musicRepository: MusicRepository) :
ViewModel(), MusicRepository.UpdateListener {
private val _currentPendingName = MutableStateFlow<PendingName?>(null)
val currentPendingName: StateFlow<PendingName?> = _currentPendingName
var pendingPlaylist: PendingPlaylist? = null
private set
private val _pendingPlaylistValid = MutableStateFlow(false)
val pendingPlaylistValid: StateFlow<Boolean> = _pendingPlaylistValid
init {
musicRepository.addUpdateListener(this)
}
override fun onMusicChanges(changes: MusicRepository.Changes) {
val pendingName = _currentPendingName.value ?: return
val deviceLibrary = musicRepository.deviceLibrary
val newSongs =
if (changes.deviceLibrary && deviceLibrary != null) {
pendingName.songs.mapNotNull { deviceLibrary.findSong(it.uid) }
} else {
pendingName.songs
}
val userLibrary = musicRepository.userLibrary
val newValid =
if (changes.userLibrary && userLibrary != null) {
validateName(pendingName.name)
} else {
pendingName.valid
}
_currentPendingName.value = PendingName(pendingName.name, newSongs, newValid)
pendingPlaylist?.let(::validateName)
}
override fun onCleared() {
@ -71,58 +57,51 @@ class PlaylistDialogViewModel @Inject constructor(private val musicRepository: M
}
/**
* Update the current [PendingName] based on the given [PendingName.Args].
* Update the current [PendingPlaylist]. Will do nothing if already equal.
*
* @param args The [PendingName.Args] to update with.
* @param pendingPlaylist The [PendingPlaylist] to update with.
*/
fun setPendingName(args: PendingName.Args) {
val deviceLibrary = musicRepository.deviceLibrary ?: return
val name =
PendingName(
args.preferredName,
args.songUids.mapNotNull(deviceLibrary::findSong),
validateName(args.preferredName))
_currentPendingName.value = name
fun setPendingName(pendingPlaylist: PendingPlaylist) {
if (this.pendingPlaylist == pendingPlaylist) return
this.pendingPlaylist = pendingPlaylist
validateName(pendingPlaylist)
}
/**
* Update the current [PendingName] based on new user input.
* Update the current [PendingPlaylist] based on new user input.
*
* @param name The new user-inputted name, directly from the UI.
* @param name The new user-inputted name.
*/
fun updatePendingName(name: String?) {
fun updatePendingName(name: String) {
val current = pendingPlaylist ?: return
// Remove any additional whitespace from the string to be consistent with all other
// music items.
val normalized = (name ?: return).trim()
_currentPendingName.value =
_currentPendingName.value?.run { PendingName(normalized, songs, validateName(name)) }
val new = PendingPlaylist(name.trim(), current.songUids)
pendingPlaylist = new
validateName(new)
}
/** Confirm the current [PendingName] operation and write it to the database. */
/** Confirm the current [PendingPlaylist] operation and write it to the database. */
fun confirmPendingName() {
val pendingName = _currentPendingName.value ?: return
musicRepository.createPlaylist(pendingName.name, pendingName.songs)
_currentPendingName.value = null
val playlist = pendingPlaylist ?: return
val deviceLibrary = musicRepository.deviceLibrary ?: return
musicRepository.createPlaylist(
playlist.name, playlist.songUids.mapNotNull(deviceLibrary::findSong))
}
private fun validateName(name: String) =
name.isNotBlank() && musicRepository.userLibrary?.findPlaylist(name) == null
private fun validateName(pendingPlaylist: PendingPlaylist) {
val userLibrary = musicRepository.userLibrary
_pendingPlaylistValid.value =
pendingPlaylist.name.isNotBlank() &&
userLibrary != null &&
userLibrary.findPlaylist(pendingPlaylist.name) == null
}
}
/**
* Represents the current state of a name operation.
* Represents a playlist that is currently being named before actually being completed.
*
* @param name The name of the playlist.
* @param songs Any songs that will be in the playlist when added.
* @param valid Whether the current configuration is valid.
* @param songUids The [Music.UID]s of the [Song]s to be contained by the playlist.
*/
data class PendingName(val name: String, val songs: List<Song>, val valid: Boolean) {
/**
* A [Parcelable] version of [PendingName], to be used as a dialog argument.
*
* @param preferredName The name to be used initially by the dialog.
* @param songUids The [Music.UID] of any pending [Song]s that will be put in the playlist.
*/
@Parcelize
data class Args(val preferredName: String, val songUids: List<Music.UID>) : Parcelable
}
@Parcelize data class PendingPlaylist(val name: String, val songUids: List<Music.UID>) : Parcelable

View file

@ -1,6 +1,6 @@
/*
* Copyright (c) 2022 Auxio Project
* ArtistNavigationPickerDialog.kt is part of Auxio.
* NavigateToArtistDialog.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -45,13 +45,13 @@ import org.oxycblt.auxio.util.collectImmediately
* @author Alexander Capehart (OxygenCobalt)
*/
@AndroidEntryPoint
class ArtistNavigationPickerDialog :
class NavigateToArtistDialog :
ViewBindingDialogFragment<DialogMusicPickerBinding>(), ClickableListListener<Artist> {
private val navigationModel: NavigationViewModel by activityViewModels()
private val pickerModel: NavigationDialogViewModel by viewModels()
// Information about what artists to show choices for is initially within the navigation
// arguments as UIDs, as that is the only safe way to parcel an artist.
private val args: ArtistNavigationPickerDialogArgs by navArgs()
private val args: NavigateToArtistDialogArgs by navArgs()
private val choiceAdapter = ArtistChoiceAdapter(this)
override fun onConfigDialog(builder: AlertDialog.Builder) {

View file

@ -1,6 +1,6 @@
/*
* Copyright (c) 2022 Auxio Project
* ArtistPlaybackPickerDialog.kt is part of Auxio.
* PlayFromArtistDialog.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -46,13 +46,13 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
* @author Alexander Capehart (OxygenCobalt)
*/
@AndroidEntryPoint
class ArtistPlaybackPickerDialog :
class PlayFromArtistDialog :
ViewBindingDialogFragment<DialogMusicPickerBinding>(), ClickableListListener<Artist> {
private val playbackModel: PlaybackViewModel by activityViewModels()
private val pickerModel: PlaybackDialogViewModel by viewModels()
// Information about what Song to show choices for is initially within the navigation arguments
// as UIDs, as that is the only safe way to parcel a Song.
private val args: ArtistPlaybackPickerDialogArgs by navArgs()
private val args: PlayFromArtistDialogArgs by navArgs()
private val choiceAdapter = ArtistChoiceAdapter(this)
override fun onConfigDialog(builder: AlertDialog.Builder) {

View file

@ -1,6 +1,6 @@
/*
* Copyright (c) 2022 Auxio Project
* GenrePlaybackPickerDialog.kt is part of Auxio.
* PlayFromGenreDialog.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -46,13 +46,13 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
* @author Alexander Capehart (OxygenCobalt)
*/
@AndroidEntryPoint
class GenrePlaybackPickerDialog :
class PlayFromGenreDialog :
ViewBindingDialogFragment<DialogMusicPickerBinding>(), ClickableListListener<Genre> {
private val playbackModel: PlaybackViewModel by activityViewModels()
private val pickerModel: PlaybackDialogViewModel by viewModels()
// Information about what Song to show choices for is initially within the navigation arguments
// as UIDs, as that is the only safe way to parcel a Song.
private val args: GenrePlaybackPickerDialogArgs by navArgs()
private val args: PlayFromGenreDialogArgs by navArgs()
private val choiceAdapter = GenreChoiceAdapter(this)
override fun onConfigDialog(builder: AlertDialog.Builder) {

View file

@ -22,13 +22,13 @@
app:destination="@id/new_playlist_dialog" />
<action
android:id="@+id/action_pick_navigation_artist"
app:destination="@id/artist_navigation_picker_dialog" />
app:destination="@id/navigate_to_artist_dialog" />
<action
android:id="@+id/action_pick_playback_artist"
app:destination="@id/artist_playback_picker_dialog" />
app:destination="@id/play_from_artist_dialog" />
<action
android:id="@+id/action_pick_playback_genre"
app:destination="@id/genre_playback_picker_dialog" />
app:destination="@id/play_from_genre_dialog" />
</fragment>
<dialog
@ -47,14 +47,14 @@
android:label="new_playlist_dialog"
tools:layout="@layout/dialog_playlist_name">
<argument
android:name="pendingName"
app:argType="org.oxycblt.auxio.music.dialog.PendingName$Args" />
android:name="pendingPlaylist"
app:argType="org.oxycblt.auxio.music.dialog.PendingPlaylist" />
</dialog>
<dialog
android:id="@+id/artist_navigation_picker_dialog"
android:name="org.oxycblt.auxio.navigation.dialog.ArtistNavigationPickerDialog"
android:label="artist_navigation_picker_dialog"
android:id="@+id/navigate_to_artist_dialog"
android:name="org.oxycblt.auxio.navigation.dialog.NavigateToArtistDialog"
android:label="navigate_to_artist_dialog"
tools:layout="@layout/dialog_music_picker">
<argument
android:name="artistUid"
@ -62,9 +62,9 @@
</dialog>
<dialog
android:id="@+id/artist_playback_picker_dialog"
android:name="org.oxycblt.auxio.playback.dialog.ArtistPlaybackPickerDialog"
android:label="artist_playback_picker_dialog"
android:id="@+id/play_from_artist_dialog"
android:name="org.oxycblt.auxio.playback.dialog.PlayFromArtistDialog"
android:label="play_from_artist_dialog"
tools:layout="@layout/dialog_music_picker">
<argument
android:name="artistUid"
@ -72,9 +72,9 @@
</dialog>
<dialog
android:id="@+id/genre_playback_picker_dialog"
android:name="org.oxycblt.auxio.playback.dialog.GenrePlaybackPickerDialog"
android:label="genre_playback_picker_dialog"
android:id="@+id/play_from_genre_dialog"
android:name="org.oxycblt.auxio.playback.dialog.PlayFromGenreDialog"
android:label="play_from_genre_dialog"
tools:layout="@layout/dialog_music_picker">
<argument
android:name="genreUid"