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

View file

@ -25,7 +25,7 @@ import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import org.oxycblt.auxio.R 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.Event
import org.oxycblt.auxio.util.MutableEvent import org.oxycblt.auxio.util.MutableEvent
@ -47,8 +47,8 @@ class MusicViewModel @Inject constructor(private val musicRepository: MusicRepos
val statistics: StateFlow<Statistics?> val statistics: StateFlow<Statistics?>
get() = _statistics get() = _statistics
private val _pendingNewPlaylist = MutableEvent<PendingName.Args?>() private val _pendingNewPlaylist = MutableEvent<PendingPlaylist?>()
val pendingNewPlaylist: Event<PendingName.Args?> = _pendingNewPlaylist val pendingNewPlaylist: Event<PendingPlaylist?> = _pendingNewPlaylist
init { init {
musicRepository.addUpdateListener(this) musicRepository.addUpdateListener(this)
@ -115,7 +115,7 @@ class MusicViewModel @Inject constructor(private val musicRepository: MusicRepos
*/ */
fun createPlaylist(name: String, songs: List<Song> = listOf()) { fun createPlaylist(name: String, songs: List<Song> = listOf()) {
// TODO: Attempt to unify playlist creation flow with dialog model // 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.appcompat.app.AlertDialog
import androidx.core.widget.addTextChangedListener import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogPlaylistNameBinding import org.oxycblt.auxio.databinding.DialogPlaylistNameBinding
import org.oxycblt.auxio.ui.ViewBindingDialogFragment import org.oxycblt.auxio.ui.ViewBindingDialogFragment
import org.oxycblt.auxio.util.collectImmediately 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. * 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 // 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 initializedInput = false
override fun onConfigDialog(builder: AlertDialog.Builder) { override fun onConfigDialog(builder: AlertDialog.Builder) {
builder builder
@ -59,26 +58,20 @@ class NewPlaylistDialog : ViewBindingDialogFragment<DialogPlaylistNameBinding>()
override fun onBindingCreated(binding: DialogPlaylistNameBinding, savedInstanceState: Bundle?) { override fun onBindingCreated(binding: DialogPlaylistNameBinding, savedInstanceState: Bundle?) {
super.onBindingCreated(binding, savedInstanceState) super.onBindingCreated(binding, savedInstanceState)
binding.playlistName.addTextChangedListener { binding.playlistName.apply {
dialogModel.updatePendingName(it?.toString()) hint = args.pendingPlaylist.name
addTextChangedListener {
dialogModel.updatePendingName(
(if (it.isNullOrEmpty()) unlikelyToBeNull(hint) else it).toString())
}
} }
dialogModel.setPendingName(args.pendingName) dialogModel.setPendingName(args.pendingPlaylist)
collectImmediately(dialogModel.currentPendingName, ::updatePendingName) collectImmediately(dialogModel.pendingPlaylistValid, ::updateValid)
} }
private fun updatePendingName(pendingName: PendingName?) { private fun updateValid(valid: Boolean) {
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
}
// Disable the OK button if the name is invalid (empty or whitespace) // Disable the OK button if the name is invalid (empty or whitespace)
(dialog as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE)?.isEnabled = (dialog as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE)?.isEnabled = valid
pendingName.valid
} }
} }

View file

@ -28,6 +28,7 @@ import kotlinx.parcelize.Parcelize
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicRepository import org.oxycblt.auxio.music.MusicRepository
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.util.logD
/** /**
* A [ViewModel] managing the state of the playlist editing dialogs. * A [ViewModel] managing the state of the playlist editing dialogs.
@ -37,33 +38,18 @@ import org.oxycblt.auxio.music.Song
@HiltViewModel @HiltViewModel
class PlaylistDialogViewModel @Inject constructor(private val musicRepository: MusicRepository) : class PlaylistDialogViewModel @Inject constructor(private val musicRepository: MusicRepository) :
ViewModel(), MusicRepository.UpdateListener { ViewModel(), MusicRepository.UpdateListener {
private val _currentPendingName = MutableStateFlow<PendingName?>(null) var pendingPlaylist: PendingPlaylist? = null
val currentPendingName: StateFlow<PendingName?> = _currentPendingName private set
private val _pendingPlaylistValid = MutableStateFlow(false)
val pendingPlaylistValid: StateFlow<Boolean> = _pendingPlaylistValid
init { init {
musicRepository.addUpdateListener(this) musicRepository.addUpdateListener(this)
} }
override fun onMusicChanges(changes: MusicRepository.Changes) { override fun onMusicChanges(changes: MusicRepository.Changes) {
val pendingName = _currentPendingName.value ?: return pendingPlaylist?.let(::validateName)
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)
} }
override fun onCleared() { 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) { fun setPendingName(pendingPlaylist: PendingPlaylist) {
val deviceLibrary = musicRepository.deviceLibrary ?: return if (this.pendingPlaylist == pendingPlaylist) return
val name = this.pendingPlaylist = pendingPlaylist
PendingName( validateName(pendingPlaylist)
args.preferredName,
args.songUids.mapNotNull(deviceLibrary::findSong),
validateName(args.preferredName))
_currentPendingName.value = name
} }
/** /**
* 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 // Remove any additional whitespace from the string to be consistent with all other
// music items. // music items.
val normalized = (name ?: return).trim() val new = PendingPlaylist(name.trim(), current.songUids)
_currentPendingName.value = pendingPlaylist = new
_currentPendingName.value?.run { PendingName(normalized, songs, validateName(name)) } 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() { fun confirmPendingName() {
val pendingName = _currentPendingName.value ?: return val playlist = pendingPlaylist ?: return
musicRepository.createPlaylist(pendingName.name, pendingName.songs) val deviceLibrary = musicRepository.deviceLibrary ?: return
_currentPendingName.value = null musicRepository.createPlaylist(
playlist.name, playlist.songUids.mapNotNull(deviceLibrary::findSong))
} }
private fun validateName(name: String) = private fun validateName(pendingPlaylist: PendingPlaylist) {
name.isNotBlank() && musicRepository.userLibrary?.findPlaylist(name) == null 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 name The name of the playlist.
* @param songs Any songs that will be in the playlist when added. * @param songUids The [Music.UID]s of the [Song]s to be contained by the playlist.
* @param valid Whether the current configuration is valid.
*/ */
data class PendingName(val name: String, val songs: List<Song>, val valid: Boolean) { @Parcelize data class PendingPlaylist(val name: String, val songUids: List<Music.UID>) : Parcelable
/**
* 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
}

View file

@ -1,6 +1,6 @@
/* /*
* Copyright (c) 2022 Auxio Project * 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 * 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 * 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) * @author Alexander Capehart (OxygenCobalt)
*/ */
@AndroidEntryPoint @AndroidEntryPoint
class ArtistNavigationPickerDialog : class NavigateToArtistDialog :
ViewBindingDialogFragment<DialogMusicPickerBinding>(), ClickableListListener<Artist> { ViewBindingDialogFragment<DialogMusicPickerBinding>(), ClickableListListener<Artist> {
private val navigationModel: NavigationViewModel by activityViewModels() private val navigationModel: NavigationViewModel by activityViewModels()
private val pickerModel: NavigationDialogViewModel by viewModels() private val pickerModel: NavigationDialogViewModel by viewModels()
// Information about what artists to show choices for is initially within the navigation // 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. // 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) private val choiceAdapter = ArtistChoiceAdapter(this)
override fun onConfigDialog(builder: AlertDialog.Builder) { override fun onConfigDialog(builder: AlertDialog.Builder) {

View file

@ -1,6 +1,6 @@
/* /*
* Copyright (c) 2022 Auxio Project * 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 * 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 * 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) * @author Alexander Capehart (OxygenCobalt)
*/ */
@AndroidEntryPoint @AndroidEntryPoint
class ArtistPlaybackPickerDialog : class PlayFromArtistDialog :
ViewBindingDialogFragment<DialogMusicPickerBinding>(), ClickableListListener<Artist> { ViewBindingDialogFragment<DialogMusicPickerBinding>(), ClickableListListener<Artist> {
private val playbackModel: PlaybackViewModel by activityViewModels() private val playbackModel: PlaybackViewModel by activityViewModels()
private val pickerModel: PlaybackDialogViewModel by viewModels() private val pickerModel: PlaybackDialogViewModel by viewModels()
// Information about what Song to show choices for is initially within the navigation arguments // 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. // 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) private val choiceAdapter = ArtistChoiceAdapter(this)
override fun onConfigDialog(builder: AlertDialog.Builder) { override fun onConfigDialog(builder: AlertDialog.Builder) {

View file

@ -1,6 +1,6 @@
/* /*
* Copyright (c) 2022 Auxio Project * 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 * 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 * 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) * @author Alexander Capehart (OxygenCobalt)
*/ */
@AndroidEntryPoint @AndroidEntryPoint
class GenrePlaybackPickerDialog : class PlayFromGenreDialog :
ViewBindingDialogFragment<DialogMusicPickerBinding>(), ClickableListListener<Genre> { ViewBindingDialogFragment<DialogMusicPickerBinding>(), ClickableListListener<Genre> {
private val playbackModel: PlaybackViewModel by activityViewModels() private val playbackModel: PlaybackViewModel by activityViewModels()
private val pickerModel: PlaybackDialogViewModel by viewModels() private val pickerModel: PlaybackDialogViewModel by viewModels()
// Information about what Song to show choices for is initially within the navigation arguments // 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. // 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) private val choiceAdapter = GenreChoiceAdapter(this)
override fun onConfigDialog(builder: AlertDialog.Builder) { override fun onConfigDialog(builder: AlertDialog.Builder) {

View file

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