From e2104c58b8f8fd7ec0abcf7ecfecae621141c6ec Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Fri, 12 May 2023 15:42:30 -0600 Subject: [PATCH] music: clean up playlist name dialog Cleanup the playlist naming dialog to have nicer UX/implementation. --- .../java/org/oxycblt/auxio/MainFragment.kt | 9 +- .../org/oxycblt/auxio/music/MusicViewModel.kt | 8 +- .../auxio/music/dialog/NewPlaylistDialog.kt | 29 +++--- .../music/dialog/PlaylistDialogViewModel.kt | 91 +++++++------------ ...kerDialog.kt => NavigateToArtistDialog.kt} | 6 +- ...ickerDialog.kt => PlayFromArtistDialog.kt} | 6 +- ...PickerDialog.kt => PlayFromGenreDialog.kt} | 6 +- app/src/main/res/navigation/nav_main.xml | 28 +++--- 8 files changed, 78 insertions(+), 105 deletions(-) rename app/src/main/java/org/oxycblt/auxio/navigation/dialog/{ArtistNavigationPickerDialog.kt => NavigateToArtistDialog.kt} (96%) rename app/src/main/java/org/oxycblt/auxio/playback/dialog/{ArtistPlaybackPickerDialog.kt => PlayFromArtistDialog.kt} (96%) rename app/src/main/java/org/oxycblt/auxio/playback/dialog/{GenrePlaybackPickerDialog.kt => PlayFromGenreDialog.kt} (96%) diff --git a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt index c06b4536b..783f16d64 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt @@ -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() } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt index 6f0075afb..b0cf77587 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt @@ -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 get() = _statistics - private val _pendingNewPlaylist = MutableEvent() - val pendingNewPlaylist: Event = _pendingNewPlaylist + private val _pendingNewPlaylist = MutableEvent() + val pendingNewPlaylist: Event = _pendingNewPlaylist init { musicRepository.addUpdateListener(this) @@ -115,7 +115,7 @@ class MusicViewModel @Inject constructor(private val musicRepository: MusicRepos */ fun createPlaylist(name: String, songs: List = 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 })) } /** diff --git a/app/src/main/java/org/oxycblt/auxio/music/dialog/NewPlaylistDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/dialog/NewPlaylistDialog.kt index 944db7fc2..8bfcf68e1 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/dialog/NewPlaylistDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/dialog/NewPlaylistDialog.kt @@ -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() // 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() 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 } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/dialog/PlaylistDialogViewModel.kt b/app/src/main/java/org/oxycblt/auxio/music/dialog/PlaylistDialogViewModel.kt index 94eb53280..b0370c618 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/dialog/PlaylistDialogViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/dialog/PlaylistDialogViewModel.kt @@ -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(null) - val currentPendingName: StateFlow = _currentPendingName + var pendingPlaylist: PendingPlaylist? = null + private set + + private val _pendingPlaylistValid = MutableStateFlow(false) + val pendingPlaylistValid: StateFlow = _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, 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) : Parcelable -} +@Parcelize data class PendingPlaylist(val name: String, val songUids: List) : Parcelable diff --git a/app/src/main/java/org/oxycblt/auxio/navigation/dialog/ArtistNavigationPickerDialog.kt b/app/src/main/java/org/oxycblt/auxio/navigation/dialog/NavigateToArtistDialog.kt similarity index 96% rename from app/src/main/java/org/oxycblt/auxio/navigation/dialog/ArtistNavigationPickerDialog.kt rename to app/src/main/java/org/oxycblt/auxio/navigation/dialog/NavigateToArtistDialog.kt index a3a1647a4..d90cc1d53 100644 --- a/app/src/main/java/org/oxycblt/auxio/navigation/dialog/ArtistNavigationPickerDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/navigation/dialog/NavigateToArtistDialog.kt @@ -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(), ClickableListListener { 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) { diff --git a/app/src/main/java/org/oxycblt/auxio/playback/dialog/ArtistPlaybackPickerDialog.kt b/app/src/main/java/org/oxycblt/auxio/playback/dialog/PlayFromArtistDialog.kt similarity index 96% rename from app/src/main/java/org/oxycblt/auxio/playback/dialog/ArtistPlaybackPickerDialog.kt rename to app/src/main/java/org/oxycblt/auxio/playback/dialog/PlayFromArtistDialog.kt index d1fc49000..0b76b75ea 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/dialog/ArtistPlaybackPickerDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/dialog/PlayFromArtistDialog.kt @@ -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(), ClickableListListener { 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) { diff --git a/app/src/main/java/org/oxycblt/auxio/playback/dialog/GenrePlaybackPickerDialog.kt b/app/src/main/java/org/oxycblt/auxio/playback/dialog/PlayFromGenreDialog.kt similarity index 96% rename from app/src/main/java/org/oxycblt/auxio/playback/dialog/GenrePlaybackPickerDialog.kt rename to app/src/main/java/org/oxycblt/auxio/playback/dialog/PlayFromGenreDialog.kt index d80157002..008fe1ee1 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/dialog/GenrePlaybackPickerDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/dialog/PlayFromGenreDialog.kt @@ -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(), ClickableListListener { 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) { diff --git a/app/src/main/res/navigation/nav_main.xml b/app/src/main/res/navigation/nav_main.xml index 244d189ca..8e3e4761d 100644 --- a/app/src/main/res/navigation/nav_main.xml +++ b/app/src/main/res/navigation/nav_main.xml @@ -22,13 +22,13 @@ app:destination="@id/new_playlist_dialog" /> + app:destination="@id/navigate_to_artist_dialog" /> + app:destination="@id/play_from_artist_dialog" /> + app:destination="@id/play_from_genre_dialog" /> + android:name="pendingPlaylist" + app:argType="org.oxycblt.auxio.music.dialog.PendingPlaylist" />