music: clean up playlist name dialog
Cleanup the playlist naming dialog to have nicer UX/implementation.
This commit is contained in:
parent
97705a37e4
commit
e2104c58b8
8 changed files with 78 additions and 105 deletions
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 }))
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
|
@ -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) {
|
|
@ -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) {
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue