From eb4adcc109dd51f388e37ee5cecb5c2cc6c2045d Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Thu, 11 May 2023 16:18:14 -0600 Subject: [PATCH] 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. --- .../auxio/list/recycler/ViewHolders.kt | 48 ++++++++ .../dialog/ArtistNavigationPickerDialog.kt | 104 +++++++++++++++++ .../NavigationPickerViewModel.kt | 50 +++++---- .../picker/ArtistNavigationPickerDialog.kt | 61 ---------- .../org/oxycblt/auxio/picker/ChoiceAdapter.kt | 87 -------------- .../org/oxycblt/auxio/picker/PickerChoices.kt | 31 ----- .../auxio/picker/PickerDialogFragment.kt | 85 -------------- .../dialog/ArtistPlaybackPickerDialog.kt | 106 ++++++++++++++++++ .../dialog/GenrePlaybackPickerDialog.kt | 104 +++++++++++++++++ .../PlaybackPickerViewModel.kt | 48 ++------ .../picker/ArtistPlaybackPickerDialog.kt | 63 ----------- .../picker/GenrePlaybackPickerDialog.kt | 64 ----------- .../main/res/layout/dialog_music_picker.xml | 2 +- app/src/main/res/navigation/nav_main.xml | 6 +- 14 files changed, 405 insertions(+), 454 deletions(-) create mode 100644 app/src/main/java/org/oxycblt/auxio/navigation/dialog/ArtistNavigationPickerDialog.kt rename app/src/main/java/org/oxycblt/auxio/navigation/{picker => dialog}/NavigationPickerViewModel.kt (69%) delete mode 100644 app/src/main/java/org/oxycblt/auxio/navigation/picker/ArtistNavigationPickerDialog.kt delete mode 100644 app/src/main/java/org/oxycblt/auxio/picker/ChoiceAdapter.kt delete mode 100644 app/src/main/java/org/oxycblt/auxio/picker/PickerChoices.kt delete mode 100644 app/src/main/java/org/oxycblt/auxio/picker/PickerDialogFragment.kt create mode 100644 app/src/main/java/org/oxycblt/auxio/playback/dialog/ArtistPlaybackPickerDialog.kt create mode 100644 app/src/main/java/org/oxycblt/auxio/playback/dialog/GenrePlaybackPickerDialog.kt rename app/src/main/java/org/oxycblt/auxio/playback/{picker => dialog}/PlaybackPickerViewModel.kt (51%) delete mode 100644 app/src/main/java/org/oxycblt/auxio/playback/picker/ArtistPlaybackPickerDialog.kt delete mode 100644 app/src/main/java/org/oxycblt/auxio/playback/picker/GenrePlaybackPickerDialog.kt diff --git a/app/src/main/java/org/oxycblt/auxio/list/recycler/ViewHolders.kt b/app/src/main/java/org/oxycblt/auxio/list/recycler/ViewHolders.kt index 0bf991037..cc0603b4c 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/recycler/ViewHolders.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/recycler/ViewHolders.kt @@ -24,8 +24,10 @@ import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.ItemHeaderBinding import org.oxycblt.auxio.databinding.ItemParentBinding +import org.oxycblt.auxio.databinding.ItemPickerChoiceBinding import org.oxycblt.auxio.databinding.ItemSongBinding import org.oxycblt.auxio.list.BasicHeader +import org.oxycblt.auxio.list.ClickableListListener import org.oxycblt.auxio.list.SelectableListListener import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter 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 +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) { + 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 from(parent: View) = + ChoiceViewHolder(ItemPickerChoiceBinding.inflate(parent.context.inflater)) + + /** Get a comparator that can be used with DiffUtil. */ + fun diffCallback() = + object : SimpleDiffCallback() { + override fun areContentsTheSame(oldItem: T, newItem: T) = + oldItem.name == newItem.name + } + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/navigation/dialog/ArtistNavigationPickerDialog.kt b/app/src/main/java/org/oxycblt/auxio/navigation/dialog/ArtistNavigationPickerDialog.kt new file mode 100644 index 000000000..c292f00fe --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/navigation/dialog/ArtistNavigationPickerDialog.kt @@ -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 . + */ + +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(), ClickableListListener { + 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) : + FlexibleListAdapter>(ChoiceViewHolder.diffCallback()) { + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): ChoiceViewHolder = ChoiceViewHolder.from(parent) + + override fun onBindViewHolder(holder: ChoiceViewHolder, position: Int) { + holder.bind(getItem(position), listener) + } + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/navigation/picker/NavigationPickerViewModel.kt b/app/src/main/java/org/oxycblt/auxio/navigation/dialog/NavigationPickerViewModel.kt similarity index 69% rename from app/src/main/java/org/oxycblt/auxio/navigation/picker/NavigationPickerViewModel.kt rename to app/src/main/java/org/oxycblt/auxio/navigation/dialog/NavigationPickerViewModel.kt index dc14b8ea0..193d9553a 100644 --- a/app/src/main/java/org/oxycblt/auxio/navigation/picker/NavigationPickerViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/navigation/dialog/NavigationPickerViewModel.kt @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.navigation.picker +package org.oxycblt.auxio.navigation.dialog import androidx.lifecycle.ViewModel import dagger.hilt.android.lifecycle.HiltViewModel @@ -24,7 +24,6 @@ import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import org.oxycblt.auxio.music.* -import org.oxycblt.auxio.picker.PickerChoices /** * A [ViewModel] that stores the current information required for [ArtistNavigationPickerDialog]. @@ -36,7 +35,7 @@ class NavigationPickerViewModel @Inject constructor(private val musicRepository: ViewModel(), MusicRepository.UpdateListener { private val _currentArtistChoices = MutableStateFlow(null) /** The current set of [Artist] choices to show in the picker, or null if to show nothing. */ - val currentArtistChoices: StateFlow?> + val currentArtistChoices: StateFlow get() = _currentArtistChoices init { @@ -49,13 +48,13 @@ class NavigationPickerViewModel @Inject constructor(private val musicRepository: // Need to sanitize different items depending on the current set of choices. _currentArtistChoices.value = when (val choices = _currentArtistChoices.value) { - is ArtistNavigationChoices.FromSong -> + is SongArtistNavigationChoices -> deviceLibrary.findSong(choices.song.uid)?.let { - ArtistNavigationChoices.FromSong(it) + SongArtistNavigationChoices(it) } - is ArtistNavigationChoices.FromAlbum -> + is AlbumArtistNavigationChoices -> deviceLibrary.findAlbum(choices.album.uid)?.let { - ArtistNavigationChoices.FromAlbum(it) + AlbumArtistNavigationChoices(it) } else -> null } @@ -75,19 +74,32 @@ class NavigationPickerViewModel @Inject constructor(private val musicRepository: // Support Songs and Albums, which have parent artists. _currentArtistChoices.value = when (val music = musicRepository.find(uid)) { - is Song -> ArtistNavigationChoices.FromSong(music) - is Album -> ArtistNavigationChoices.FromAlbum(music) + is Song -> SongArtistNavigationChoices(music) + is Album -> AlbumArtistNavigationChoices(music) else -> null } } - - private sealed interface ArtistNavigationChoices : PickerChoices { - data class FromSong(val song: Song) : ArtistNavigationChoices { - override val choices = song.artists - } - - data class FromAlbum(val album: Album) : ArtistNavigationChoices { - override val choices = album.artists - } - } +} + +/** + * The current list of choices to show in the artist navigation picker dialog. + * + * @author Alexander Capehart (OxygenCobalt) + */ +sealed interface ArtistNavigationChoices { + /** The current [Artist] choices. */ + val choices: List +} + +/** 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 } diff --git a/app/src/main/java/org/oxycblt/auxio/navigation/picker/ArtistNavigationPickerDialog.kt b/app/src/main/java/org/oxycblt/auxio/navigation/picker/ArtistNavigationPickerDialog.kt deleted file mode 100644 index 24d49a8cf..000000000 --- a/app/src/main/java/org/oxycblt/auxio/navigation/picker/ArtistNavigationPickerDialog.kt +++ /dev/null @@ -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 . - */ - -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() { - 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?> - 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) - } -} diff --git a/app/src/main/java/org/oxycblt/auxio/picker/ChoiceAdapter.kt b/app/src/main/java/org/oxycblt/auxio/picker/ChoiceAdapter.kt deleted file mode 100644 index 723777018..000000000 --- a/app/src/main/java/org/oxycblt/auxio/picker/ChoiceAdapter.kt +++ /dev/null @@ -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 . - */ - -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(private val listener: ClickableListListener) : - FlexibleListAdapter>(ChoiceViewHolder.diffCallback()) { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = - ChoiceViewHolder.from(parent) - - override fun onBindViewHolder(holder: ChoiceViewHolder, 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 -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) { - 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 from(parent: View) = - ChoiceViewHolder(ItemPickerChoiceBinding.inflate(parent.context.inflater)) - - /** Get a comparator that can be used with DiffUtil. */ - fun diffCallback() = - object : SimpleDiffCallback() { - override fun areContentsTheSame(oldItem: T, newItem: T) = - oldItem.name == newItem.name - } - } -} diff --git a/app/src/main/java/org/oxycblt/auxio/picker/PickerChoices.kt b/app/src/main/java/org/oxycblt/auxio/picker/PickerChoices.kt deleted file mode 100644 index b2de58fd5..000000000 --- a/app/src/main/java/org/oxycblt/auxio/picker/PickerChoices.kt +++ /dev/null @@ -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 . - */ - -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 { - /** The list of choices to show. */ - val choices: List -} diff --git a/app/src/main/java/org/oxycblt/auxio/picker/PickerDialogFragment.kt b/app/src/main/java/org/oxycblt/auxio/picker/PickerDialogFragment.kt deleted file mode 100644 index abbf498fe..000000000 --- a/app/src/main/java/org/oxycblt/auxio/picker/PickerDialogFragment.kt +++ /dev/null @@ -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 . - */ - -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 : - ViewBindingDialogFragment(), ClickableListListener { - // 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?> - /** 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() - } -} diff --git a/app/src/main/java/org/oxycblt/auxio/playback/dialog/ArtistPlaybackPickerDialog.kt b/app/src/main/java/org/oxycblt/auxio/playback/dialog/ArtistPlaybackPickerDialog.kt new file mode 100644 index 000000000..9c74577d7 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/playback/dialog/ArtistPlaybackPickerDialog.kt @@ -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 . + */ + +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(), ClickableListListener { + 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) : + FlexibleListAdapter>(ChoiceViewHolder.diffCallback()) { + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): ChoiceViewHolder = ChoiceViewHolder.from(parent) + + override fun onBindViewHolder(holder: ChoiceViewHolder, position: Int) { + holder.bind(getItem(position), listener) + } + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/playback/dialog/GenrePlaybackPickerDialog.kt b/app/src/main/java/org/oxycblt/auxio/playback/dialog/GenrePlaybackPickerDialog.kt new file mode 100644 index 000000000..9dbb271f1 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/playback/dialog/GenrePlaybackPickerDialog.kt @@ -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 . + */ + +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(), ClickableListListener { + 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) : + FlexibleListAdapter>(ChoiceViewHolder.diffCallback()) { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChoiceViewHolder = + ChoiceViewHolder.from(parent) + + override fun onBindViewHolder(holder: ChoiceViewHolder, position: Int) { + holder.bind(getItem(position), listener) + } + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/playback/picker/PlaybackPickerViewModel.kt b/app/src/main/java/org/oxycblt/auxio/playback/dialog/PlaybackPickerViewModel.kt similarity index 51% rename from app/src/main/java/org/oxycblt/auxio/playback/picker/PlaybackPickerViewModel.kt rename to app/src/main/java/org/oxycblt/auxio/playback/dialog/PlaybackPickerViewModel.kt index 2fcd20e13..fea0d04da 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/picker/PlaybackPickerViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/dialog/PlaybackPickerViewModel.kt @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.playback.picker +package org.oxycblt.auxio.playback.dialog import androidx.lifecycle.ViewModel import dagger.hilt.android.lifecycle.HiltViewModel @@ -24,7 +24,6 @@ import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import org.oxycblt.auxio.music.* -import org.oxycblt.auxio.picker.PickerChoices /** * A [ViewModel] that stores the choices shown in the playback picker dialogs. @@ -34,15 +33,10 @@ import org.oxycblt.auxio.picker.PickerChoices @HiltViewModel class PlaybackPickerViewModel @Inject constructor(private val musicRepository: MusicRepository) : ViewModel(), MusicRepository.UpdateListener { - private val _currentArtistChoices = MutableStateFlow(null) + private val _currentPickerSong = MutableStateFlow(null) /** The current set of [Artist] choices to show in the picker, or null if to show nothing. */ - val currentArtistChoices: StateFlow - get() = _currentArtistChoices - - private val _currentGenreChoices = MutableStateFlow(null) - /** The current set of [Genre] choices to show in the picker, or null if to show nothing. */ - val currentGenreChoices: StateFlow - get() = _currentGenreChoices + val currentPickerSong: StateFlow + get() = _currentPickerSong init { musicRepository.addUpdateListener(this) @@ -51,14 +45,7 @@ class PlaybackPickerViewModel @Inject constructor(private val musicRepository: M override fun onMusicChanges(changes: MusicRepository.Changes) { if (!changes.deviceLibrary) return val deviceLibrary = musicRepository.deviceLibrary ?: return - _currentArtistChoices.value = - _currentArtistChoices.value?.run { - deviceLibrary.findSong(song.uid)?.let { newSong -> ArtistPlaybackChoices(newSong) } - } - _currentGenreChoices.value = - _currentGenreChoices.value?.run { - deviceLibrary.findSong(song.uid)?.let { newSong -> GenrePlaybackChoices(newSong) } - } + _currentPickerSong.value = _currentPickerSong.value?.run { deviceLibrary.findSong(uid) } } 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]. */ - fun setArtistChoiceUid(uid: Music.UID) { - _currentArtistChoices.value = - 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) } + fun setPickerSongUid(uid: Music.UID) { + _currentPickerSong.value = musicRepository.deviceLibrary?.findSong(uid) } } - -data class ArtistPlaybackChoices(val song: Song) : PickerChoices { - override val choices = song.artists -} - -data class GenrePlaybackChoices(val song: Song) : PickerChoices { - override val choices = song.genres -} diff --git a/app/src/main/java/org/oxycblt/auxio/playback/picker/ArtistPlaybackPickerDialog.kt b/app/src/main/java/org/oxycblt/auxio/playback/picker/ArtistPlaybackPickerDialog.kt deleted file mode 100644 index ff8dbabfa..000000000 --- a/app/src/main/java/org/oxycblt/auxio/playback/picker/ArtistPlaybackPickerDialog.kt +++ /dev/null @@ -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 . - */ - -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() { - 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?> - 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) - } -} diff --git a/app/src/main/java/org/oxycblt/auxio/playback/picker/GenrePlaybackPickerDialog.kt b/app/src/main/java/org/oxycblt/auxio/playback/picker/GenrePlaybackPickerDialog.kt deleted file mode 100644 index e4af11d41..000000000 --- a/app/src/main/java/org/oxycblt/auxio/playback/picker/GenrePlaybackPickerDialog.kt +++ /dev/null @@ -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 . - */ - -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() { - 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?> - 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) - } -} diff --git a/app/src/main/res/layout/dialog_music_picker.xml b/app/src/main/res/layout/dialog_music_picker.xml index f7f81f9af..7137339c7 100644 --- a/app/src/main/res/layout/dialog_music_picker.xml +++ b/app/src/main/res/layout/dialog_music_picker.xml @@ -2,7 +2,7 @@