music: streamline new playlist implementation

Make the implementation of the playlist creation dialog signifigantly
simpler by removing some aspects that don't really need implementation
yet.
This commit is contained in:
Alexander Capehart 2023-05-13 11:39:51 -06:00
parent 13709e3e8e
commit 4fe91c25e3
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
17 changed files with 236 additions and 184 deletions

View file

@ -42,7 +42,6 @@ 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.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
@ -135,7 +134,7 @@ class MainFragment :
collect(navModel.mainNavigationAction.flow, ::handleMainNavigation) collect(navModel.mainNavigationAction.flow, ::handleMainNavigation)
collect(navModel.exploreNavigationItem.flow, ::handleExploreNavigation) collect(navModel.exploreNavigationItem.flow, ::handleExploreNavigation)
collect(navModel.exploreArtistNavigationItem.flow, ::handleArtistNavigationPicker) collect(navModel.exploreArtistNavigationItem.flow, ::handleArtistNavigationPicker)
collect(musicModel.pendingNewPlaylist.flow, ::handlePlaylistNaming) collect(musicModel.newPlaylistSongs.flow, ::handleNewPlaylist)
collectImmediately(playbackModel.song, ::updateSong) collectImmediately(playbackModel.song, ::updateSong)
collect(playbackModel.artistPickerSong.flow, ::handlePlaybackArtistPicker) collect(playbackModel.artistPickerSong.flow, ::handlePlaybackArtistPicker)
collect(playbackModel.genrePickerSong.flow, ::handlePlaybackGenrePicker) collect(playbackModel.genrePickerSong.flow, ::handlePlaybackGenrePicker)
@ -304,11 +303,12 @@ class MainFragment :
} }
} }
private fun handlePlaylistNaming(pendingPlaylist: PendingPlaylist?) { private fun handleNewPlaylist(songs: List<Song>?) {
if (pendingPlaylist != null) { if (songs != null) {
findNavController() findNavController()
.navigateSafe(MainFragmentDirections.actionNewPlaylist(pendingPlaylist)) .navigateSafe(
musicModel.pendingNewPlaylist.consume() MainFragmentDirections.actionNewPlaylist(songs.map { it.uid }.toTypedArray()))
musicModel.newPlaylistSongs.consume()
} }
} }

View file

@ -321,7 +321,7 @@ class HomeFragment :
} }
} else { } else {
binding.homeFab.flipTo(R.drawable.ic_add_24, R.string.desc_new_playlist) { binding.homeFab.flipTo(R.drawable.ic_add_24, R.string.desc_new_playlist) {
musicModel.createPlaylist(requireContext()) musicModel.createPlaylist()
} }
} }
} }

View file

@ -18,14 +18,11 @@
package org.oxycblt.auxio.music package org.oxycblt.auxio.music
import android.content.Context
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject 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.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 +44,9 @@ class MusicViewModel @Inject constructor(private val musicRepository: MusicRepos
val statistics: StateFlow<Statistics?> val statistics: StateFlow<Statistics?>
get() = _statistics get() = _statistics
private val _pendingNewPlaylist = MutableEvent<PendingPlaylist?>() private val _newPlaylistSongs = MutableEvent<List<Song>?>()
val pendingNewPlaylist: Event<PendingPlaylist?> = _pendingNewPlaylist /** Flag for opening a dialog to create a playlist of the given [Song]s. */
val newPlaylistSongs: Event<List<Song>?> = _newPlaylistSongs
init { init {
musicRepository.addUpdateListener(this) musicRepository.addUpdateListener(this)
@ -87,35 +85,23 @@ class MusicViewModel @Inject constructor(private val musicRepository: MusicRepos
} }
/** /**
* Create a new generic playlist. This will automatically generate a playlist name and then * Create a new generic playlist. This will first open a dialog for the user to make a naming
* prompt the user to edit the name before the creation finished. * choice before committing the playlist to the database.
* *
* @param context The [Context] required to generate the playlist name.
* @param songs The [Song]s to be contained in the new playlist. * @param songs The [Song]s to be contained in the new playlist.
*/ */
fun createPlaylist(context: Context, songs: List<Song> = listOf()) { fun createPlaylist(songs: List<Song> = listOf()) {
val userLibrary = musicRepository.userLibrary ?: return _newPlaylistSongs.put(songs)
var i = 1
while (true) {
val possibleName = context.getString(R.string.fmt_def_playlist, i)
if (userLibrary.playlists.none { it.name.resolve(context) == possibleName }) {
createPlaylist(possibleName, songs)
return
}
++i
}
} }
/** /**
* Create a new generic playlist. This will prompt the user to edit the name before the creation * Create a new generic playlist. This will immediately commit the playlist to the database.
* finishes.
* *
* @param name The preferred name of the new playlist. * @param name The name of the new playlist.
* @param songs The [Song]s to be contained in the new playlist. * @param songs The [Song]s to be contained in the new playlist.
*/ */
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 musicRepository.createPlaylist(name, songs)
_pendingNewPlaylist.put(PendingPlaylist(name, songs.map { it.uid }))
} }
/** /**

View file

@ -16,13 +16,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.music.fs package org.oxycblt.auxio.music.config
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.databinding.ItemMusicDirBinding import org.oxycblt.auxio.databinding.ItemMusicDirBinding
import org.oxycblt.auxio.list.recycler.DialogRecyclerView import org.oxycblt.auxio.list.recycler.DialogRecyclerView
import org.oxycblt.auxio.music.fs.Directory
import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.inflater import org.oxycblt.auxio.util.inflater

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.music.fs package org.oxycblt.auxio.music.config
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.net.Uri import android.net.Uri
@ -35,6 +35,8 @@ import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogMusicDirsBinding import org.oxycblt.auxio.databinding.DialogMusicDirsBinding
import org.oxycblt.auxio.music.MusicSettings import org.oxycblt.auxio.music.MusicSettings
import org.oxycblt.auxio.music.fs.Directory
import org.oxycblt.auxio.music.fs.MusicDirectories
import org.oxycblt.auxio.ui.ViewBindingDialogFragment import org.oxycblt.auxio.ui.ViewBindingDialogFragment
import org.oxycblt.auxio.util.getSystemServiceCompat import org.oxycblt.auxio.util.getSystemServiceCompat
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.music.dialog package org.oxycblt.auxio.music.config
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater

View file

@ -1,106 +0,0 @@
/*
* Copyright (c) 2023 Auxio Project
* PlaylistDialogViewModel.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
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.music.dialog
import android.os.Parcelable
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.parcelize.Parcelize
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicRepository
import org.oxycblt.auxio.music.Song
/**
* A [ViewModel] managing the state of the playlist editing dialogs.
*
* @author Alexander Capehart
*/
@HiltViewModel
class PlaylistDialogViewModel @Inject constructor(private val musicRepository: MusicRepository) :
ViewModel(), MusicRepository.UpdateListener {
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) {
pendingPlaylist?.let(::validateName)
}
override fun onCleared() {
musicRepository.removeUpdateListener(this)
}
/**
* Update the current [PendingPlaylist]. Will do nothing if already equal.
*
* @param pendingPlaylist The [PendingPlaylist] to update with.
*/
fun setPendingName(pendingPlaylist: PendingPlaylist) {
if (this.pendingPlaylist == pendingPlaylist) return
this.pendingPlaylist = pendingPlaylist
validateName(pendingPlaylist)
}
/**
* Update the current [PendingPlaylist] based on new user input.
*
* @param name The new user-inputted name.
*/
fun updatePendingName(name: String) {
val current = pendingPlaylist ?: return
// Remove any additional whitespace from the string to be consistent with all other
// music items.
val new = PendingPlaylist(name.trim(), current.songUids)
pendingPlaylist = new
validateName(new)
}
/** Confirm the current [PendingPlaylist] operation and write it to the database. */
fun confirmPendingName() {
val playlist = pendingPlaylist ?: return
val deviceLibrary = musicRepository.deviceLibrary ?: return
musicRepository.createPlaylist(
playlist.name, playlist.songUids.mapNotNull(deviceLibrary::findSong))
}
private fun validateName(pendingPlaylist: PendingPlaylist) {
val userLibrary = musicRepository.userLibrary
_pendingPlaylistValid.value =
pendingPlaylist.name.isNotBlank() &&
userLibrary != null &&
userLibrary.findPlaylist(pendingPlaylist.name) == null
}
}
/**
* Represents a playlist that is currently being named before actually being completed.
*
* @param name The name of the playlist.
* @param songUids The [Music.UID]s of the [Song]s to be contained by the playlist.
*/
@Parcelize data class PendingPlaylist(val name: String, val songUids: List<Music.UID>) : Parcelable

View file

@ -16,17 +16,20 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.music.dialog package org.oxycblt.auxio.music.picker
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater 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.fragment.app.viewModels
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.music.MusicViewModel
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 import org.oxycblt.auxio.util.unlikelyToBeNull
@ -38,9 +41,8 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
*/ */
@AndroidEntryPoint @AndroidEntryPoint
class NewPlaylistDialog : ViewBindingDialogFragment<DialogPlaylistNameBinding>() { class NewPlaylistDialog : ViewBindingDialogFragment<DialogPlaylistNameBinding>() {
// activityViewModels is intentional here as the ViewModel will do work that we private val musicModel: MusicViewModel by activityViewModels()
// do not want to cancel after this dialog closes. private val pickerModel: PlaylistPickerViewModel by viewModels()
private val dialogModel: PlaylistDialogViewModel by activityViewModels()
// 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()
@ -48,7 +50,16 @@ class NewPlaylistDialog : ViewBindingDialogFragment<DialogPlaylistNameBinding>()
override fun onConfigDialog(builder: AlertDialog.Builder) { override fun onConfigDialog(builder: AlertDialog.Builder) {
builder builder
.setTitle(R.string.lbl_new_playlist) .setTitle(R.string.lbl_new_playlist)
.setPositiveButton(R.string.lbl_ok) { _, _ -> dialogModel.confirmPendingName() } .setPositiveButton(R.string.lbl_ok) { _, _ ->
val pendingPlaylist = unlikelyToBeNull(pickerModel.currentPendingPlaylist.value)
val name =
when (val chosenName = pickerModel.chosenName.value) {
is ChosenName.Valid -> chosenName.value
is ChosenName.Empty -> pendingPlaylist.preferredName
else -> throw IllegalStateException()
}
musicModel.createPlaylist(name, pendingPlaylist.songs)
}
.setNegativeButton(R.string.lbl_cancel, null) .setNegativeButton(R.string.lbl_cancel, null)
} }
@ -58,20 +69,24 @@ 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.apply { binding.playlistName.addTextChangedListener { pickerModel.updateChosenName(it?.toString()) }
hint = args.pendingPlaylist.name
addTextChangedListener { pickerModel.setPendingPlaylist(requireContext(), args.songUids)
dialogModel.updatePendingName( collectImmediately(pickerModel.currentPendingPlaylist, ::updatePendingPlaylist)
(if (it.isNullOrEmpty()) unlikelyToBeNull(hint) else it).toString()) collectImmediately(pickerModel.chosenName, ::handleChosenName)
} }
private fun updatePendingPlaylist(pendingPlaylist: PendingPlaylist?) {
if (pendingPlaylist == null) {
findNavController().navigateUp()
return
} }
dialogModel.setPendingName(args.pendingPlaylist) requireBinding().playlistName.hint = pendingPlaylist.preferredName
collectImmediately(dialogModel.pendingPlaylistValid, ::updateValid)
} }
private fun updateValid(valid: Boolean) { private fun handleChosenName(chosenName: ChosenName) {
// 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 chosenName is ChosenName.Valid || chosenName is ChosenName.Empty
} }
} }

View file

@ -0,0 +1,151 @@
/*
* Copyright (c) 2023 Auxio Project
* PlaylistPickerViewModel.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
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.music.picker
import android.content.Context
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicRepository
import org.oxycblt.auxio.music.Song
/**
* A [ViewModel] managing the state of the playlist picker dialogs.
*
* @author Alexander Capehart
*/
@HiltViewModel
class PlaylistPickerViewModel @Inject constructor(private val musicRepository: MusicRepository) :
ViewModel(), MusicRepository.UpdateListener {
private val _currentPendingPlaylist = MutableStateFlow<PendingPlaylist?>(null)
val currentPendingPlaylist: StateFlow<PendingPlaylist?>
get() = _currentPendingPlaylist
private val _chosenName = MutableStateFlow<ChosenName>(ChosenName.Empty)
val chosenName: StateFlow<ChosenName>
get() = _chosenName
init {
musicRepository.addUpdateListener(this)
}
override fun onMusicChanges(changes: MusicRepository.Changes) {
val deviceLibrary = musicRepository.deviceLibrary
if (changes.deviceLibrary && deviceLibrary != null) {
_currentPendingPlaylist.value =
_currentPendingPlaylist.value?.let { pendingPlaylist ->
PendingPlaylist(
pendingPlaylist.preferredName,
pendingPlaylist.songs.mapNotNull { deviceLibrary.findSong(it.uid) })
}
}
val chosenName = _chosenName.value
if (changes.userLibrary) {
when (chosenName) {
is ChosenName.Valid -> updateChosenName(chosenName.value)
is ChosenName.AlreadyExists -> updateChosenName(chosenName.prior)
else -> {
// Nothing to do.
}
}
}
}
override fun onCleared() {
musicRepository.removeUpdateListener(this)
}
/**
* Update the current [PendingPlaylist]. Will do nothing if already equal.
*
* @param context [Context] required to generate a playlist name.
* @param songUids The list of [Music.UID] representing the songs to be present in the playlist.
*/
fun setPendingPlaylist(context: Context, songUids: Array<Music.UID>) {
if (currentPendingPlaylist.value?.songs?.map { it.uid } == songUids) {
// Nothing to do.
return
}
val deviceLibrary = musicRepository.deviceLibrary ?: return
val songs = songUids.mapNotNull(deviceLibrary::findSong)
val userLibrary = musicRepository.userLibrary ?: return
var i = 1
while (true) {
val possibleName = context.getString(R.string.fmt_def_playlist, i)
if (userLibrary.playlists.none { it.name.resolve(context) == possibleName }) {
_currentPendingPlaylist.value = PendingPlaylist(possibleName, songs)
return
}
++i
}
}
/**
* Update the current [ChosenName] based on new user input.
*
* @param name The new user-inputted name, or null if not present.
*/
fun updateChosenName(name: String?) {
_chosenName.value =
when {
name.isNullOrEmpty() -> ChosenName.Empty
name.isBlank() -> ChosenName.Blank
else -> {
val trimmed = name.trim()
val userLibrary = musicRepository.userLibrary
if (userLibrary != null && userLibrary.findPlaylist(trimmed) == null) {
ChosenName.Valid(trimmed)
} else {
ChosenName.AlreadyExists(trimmed)
}
}
}
}
}
/**
* 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]
* @author Alexander Capehart (OxygenCobalt)
*/
data class PendingPlaylist(val preferredName: String, val songs: List<Song>)
/**
* Represents the (processed) user input from the playlist naming dialogs.
*
* @author Alexander Capehart (OxygenCobalt)
*/
sealed interface ChosenName {
/** The current name is valid. */
data class Valid(val value: String) : ChosenName
/** The current name already exists. */
data class AlreadyExists(val prior: String) : ChosenName
/** The current name is empty. */
object Empty : ChosenName
/** The current name only consists of whitespace. */
object Blank : ChosenName
}

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.navigation.dialog package org.oxycblt.auxio.navigation.picker
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
@ -48,7 +48,7 @@ import org.oxycblt.auxio.util.collectImmediately
class NavigateToArtistDialog : 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: NavigationPickerViewModel 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: NavigateToArtistDialogArgs by navArgs() private val args: NavigateToArtistDialogArgs by navArgs()
@ -69,7 +69,7 @@ class NavigateToArtistDialog :
adapter = choiceAdapter adapter = choiceAdapter
} }
pickerModel.setArtistChoiceUid(args.artistUid) pickerModel.setArtistChoiceUid(args.itemUid)
collectImmediately(pickerModel.currentArtistChoices) { collectImmediately(pickerModel.currentArtistChoices) {
if (it != null) { if (it != null) {
choiceAdapter.update(it.choices, UpdateInstructions.Replace(0)) choiceAdapter.update(it.choices, UpdateInstructions.Replace(0))

View file

@ -1,6 +1,6 @@
/* /*
* Copyright (c) 2023 Auxio Project * Copyright (c) 2023 Auxio Project
* NavigationDialogViewModel.kt is part of Auxio. * NavigationPickerViewModel.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
@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.navigation.dialog package org.oxycblt.auxio.navigation.picker
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
@ -26,12 +26,12 @@ import kotlinx.coroutines.flow.StateFlow
import org.oxycblt.auxio.music.* import org.oxycblt.auxio.music.*
/** /**
* A [ViewModel] that stores the current information required for navigation dialogs * A [ViewModel] that stores the current information required for navigation picker dialogs
* *
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
@HiltViewModel @HiltViewModel
class NavigationDialogViewModel @Inject constructor(private val musicRepository: MusicRepository) : class NavigationPickerViewModel @Inject constructor(private val musicRepository: MusicRepository) :
ViewModel(), MusicRepository.UpdateListener { ViewModel(), MusicRepository.UpdateListener {
private val _currentArtistChoices = MutableStateFlow<ArtistNavigationChoices?>(null) private val _currentArtistChoices = MutableStateFlow<ArtistNavigationChoices?>(null)
/** The current set of [Artist] choices to show in the picker, or null if to show nothing. */ /** The current set of [Artist] choices to show in the picker, or null if to show nothing. */
@ -68,12 +68,12 @@ class NavigationDialogViewModel @Inject constructor(private val musicRepository:
/** /**
* Set the [Music.UID] of the item to show artist choices for. * Set the [Music.UID] of the item to show artist choices for.
* *
* @param uid The [Music.UID] of the item to show. Must be a [Song] or [Album]. * @param itemUid The [Music.UID] of the item to show. Must be a [Song] or [Album].
*/ */
fun setArtistChoiceUid(uid: Music.UID) { fun setArtistChoiceUid(itemUid: Music.UID) {
// Support Songs and Albums, which have parent artists. // Support Songs and Albums, which have parent artists.
_currentArtistChoices.value = _currentArtistChoices.value =
when (val music = musicRepository.find(uid)) { when (val music = musicRepository.find(itemUid)) {
is Song -> SongArtistNavigationChoices(music) is Song -> SongArtistNavigationChoices(music)
is Album -> AlbumArtistNavigationChoices(music) is Album -> AlbumArtistNavigationChoices(music)
else -> null else -> null

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.playback.dialog package org.oxycblt.auxio.playback.picker
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
@ -49,7 +49,7 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
class PlayFromArtistDialog : 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: PlaybackPickerViewModel 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: PlayFromArtistDialogArgs by navArgs() private val args: PlayFromArtistDialogArgs by navArgs()

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.playback.dialog package org.oxycblt.auxio.playback.picker
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
@ -49,7 +49,7 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
class PlayFromGenreDialog : 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: PlaybackPickerViewModel 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: PlayFromGenreDialogArgs by navArgs() private val args: PlayFromGenreDialogArgs by navArgs()

View file

@ -1,6 +1,6 @@
/* /*
* Copyright (c) 2023 Auxio Project * Copyright (c) 2023 Auxio Project
* PlaybackDialogViewModel.kt is part of Auxio. * PlaybackPickerViewModel.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
@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.playback.dialog package org.oxycblt.auxio.playback.picker
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
@ -31,7 +31,7 @@ import org.oxycblt.auxio.music.*
* @author OxygenCobalt (Alexander Capehart) * @author OxygenCobalt (Alexander Capehart)
*/ */
@HiltViewModel @HiltViewModel
class PlaybackDialogViewModel @Inject constructor(private val musicRepository: MusicRepository) : class PlaybackPickerViewModel @Inject constructor(private val musicRepository: MusicRepository) :
ViewModel(), MusicRepository.UpdateListener { ViewModel(), MusicRepository.UpdateListener {
private val _currentPickerSong = MutableStateFlow<Song?>(null) private val _currentPickerSong = MutableStateFlow<Song?>(null)
/** The current set of [Artist] choices to show in the picker, or null if to show nothing. */ /** The current set of [Artist] choices to show in the picker, or null if to show nothing. */

View file

@ -33,6 +33,7 @@ import androidx.navigation.NavController
import androidx.navigation.NavDirections import androidx.navigation.NavDirections
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import java.lang.IllegalArgumentException
/** /**
* Get if this [View] contains the given [PointF], with optional leeway. * Get if this [View] contains the given [PointF], with optional leeway.
@ -124,8 +125,10 @@ fun AppCompatButton.fixDoubleRipple() {
fun NavController.navigateSafe(directions: NavDirections) = fun NavController.navigateSafe(directions: NavDirections) =
try { try {
navigate(directions) navigate(directions)
} catch (e: IllegalStateException) { } catch (e: IllegalArgumentException) {
// Nothing to do. // Nothing to do.
logE("Could not navigate from this destination.")
logE(e.stackTraceToString())
} }
/** /**

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.textfield.TextInputLayout xmlns:android="http://schemas.android.com/apk/res/android" <com.google.android.material.textfield.TextInputLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/playlist_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingTop="@dimen/spacing_medium" android:paddingTop="@dimen/spacing_medium"

View file

@ -43,27 +43,27 @@
<dialog <dialog
android:id="@+id/new_playlist_dialog" android:id="@+id/new_playlist_dialog"
android:name="org.oxycblt.auxio.music.dialog.NewPlaylistDialog" android:name="org.oxycblt.auxio.music.picker.NewPlaylistDialog"
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="pendingPlaylist" android:name="songUids"
app:argType="org.oxycblt.auxio.music.dialog.PendingPlaylist" /> app:argType="org.oxycblt.auxio.music.Music$UID[]" />
</dialog> </dialog>
<dialog <dialog
android:id="@+id/navigate_to_artist_dialog" android:id="@+id/navigate_to_artist_dialog"
android:name="org.oxycblt.auxio.navigation.dialog.NavigateToArtistDialog" android:name="org.oxycblt.auxio.navigation.picker.NavigateToArtistDialog"
android:label="navigate_to_artist_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="itemUid"
app:argType="org.oxycblt.auxio.music.Music$UID" /> app:argType="org.oxycblt.auxio.music.Music$UID" />
</dialog> </dialog>
<dialog <dialog
android:id="@+id/play_from_artist_dialog" android:id="@+id/play_from_artist_dialog"
android:name="org.oxycblt.auxio.playback.dialog.PlayFromArtistDialog" android:name="org.oxycblt.auxio.playback.picker.PlayFromArtistDialog"
android:label="play_from_artist_dialog" android:label="play_from_artist_dialog"
tools:layout="@layout/dialog_music_picker"> tools:layout="@layout/dialog_music_picker">
<argument <argument
@ -73,7 +73,7 @@
<dialog <dialog
android:id="@+id/play_from_genre_dialog" android:id="@+id/play_from_genre_dialog"
android:name="org.oxycblt.auxio.playback.dialog.PlayFromGenreDialog" android:name="org.oxycblt.auxio.playback.picker.PlayFromGenreDialog"
android:label="play_from_genre_dialog" android:label="play_from_genre_dialog"
tools:layout="@layout/dialog_music_picker"> tools:layout="@layout/dialog_music_picker">
<argument <argument
@ -81,7 +81,6 @@
app:argType="org.oxycblt.auxio.music.Music$UID" /> app:argType="org.oxycblt.auxio.music.Music$UID" />
</dialog> </dialog>
<fragment <fragment
android:id="@+id/root_preferences_fragment" android:id="@+id/root_preferences_fragment"
android:name="org.oxycblt.auxio.settings.RootPreferenceFragment" android:name="org.oxycblt.auxio.settings.RootPreferenceFragment"
@ -156,12 +155,12 @@
tools:layout="@layout/dialog_pre_amp" /> tools:layout="@layout/dialog_pre_amp" />
<dialog <dialog
android:id="@+id/music_dirs_dialog" android:id="@+id/music_dirs_dialog"
android:name="org.oxycblt.auxio.music.fs.MusicDirsDialog" android:name="org.oxycblt.auxio.music.config.MusicDirsDialog"
android:label="music_dirs_dialog" android:label="music_dirs_dialog"
tools:layout="@layout/dialog_music_dirs" /> tools:layout="@layout/dialog_music_dirs" />
<dialog <dialog
android:id="@+id/separators_dialog" android:id="@+id/separators_dialog"
android:name="org.oxycblt.auxio.music.dialog.SeparatorsDialog" android:name="org.oxycblt.auxio.music.config.SeparatorsDialog"
android:label="music_dirs_dialog" android:label="music_dirs_dialog"
tools:layout="@layout/dialog_separators" /> tools:layout="@layout/dialog_separators" />