all: unwind picker abstraction

Unwind the picker abstraction into smaller dialog packages.

While this increases repetition, it will make the playlist dialog
implementations much less shoddy.
This commit is contained in:
Alexander Capehart 2023-05-11 16:18:14 -06:00
parent e68cc4d620
commit eb4adcc109
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
14 changed files with 405 additions and 454 deletions

View file

@ -24,8 +24,10 @@ import org.oxycblt.auxio.IntegerTable
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.ItemHeaderBinding import org.oxycblt.auxio.databinding.ItemHeaderBinding
import org.oxycblt.auxio.databinding.ItemParentBinding import org.oxycblt.auxio.databinding.ItemParentBinding
import org.oxycblt.auxio.databinding.ItemPickerChoiceBinding
import org.oxycblt.auxio.databinding.ItemSongBinding import org.oxycblt.auxio.databinding.ItemSongBinding
import org.oxycblt.auxio.list.BasicHeader import org.oxycblt.auxio.list.BasicHeader
import org.oxycblt.auxio.list.ClickableListListener
import org.oxycblt.auxio.list.SelectableListListener import org.oxycblt.auxio.list.SelectableListListener
import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter
import org.oxycblt.auxio.list.adapter.SimpleDiffCallback import org.oxycblt.auxio.list.adapter.SimpleDiffCallback
@ -347,3 +349,49 @@ class BasicHeaderViewHolder private constructor(private val binding: ItemHeaderB
} }
} }
} }
/**
* A [DialogRecyclerView.ViewHolder] that displays a smaller variant of a typical [T] item, for use
* in choice dialogs. Use [from] to create an instance.
*/
class ChoiceViewHolder<T : Music>
private constructor(private val binding: ItemPickerChoiceBinding) :
DialogRecyclerView.ViewHolder(binding.root) {
/**
* Bind new data to this instance.
*
* @param music The new [T] to bind.
* @param listener A [ClickableListListener] to bind interactions to.
*/
fun bind(music: T, listener: ClickableListListener<T>) {
listener.bind(music, this)
// ImageGroup is not generic, so we must downcast to specific types for now.
when (music) {
is Song -> binding.pickerImage.bind(music)
is Album -> binding.pickerImage.bind(music)
is Artist -> binding.pickerImage.bind(music)
is Genre -> binding.pickerImage.bind(music)
is Playlist -> binding.pickerImage.bind(music)
}
binding.pickerName.text = music.name.resolve(binding.context)
}
companion object {
/**
* Create a new instance.
*
* @param parent The parent to inflate this instance from.
* @return A new instance.
*/
fun <T : Music> from(parent: View) =
ChoiceViewHolder<T>(ItemPickerChoiceBinding.inflate(parent.context.inflater))
/** Get a comparator that can be used with DiffUtil. */
fun <T : Music> diffCallback() =
object : SimpleDiffCallback<T>() {
override fun areContentsTheSame(oldItem: T, newItem: T) =
oldItem.name == newItem.name
}
}
}

View file

@ -0,0 +1,104 @@
/*
* Copyright (c) 2022 Auxio Project
* ArtistNavigationPickerDialog.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.navigation.dialog
import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.RecyclerView
import dagger.hilt.android.AndroidEntryPoint
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogMusicPickerBinding
import org.oxycblt.auxio.list.ClickableListListener
import org.oxycblt.auxio.list.adapter.FlexibleListAdapter
import org.oxycblt.auxio.list.adapter.UpdateInstructions
import org.oxycblt.auxio.list.recycler.ChoiceViewHolder
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.navigation.NavigationViewModel
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
import org.oxycblt.auxio.util.collectImmediately
/**
* A picker [ViewBindingDialogFragment] intended for when [Artist] navigation is ambiguous.
*
* @author Alexander Capehart (OxygenCobalt)
*/
@AndroidEntryPoint
class ArtistNavigationPickerDialog :
ViewBindingDialogFragment<DialogMusicPickerBinding>(), ClickableListListener<Artist> {
private val navigationModel: NavigationViewModel by activityViewModels()
private val pickerModel: NavigationPickerViewModel 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 choiceAdapter = ArtistChoiceAdapter(this)
override fun onConfigDialog(builder: AlertDialog.Builder) {
builder.setTitle(R.string.lbl_artists).setNegativeButton(R.string.lbl_cancel, null)
}
override fun onCreateBinding(inflater: LayoutInflater) =
DialogMusicPickerBinding.inflate(inflater)
override fun onBindingCreated(binding: DialogMusicPickerBinding, savedInstanceState: Bundle?) {
super.onBindingCreated(binding, savedInstanceState)
binding.pickerChoiceRecycler.apply {
itemAnimator = null
adapter = choiceAdapter
}
pickerModel.setArtistChoiceUid(args.artistUid)
collectImmediately(pickerModel.currentArtistChoices) {
if (it != null) {
choiceAdapter.update(it.choices, UpdateInstructions.Replace(0))
} else {
findNavController().navigateUp()
}
}
}
override fun onDestroyBinding(binding: DialogMusicPickerBinding) {
super.onDestroyBinding(binding)
choiceAdapter
}
override fun onClick(item: Artist, viewHolder: RecyclerView.ViewHolder) {
// User made a choice, navigate to the artist.
navigationModel.exploreNavigateTo(item)
findNavController().navigateUp()
}
private class ArtistChoiceAdapter(private val listener: ClickableListListener<Artist>) :
FlexibleListAdapter<Artist, ChoiceViewHolder<Artist>>(ChoiceViewHolder.diffCallback()) {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): ChoiceViewHolder<Artist> = ChoiceViewHolder.from(parent)
override fun onBindViewHolder(holder: ChoiceViewHolder<Artist>, position: Int) {
holder.bind(getItem(position), listener)
}
}
}

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.picker package org.oxycblt.auxio.navigation.dialog
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
@ -24,7 +24,6 @@ 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.music.* import org.oxycblt.auxio.music.*
import org.oxycblt.auxio.picker.PickerChoices
/** /**
* A [ViewModel] that stores the current information required for [ArtistNavigationPickerDialog]. * A [ViewModel] that stores the current information required for [ArtistNavigationPickerDialog].
@ -36,7 +35,7 @@ class NavigationPickerViewModel @Inject constructor(private val 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. */
val currentArtistChoices: StateFlow<PickerChoices<Artist>?> val currentArtistChoices: StateFlow<ArtistNavigationChoices?>
get() = _currentArtistChoices get() = _currentArtistChoices
init { init {
@ -49,13 +48,13 @@ class NavigationPickerViewModel @Inject constructor(private val musicRepository:
// Need to sanitize different items depending on the current set of choices. // Need to sanitize different items depending on the current set of choices.
_currentArtistChoices.value = _currentArtistChoices.value =
when (val choices = _currentArtistChoices.value) { when (val choices = _currentArtistChoices.value) {
is ArtistNavigationChoices.FromSong -> is SongArtistNavigationChoices ->
deviceLibrary.findSong(choices.song.uid)?.let { deviceLibrary.findSong(choices.song.uid)?.let {
ArtistNavigationChoices.FromSong(it) SongArtistNavigationChoices(it)
} }
is ArtistNavigationChoices.FromAlbum -> is AlbumArtistNavigationChoices ->
deviceLibrary.findAlbum(choices.album.uid)?.let { deviceLibrary.findAlbum(choices.album.uid)?.let {
ArtistNavigationChoices.FromAlbum(it) AlbumArtistNavigationChoices(it)
} }
else -> null else -> null
} }
@ -75,19 +74,32 @@ class NavigationPickerViewModel @Inject constructor(private val musicRepository:
// 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(uid)) {
is Song -> ArtistNavigationChoices.FromSong(music) is Song -> SongArtistNavigationChoices(music)
is Album -> ArtistNavigationChoices.FromAlbum(music) is Album -> AlbumArtistNavigationChoices(music)
else -> null else -> null
} }
} }
}
private sealed interface ArtistNavigationChoices : PickerChoices<Artist> {
data class FromSong(val song: Song) : ArtistNavigationChoices { /**
override val choices = song.artists * The current list of choices to show in the artist navigation picker dialog.
} *
* @author Alexander Capehart (OxygenCobalt)
data class FromAlbum(val album: Album) : ArtistNavigationChoices { */
override val choices = album.artists sealed interface ArtistNavigationChoices {
} /** The current [Artist] choices. */
} val choices: List<Artist>
}
/** Backing implementation of [ArtistNavigationChoices] that is based on a [Song]. */
private data class SongArtistNavigationChoices(val song: Song) : ArtistNavigationChoices {
override val choices = song.artists
}
/**
* Backing implementation of [ArtistNavigationChoices] that is based on an
* [AlbumArtistNavigationChoices].
*/
private data class AlbumArtistNavigationChoices(val album: Album) : ArtistNavigationChoices {
override val choices = album.artists
} }

View file

@ -1,61 +0,0 @@
/*
* Copyright (c) 2022 Auxio Project
* ArtistNavigationPickerDialog.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.navigation.picker
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.RecyclerView
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.StateFlow
import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.navigation.NavigationViewModel
import org.oxycblt.auxio.picker.PickerChoices
import org.oxycblt.auxio.picker.PickerDialogFragment
/**
* A [PickerDialogFragment] intended for when [Artist] navigation is ambiguous.
*
* @author Alexander Capehart (OxygenCobalt)
*/
@AndroidEntryPoint
class ArtistNavigationPickerDialog : PickerDialogFragment<Artist>() {
private val navModel: NavigationViewModel by activityViewModels()
private val pickerModel: NavigationPickerViewModel 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: ArtistNavigationPickerDialogArgs by navArgs()
override val titleRes: Int
get() = R.string.lbl_artists
override val pickerChoices: StateFlow<PickerChoices<Artist>?>
get() = pickerModel.currentArtistChoices
override fun initChoices() {
pickerModel.setArtistChoiceUid(args.artistUid)
}
override fun onClick(item: Artist, viewHolder: RecyclerView.ViewHolder) {
super.onClick(item, viewHolder)
// User made a choice, navigate to it.
navModel.exploreNavigateTo(item)
}
}

View file

@ -1,87 +0,0 @@
/*
* Copyright (c) 2023 Auxio Project
* ChoiceAdapter.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.picker
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.databinding.ItemPickerChoiceBinding
import org.oxycblt.auxio.list.ClickableListListener
import org.oxycblt.auxio.list.adapter.FlexibleListAdapter
import org.oxycblt.auxio.list.adapter.SimpleDiffCallback
import org.oxycblt.auxio.list.recycler.DialogRecyclerView
import org.oxycblt.auxio.music.*
import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.inflater
/** A [RecyclerView.Adapter] that shows a list */
class ChoiceAdapter<T : Music>(private val listener: ClickableListListener<T>) :
FlexibleListAdapter<T, ChoiceViewHolder<T>>(ChoiceViewHolder.diffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
ChoiceViewHolder.from<T>(parent)
override fun onBindViewHolder(holder: ChoiceViewHolder<T>, position: Int) =
holder.bind(getItem(position), listener)
}
/**
* A [DialogRecyclerView.ViewHolder] that displays a smaller variant of a typical [T] item, for use
* with [ChoiceAdapter]. Use [from] to create an instance.
*/
class ChoiceViewHolder<T : Music>
private constructor(private val binding: ItemPickerChoiceBinding) :
DialogRecyclerView.ViewHolder(binding.root) {
/**
* Bind new data to this instance.
*
* @param music The new [T] to bind.
* @param listener A [ClickableListListener] to bind interactions to.
*/
fun bind(music: T, listener: ClickableListListener<T>) {
listener.bind(music, this)
// ImageGroup is not generic, so we must downcast to specific types for now.
when (music) {
is Song -> binding.pickerImage.bind(music)
is Album -> binding.pickerImage.bind(music)
is Artist -> binding.pickerImage.bind(music)
is Genre -> binding.pickerImage.bind(music)
is Playlist -> binding.pickerImage.bind(music)
}
binding.pickerName.text = music.name.resolve(binding.context)
}
companion object {
/**
* Create a new instance.
*
* @param parent The parent to inflate this instance from.
* @return A new instance.
*/
fun <T : Music> from(parent: View) =
ChoiceViewHolder<T>(ItemPickerChoiceBinding.inflate(parent.context.inflater))
/** Get a comparator that can be used with DiffUtil. */
fun <T : Music> diffCallback() =
object : SimpleDiffCallback<T>() {
override fun areContentsTheSame(oldItem: T, newItem: T) =
oldItem.name == newItem.name
}
}
}

View file

@ -1,31 +0,0 @@
/*
* Copyright (c) 2023 Auxio Project
* PickerChoices.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.picker
import org.oxycblt.auxio.music.Music
/**
* Represents a list of [Music] to show in a picker UI.
*
* @author Alexander Capehart (OxygenCobalt)
*/
interface PickerChoices<T : Music> {
/** The list of choices to show. */
val choices: List<T>
}

View file

@ -1,85 +0,0 @@
/*
* Copyright (c) 2023 Auxio Project
* PickerDialogFragment.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.picker
import android.os.Bundle
import android.view.LayoutInflater
import androidx.appcompat.app.AlertDialog
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.flow.StateFlow
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogMusicPickerBinding
import org.oxycblt.auxio.list.ClickableListListener
import org.oxycblt.auxio.list.adapter.UpdateInstructions
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
import org.oxycblt.auxio.util.collectImmediately
/**
* A [ViewBindingDialogFragment] that acts as the base for a "picker" UI, shown when a given choice
* is ambiguous.
*
* @author Alexander Capehart (OxygenCobalt)
*/
abstract class PickerDialogFragment<T : Music> :
ViewBindingDialogFragment<DialogMusicPickerBinding>(), ClickableListListener<T> {
// Okay to leak this since the Listener will not be called until after initialization.
private val choiceAdapter = ChoiceAdapter(@Suppress("LeakingThis") this)
/** The string resource to use in the dialog title. */
abstract val titleRes: Int
/** The [StateFlow] of choices to show in the picker. */
abstract val pickerChoices: StateFlow<PickerChoices<T>?>
/** Called when the choice list should be initialized from the stored arguments. */
abstract fun initChoices()
override fun onCreateBinding(inflater: LayoutInflater) =
DialogMusicPickerBinding.inflate(inflater)
override fun onConfigDialog(builder: AlertDialog.Builder) {
builder.setTitle(titleRes).setNegativeButton(R.string.lbl_cancel, null)
}
override fun onBindingCreated(binding: DialogMusicPickerBinding, savedInstanceState: Bundle?) {
binding.pickerRecycler.apply {
itemAnimator = null
adapter = choiceAdapter
}
initChoices()
collectImmediately(pickerChoices) { item ->
if (item != null) {
// Make sure the choices align with any changes in the music library.
choiceAdapter.update(item.choices, UpdateInstructions.Diff)
} else {
// Not showing any choices, navigate up.
findNavController().navigateUp()
}
}
}
override fun onDestroyBinding(binding: DialogMusicPickerBinding) {
binding.pickerRecycler.adapter = null
}
override fun onClick(item: T, viewHolder: RecyclerView.ViewHolder) {
findNavController().navigateUp()
}
}

View file

@ -0,0 +1,106 @@
/*
* Copyright (c) 2022 Auxio Project
* ArtistPlaybackPickerDialog.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.playback.dialog
import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.RecyclerView
import dagger.hilt.android.AndroidEntryPoint
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogMusicPickerBinding
import org.oxycblt.auxio.list.ClickableListListener
import org.oxycblt.auxio.list.adapter.FlexibleListAdapter
import org.oxycblt.auxio.list.adapter.UpdateInstructions
import org.oxycblt.auxio.list.recycler.ChoiceViewHolder
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.unlikelyToBeNull
/**
* A picker [ViewBindingDialogFragment] intended for when [Artist] playback is ambiguous.
*
* @author Alexander Capehart (OxygenCobalt)
*/
@AndroidEntryPoint
class ArtistPlaybackPickerDialog :
ViewBindingDialogFragment<DialogMusicPickerBinding>(), ClickableListListener<Artist> {
private val playbackModel: PlaybackViewModel by activityViewModels()
private val pickerModel: PlaybackPickerViewModel 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 choiceAdapter = ArtistChoiceAdapter(this)
override fun onConfigDialog(builder: AlertDialog.Builder) {
builder.setTitle(R.string.lbl_artists).setNegativeButton(R.string.lbl_cancel, null)
}
override fun onCreateBinding(inflater: LayoutInflater) =
DialogMusicPickerBinding.inflate(inflater)
override fun onBindingCreated(binding: DialogMusicPickerBinding, savedInstanceState: Bundle?) {
super.onBindingCreated(binding, savedInstanceState)
binding.pickerChoiceRecycler.apply {
itemAnimator = null
adapter = choiceAdapter
}
pickerModel.setPickerSongUid(args.artistUid)
collectImmediately(pickerModel.currentPickerSong) {
if (it != null) {
choiceAdapter.update(it.artists, UpdateInstructions.Replace(0))
} else {
findNavController().navigateUp()
}
}
}
override fun onDestroyBinding(binding: DialogMusicPickerBinding) {
super.onDestroyBinding(binding)
choiceAdapter
}
override fun onClick(item: Artist, viewHolder: RecyclerView.ViewHolder) {
// User made a choice, play the given song from that artist.
val song = unlikelyToBeNull(pickerModel.currentPickerSong.value)
playbackModel.playFromArtist(song, item)
findNavController().navigateUp()
}
private class ArtistChoiceAdapter(private val listener: ClickableListListener<Artist>) :
FlexibleListAdapter<Artist, ChoiceViewHolder<Artist>>(ChoiceViewHolder.diffCallback()) {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): ChoiceViewHolder<Artist> = ChoiceViewHolder.from(parent)
override fun onBindViewHolder(holder: ChoiceViewHolder<Artist>, position: Int) {
holder.bind(getItem(position), listener)
}
}
}

View file

@ -0,0 +1,104 @@
/*
* Copyright (c) 2022 Auxio Project
* GenrePlaybackPickerDialog.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.playback.dialog
import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.RecyclerView
import dagger.hilt.android.AndroidEntryPoint
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogMusicPickerBinding
import org.oxycblt.auxio.list.ClickableListListener
import org.oxycblt.auxio.list.adapter.FlexibleListAdapter
import org.oxycblt.auxio.list.adapter.UpdateInstructions
import org.oxycblt.auxio.list.recycler.ChoiceViewHolder
import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.unlikelyToBeNull
/**
* A picker [ViewBindingDialogFragment] intended for when [Genre] playback is ambiguous.
*
* @author Alexander Capehart (OxygenCobalt)
*/
@AndroidEntryPoint
class GenrePlaybackPickerDialog :
ViewBindingDialogFragment<DialogMusicPickerBinding>(), ClickableListListener<Genre> {
private val playbackModel: PlaybackViewModel by activityViewModels()
private val pickerModel: PlaybackPickerViewModel 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 choiceAdapter = GenreChoiceAdapter(this)
override fun onConfigDialog(builder: AlertDialog.Builder) {
builder.setTitle(R.string.lbl_genres).setNegativeButton(R.string.lbl_cancel, null)
}
override fun onCreateBinding(inflater: LayoutInflater) =
DialogMusicPickerBinding.inflate(inflater)
override fun onBindingCreated(binding: DialogMusicPickerBinding, savedInstanceState: Bundle?) {
super.onBindingCreated(binding, savedInstanceState)
binding.pickerChoiceRecycler.apply {
itemAnimator = null
adapter = choiceAdapter
}
pickerModel.setPickerSongUid(args.genreUid)
collectImmediately(pickerModel.currentPickerSong) {
if (it != null) {
choiceAdapter.update(it.genres, UpdateInstructions.Replace(0))
} else {
findNavController().navigateUp()
}
}
}
override fun onDestroyBinding(binding: DialogMusicPickerBinding) {
super.onDestroyBinding(binding)
choiceAdapter
}
override fun onClick(item: Genre, viewHolder: RecyclerView.ViewHolder) {
// User made a choice, play the given song from that genre.
val song = unlikelyToBeNull(pickerModel.currentPickerSong.value)
playbackModel.playFromGenre(song, item)
findNavController().navigateUp()
}
private class GenreChoiceAdapter(private val listener: ClickableListListener<Genre>) :
FlexibleListAdapter<Genre, ChoiceViewHolder<Genre>>(ChoiceViewHolder.diffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChoiceViewHolder<Genre> =
ChoiceViewHolder.from(parent)
override fun onBindViewHolder(holder: ChoiceViewHolder<Genre>, position: Int) {
holder.bind(getItem(position), listener)
}
}
}

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.picker package org.oxycblt.auxio.playback.dialog
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
@ -24,7 +24,6 @@ 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.music.* import org.oxycblt.auxio.music.*
import org.oxycblt.auxio.picker.PickerChoices
/** /**
* A [ViewModel] that stores the choices shown in the playback picker dialogs. * A [ViewModel] that stores the choices shown in the playback picker dialogs.
@ -34,15 +33,10 @@ import org.oxycblt.auxio.picker.PickerChoices
@HiltViewModel @HiltViewModel
class PlaybackPickerViewModel @Inject constructor(private val musicRepository: MusicRepository) : class PlaybackPickerViewModel @Inject constructor(private val musicRepository: MusicRepository) :
ViewModel(), MusicRepository.UpdateListener { ViewModel(), MusicRepository.UpdateListener {
private val _currentArtistChoices = MutableStateFlow<ArtistPlaybackChoices?>(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. */
val currentArtistChoices: StateFlow<ArtistPlaybackChoices?> val currentPickerSong: StateFlow<Song?>
get() = _currentArtistChoices get() = _currentPickerSong
private val _currentGenreChoices = MutableStateFlow<GenrePlaybackChoices?>(null)
/** The current set of [Genre] choices to show in the picker, or null if to show nothing. */
val currentGenreChoices: StateFlow<GenrePlaybackChoices?>
get() = _currentGenreChoices
init { init {
musicRepository.addUpdateListener(this) musicRepository.addUpdateListener(this)
@ -51,14 +45,7 @@ class PlaybackPickerViewModel @Inject constructor(private val musicRepository: M
override fun onMusicChanges(changes: MusicRepository.Changes) { override fun onMusicChanges(changes: MusicRepository.Changes) {
if (!changes.deviceLibrary) return if (!changes.deviceLibrary) return
val deviceLibrary = musicRepository.deviceLibrary ?: return val deviceLibrary = musicRepository.deviceLibrary ?: return
_currentArtistChoices.value = _currentPickerSong.value = _currentPickerSong.value?.run { deviceLibrary.findSong(uid) }
_currentArtistChoices.value?.run {
deviceLibrary.findSong(song.uid)?.let { newSong -> ArtistPlaybackChoices(newSong) }
}
_currentGenreChoices.value =
_currentGenreChoices.value?.run {
deviceLibrary.findSong(song.uid)?.let { newSong -> GenrePlaybackChoices(newSong) }
}
} }
override fun onCleared() { override fun onCleared() {
@ -67,30 +54,11 @@ class PlaybackPickerViewModel @Inject constructor(private val musicRepository: M
} }
/** /**
* Set the [Music.UID] of the item to show [Artist] choices for. * Set the [Music.UID] of the [Song] to show choices for.
* *
* @param uid The [Music.UID] of the item to show. Must be a [Song]. * @param uid The [Music.UID] of the item to show. Must be a [Song].
*/ */
fun setArtistChoiceUid(uid: Music.UID) { fun setPickerSongUid(uid: Music.UID) {
_currentArtistChoices.value = _currentPickerSong.value = musicRepository.deviceLibrary?.findSong(uid)
musicRepository.deviceLibrary?.findSong(uid)?.let { ArtistPlaybackChoices(it) }
}
/**
* Set the [Music.UID] of the item to show [Genre] choices for.
*
* @param uid The [Music.UID] of the item to show. Must be a [Song].
*/
fun setGenreChoiceUid(uid: Music.UID) {
_currentGenreChoices.value =
musicRepository.deviceLibrary?.findSong(uid)?.let { GenrePlaybackChoices(it) }
} }
} }
data class ArtistPlaybackChoices(val song: Song) : PickerChoices<Artist> {
override val choices = song.artists
}
data class GenrePlaybackChoices(val song: Song) : PickerChoices<Genre> {
override val choices = song.genres
}

View file

@ -1,63 +0,0 @@
/*
* Copyright (c) 2022 Auxio Project
* ArtistPlaybackPickerDialog.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.playback.picker
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.RecyclerView
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.StateFlow
import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.picker.PickerChoices
import org.oxycblt.auxio.picker.PickerDialogFragment
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.util.unlikelyToBeNull
/**
* An [ArtistPickerDialog] intended for when [Artist] playback is ambiguous.
*
* @author Alexander Capehart (OxygenCobalt)
*/
@AndroidEntryPoint
class ArtistPlaybackPickerDialog : PickerDialogFragment<Artist>() {
private val playbackModel: PlaybackViewModel by activityViewModels()
private val pickerModel: PlaybackPickerViewModel 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()
override val titleRes: Int
get() = R.string.lbl_artists
override val pickerChoices: StateFlow<PickerChoices<Artist>?>
get() = pickerModel.currentArtistChoices
override fun initChoices() {
pickerModel.setArtistChoiceUid(args.artistUid)
}
override fun onClick(item: Artist, viewHolder: RecyclerView.ViewHolder) {
super.onClick(item, viewHolder)
// User made a choice, play the given song from that artist.
val song = unlikelyToBeNull(pickerModel.currentArtistChoices.value).song
playbackModel.playFromArtist(song, item)
}
}

View file

@ -1,64 +0,0 @@
/*
* Copyright (c) 2022 Auxio Project
* GenrePlaybackPickerDialog.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.playback.picker
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.RecyclerView
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.StateFlow
import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.picker.PickerChoices
import org.oxycblt.auxio.picker.PickerDialogFragment
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
import org.oxycblt.auxio.util.unlikelyToBeNull
/**
* A picker [ViewBindingDialogFragment] intended for when [Genre] playback is ambiguous.
*
* @author Alexander Capehart (OxygenCobalt)
*/
@AndroidEntryPoint
class GenrePlaybackPickerDialog : PickerDialogFragment<Genre>() {
private val playbackModel: PlaybackViewModel by activityViewModels()
private val pickerModel: PlaybackPickerViewModel 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()
override val titleRes: Int
get() = R.string.lbl_genres
override val pickerChoices: StateFlow<PickerChoices<Genre>?>
get() = pickerModel.currentGenreChoices
override fun initChoices() {
pickerModel.setGenreChoiceUid(args.genreUid)
}
override fun onClick(item: Genre, viewHolder: RecyclerView.ViewHolder) {
super.onClick(item, viewHolder)
// User made a choice, play the given song from that genre.
val song = unlikelyToBeNull(pickerModel.currentGenreChoices.value).song
playbackModel.playFromGenre(song, item)
}
}

View file

@ -2,7 +2,7 @@
<org.oxycblt.auxio.list.recycler.DialogRecyclerView xmlns:android="http://schemas.android.com/apk/res/android" <org.oxycblt.auxio.list.recycler.DialogRecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/picker_recycler" android:id="@+id/picker_choice_recycler"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"

View file

@ -30,7 +30,7 @@
<dialog <dialog
android:id="@+id/artist_playback_picker_dialog" android:id="@+id/artist_playback_picker_dialog"
android:name="org.oxycblt.auxio.playback.picker.ArtistPlaybackPickerDialog" android:name="org.oxycblt.auxio.playback.dialog.ArtistPlaybackPickerDialog"
android:label="artist_playback_picker_dialog" android:label="artist_playback_picker_dialog"
tools:layout="@layout/dialog_music_picker"> tools:layout="@layout/dialog_music_picker">
<argument <argument
@ -40,7 +40,7 @@
<dialog <dialog
android:id="@+id/artist_navigation_picker_dialog" android:id="@+id/artist_navigation_picker_dialog"
android:name="org.oxycblt.auxio.navigation.picker.ArtistNavigationPickerDialog" android:name="org.oxycblt.auxio.navigation.dialog.ArtistNavigationPickerDialog"
android:label="artist_navigation_picker_dialog" android:label="artist_navigation_picker_dialog"
tools:layout="@layout/dialog_music_picker"> tools:layout="@layout/dialog_music_picker">
<argument <argument
@ -50,7 +50,7 @@
<dialog <dialog
android:id="@+id/genre_playback_picker_dialog" android:id="@+id/genre_playback_picker_dialog"
android:name="org.oxycblt.auxio.playback.picker.GenrePlaybackPickerDialog" android:name="org.oxycblt.auxio.playback.dialog.GenrePlaybackPickerDialog"
android:label="genre_playback_picker_dialog" android:label="genre_playback_picker_dialog"
tools:layout="@layout/dialog_music_picker"> tools:layout="@layout/dialog_music_picker">
<argument <argument