diff --git a/app/src/main/java/org/oxycblt/auxio/AuxioApp.kt b/app/src/main/java/org/oxycblt/auxio/AuxioApp.kt index 6b4202953..1e645d506 100644 --- a/app/src/main/java/org/oxycblt/auxio/AuxioApp.kt +++ b/app/src/main/java/org/oxycblt/auxio/AuxioApp.kt @@ -74,10 +74,9 @@ class AuxioApp : Application(), ImageLoaderFactory { .build() companion object { - /** The ID of the "Shuffle All" shortcut. */ - const val SHORTCUT_SHUFFLE_ID = "shortcut_shuffle" - /** The [Intent] name for the "Shuffle All" shortcut. */ const val INTENT_KEY_SHORTCUT_SHUFFLE = BuildConfig.APPLICATION_ID + ".action.SHUFFLE_ALL" + /** The ID of the "Shuffle All" shortcut. */ + private const val SHORTCUT_SHUFFLE_ID = "shortcut_shuffle" } } diff --git a/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt b/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt index e7a16b502..e2cbcb5ce 100644 --- a/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt +++ b/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt @@ -61,6 +61,8 @@ object IntegerTable { const val REPEAT_MODE_ALL = 0xA101 /** RepeatMode.TRACK */ const val REPEAT_MODE_TRACK = 0xA102 + /** PlaybackMode.IN_GENRE */ + const val PLAYBACK_MODE_IN_GENRE = 0xA103 /** PlaybackMode.IN_ARTIST */ const val PLAYBACK_MODE_IN_ARTIST = 0xA104 /** PlaybackMode.IN_ALBUM */ diff --git a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt index 30fdfac67..e66ef86cc 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt @@ -128,9 +128,10 @@ class MainFragment : // --- VIEWMODEL SETUP --- collect(navModel.mainNavigationAction, ::handleMainNavigation) collect(navModel.exploreNavigationItem, ::handleExploreNavigation) - collect(navModel.exploreNavigationArtists, ::handleExplorePicker) + collect(navModel.exploreArtistNavigationItem, ::handleArtistNavigationPicker) collectImmediately(playbackModel.song, ::updateSong) - collect(playbackModel.artistPlaybackPickerSong, ::handlePlaybackArtistPicker) + collect(playbackModel.artistPickerSong, ::handlePlaybackArtistPicker) + collect(playbackModel.genrePickerSong, ::handlePlaybackGenrePicker) } override fun onStart() { @@ -278,13 +279,11 @@ class MainFragment : } } - private fun handleExplorePicker(items: List?) { - if (items != null) { - // Navigate to the analogous artist picker dialog. + private fun handleArtistNavigationPicker(item: Music?) { + if (item != null) { navModel.mainNavigateTo( MainNavigationAction.Directions( - MainFragmentDirections.actionPickNavigationArtist( - items.map { it.uid }.toTypedArray()))) + MainFragmentDirections.actionPickNavigationArtist(item.uid))) navModel.finishExploreNavigation() } } @@ -299,7 +298,6 @@ class MainFragment : private fun handlePlaybackArtistPicker(song: Song?) { if (song != null) { - // Navigate to the analogous artist picker dialog. navModel.mainNavigateTo( MainNavigationAction.Directions( MainFragmentDirections.actionPickPlaybackArtist(song.uid))) @@ -307,6 +305,15 @@ class MainFragment : } } + private fun handlePlaybackGenrePicker(song: Song?) { + if (song != null) { + navModel.mainNavigateTo( + MainNavigationAction.Directions( + MainFragmentDirections.actionPickPlaybackGenre(song.uid))) + playbackModel.finishPlaybackArtistPicker() + } + } + private fun tryExpandSheets() { val binding = requireBinding() val playbackSheetBehavior = diff --git a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt index 3ea4dc802..86efe4cd6 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt @@ -128,14 +128,14 @@ class AlbumDetailFragment : ListFragment(), AlbumDetailAd override fun onRealClick(music: Music) { check(music is Song) { "Unexpected datatype: ${music::class.java}" } - when (val mode = Settings(requireContext()).detailPlaybackMode) { + when (Settings(requireContext()).detailPlaybackMode) { // "Play from shown item" and "Play from album" functionally have the same // behavior since a song can only have one album. null, MusicMode.ALBUMS -> playbackModel.playFromAlbum(music) MusicMode.SONGS -> playbackModel.playFromAll(music) MusicMode.ARTISTS -> playbackModel.playFromArtist(music) - else -> error("Unexpected playback mode: $mode") + MusicMode.GENRES -> playbackModel.playFromGenre(music) } } @@ -171,7 +171,7 @@ class AlbumDetailFragment : ListFragment(), AlbumDetailAd } override fun onNavigateToParentArtist() { - navModel.exploreNavigateTo(unlikelyToBeNull(detailModel.currentAlbum.value).artists) + navModel.exploreNavigateToParentArtist(unlikelyToBeNull(detailModel.currentAlbum.value)) } private fun updateAlbum(album: Album?) { @@ -180,7 +180,6 @@ class AlbumDetailFragment : ListFragment(), AlbumDetailAd findNavController().navigateUp() return } - requireBinding().detailToolbar.title = album.resolveName(requireContext()) } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt index 28cb2ef1b..e1d103861 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt @@ -124,16 +124,16 @@ class ArtistDetailFragment : ListFragment(), DetailAdapte override fun onRealClick(music: Music) { when (music) { is Song -> { - when (val mode = Settings(requireContext()).detailPlaybackMode) { - // "Play from selected item" and "Play from artist" differ, as the latter - // actually should show a picker choice in the case of multiple artists. + when (Settings(requireContext()).detailPlaybackMode) { + // When configured to play from the selected item, we already have an Artist + // to play from. null -> playbackModel.playFromArtist( music, unlikelyToBeNull(detailModel.currentArtist.value)) MusicMode.SONGS -> playbackModel.playFromAll(music) MusicMode.ALBUMS -> playbackModel.playFromAlbum(music) MusicMode.ARTISTS -> playbackModel.playFromArtist(music) - else -> error("Unexpected playback mode: $mode") + MusicMode.GENRES -> playbackModel.playFromGenre(music) } } is Album -> navModel.exploreNavigateTo(music) diff --git a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt index bb266d1cd..bfeca52c3 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt @@ -124,15 +124,16 @@ class GenreDetailFragment : ListFragment(), DetailAdapter when (music) { is Artist -> navModel.exploreNavigateTo(music) is Song -> - when (val mode = Settings(requireContext()).detailPlaybackMode) { - // Only way to play from the genre is through "Play from selected item". + when (Settings(requireContext()).detailPlaybackMode) { + // When configured to play from the selected item, we already have a Genre + // to play from. null -> playbackModel.playFromGenre( music, unlikelyToBeNull(detailModel.currentGenre.value)) MusicMode.SONGS -> playbackModel.playFromAll(music) MusicMode.ALBUMS -> playbackModel.playFromAlbum(music) MusicMode.ARTISTS -> playbackModel.playFromArtist(music) - else -> error("Unexpected playback mode: $mode") + MusicMode.GENRES -> playbackModel.playFromGenre(music) } else -> error("Unexpected datatype: ${music::class.simpleName}") } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt b/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt index fb51a4593..e7444ab16 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt @@ -52,7 +52,7 @@ class SongDetailDialog : ViewBindingDialogFragment() { override fun onBindingCreated(binding: DialogSongDetailBinding, savedInstanceState: Bundle?) { super.onBindingCreated(binding, savedInstanceState) // DetailViewModel handles most initialization from the navigation argument. - detailModel.setSongUid(args.songUid) + detailModel.setSongUid(args.itemUid) collectImmediately(detailModel.currentSong, ::updateSong) } diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt index ed68a2fde..8915605b2 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt @@ -155,9 +155,7 @@ class HomeFragment : collect(homeModel.shouldRecreate, ::handleRecreate) collectImmediately(homeModel.currentTabMode, ::updateCurrentTab) collectImmediately(homeModel.songLists, homeModel.isFastScrolling, ::updateFab) - collectImmediately(musicModel.indexerState, ::updateIndexerState) - collect(navModel.exploreNavigationItem, ::handleNavigation) collectImmediately(selectionModel.selected, ::updateSelection) } @@ -484,9 +482,7 @@ class HomeFragment : fragmentManager: FragmentManager, lifecycleOwner: LifecycleOwner ) : FragmentStateAdapter(fragmentManager, lifecycleOwner.lifecycle) { - override fun getItemCount() = tabs.size - override fun createFragment(position: Int): Fragment = when (tabs[position]) { MusicMode.SONGS -> SongListFragment() diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt index 6fc1a257c..c040761fc 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt @@ -132,11 +132,11 @@ class SongListFragment : override fun onRealClick(music: Music) { check(music is Song) { "Unexpected datatype: ${music::class.java}" } - when (val mode = Settings(requireContext()).libPlaybackMode) { + when (Settings(requireContext()).libPlaybackMode) { MusicMode.SONGS -> playbackModel.playFromAll(music) MusicMode.ALBUMS -> playbackModel.playFromAlbum(music) MusicMode.ARTISTS -> playbackModel.playFromArtist(music) - else -> error("Unexpected playback mode: $mode") + MusicMode.GENRES -> playbackModel.playFromGenre(music) } } diff --git a/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt b/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt index 79c2db68f..f8071816e 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt @@ -90,7 +90,7 @@ abstract class ListFragment : SelectionFragment(), Selecta requireContext().showToast(R.string.lng_queue_added) } R.id.action_go_artist -> { - navModel.exploreNavigateTo(song.artists) + navModel.exploreNavigateToParentArtist(song) } R.id.action_go_album -> { navModel.exploreNavigateTo(song.album) @@ -134,7 +134,7 @@ abstract class ListFragment : SelectionFragment(), Selecta requireContext().showToast(R.string.lng_queue_added) } R.id.action_go_artist -> { - navModel.exploreNavigateTo(album.artists) + navModel.exploreNavigateToParentArtist(album) } else -> { error("Unexpected menu item selected") diff --git a/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistChoiceAdapter.kt b/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistChoiceAdapter.kt index 73065eabc..89c502d04 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistChoiceAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistChoiceAdapter.kt @@ -28,7 +28,7 @@ import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.inflater /** - * An adapter responsible for showing a list of [Artist] choices in [ArtistPickerDialog]. + * An [RecyclerView.Adapter] that displays a list of [Artist] choices. * @param listener A [ClickableListListener] to bind interactions to. * @author OxygenCobalt. */ diff --git a/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistNavigationPickerDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistNavigationPickerDialog.kt index 86f9af08b..a8ea49687 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistNavigationPickerDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistNavigationPickerDialog.kt @@ -31,12 +31,12 @@ import org.oxycblt.auxio.ui.NavigationViewModel */ class ArtistNavigationPickerDialog : ArtistPickerDialog() { private val navModel: NavigationViewModel by activityViewModels() - // Information about what artists to display is initially within the navigation arguments - // as a list of UIDs, as that is the only safe way to parcel an artist. + // 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 fun onBindingCreated(binding: DialogMusicPickerBinding, savedInstanceState: Bundle?) { - pickerModel.setArtistUids(args.artistUids) + pickerModel.setItemUid(args.itemUid) super.onBindingCreated(binding, savedInstanceState) } diff --git a/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistPickerDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistPickerDialog.kt index 7c59364db..30b5dd996 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistPickerDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistPickerDialog.kt @@ -52,11 +52,9 @@ abstract class ArtistPickerDialog : override fun onBindingCreated(binding: DialogMusicPickerBinding, savedInstanceState: Bundle?) { binding.pickerRecycler.adapter = artistAdapter - collectImmediately(pickerModel.currentArtists) { artists -> - if (!artists.isNullOrEmpty()) { + collectImmediately(pickerModel.artistChoices) { artists -> + if (artists.isNotEmpty()) { // Make sure the artist choices align with any changes in the music library. - // TODO: I really don't think it makes sense to do this. I'd imagine it would - // be more productive to just exit this dialog rather than try to update it. artistAdapter.submitList(artists) } else { // Not showing any choices, navigate up. diff --git a/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistPlaybackPickerDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistPlaybackPickerDialog.kt index c08b6cc12..b81e2604a 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistPlaybackPickerDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistPlaybackPickerDialog.kt @@ -22,6 +22,7 @@ import androidx.navigation.fragment.navArgs import org.oxycblt.auxio.databinding.DialogMusicPickerBinding import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.music.Artist +import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.util.androidActivityViewModels @@ -31,19 +32,21 @@ import org.oxycblt.auxio.util.androidActivityViewModels */ class ArtistPlaybackPickerDialog : ArtistPickerDialog() { private val playbackModel: PlaybackViewModel by androidActivityViewModels() - // Information about what artists to display is initially within the navigation arguments - // as a list of UIDs, as that is the only safe way to parcel an artist. + // 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 fun onBindingCreated(binding: DialogMusicPickerBinding, savedInstanceState: Bundle?) { - pickerModel.setSongUid(args.songUid) + pickerModel.setItemUid(args.itemUid) super.onBindingCreated(binding, savedInstanceState) } override fun onClick(item: Item) { super.onClick(item) - check(item is Artist) { "Unexpected datatype: ${item::class.simpleName}" } // User made a choice, play the given song from that artist. - pickerModel.currentSong.value?.let { song -> playbackModel.playFromArtist(song, item) } + check(item is Artist) { "Unexpected datatype: ${item::class.simpleName}" } + val song = pickerModel.currentItem.value + check(song is Song) { "Unexpected datatype: ${item::class.simpleName}" } + playbackModel.playFromArtist(song, item) } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/picker/GenreChoiceAdapter.kt b/app/src/main/java/org/oxycblt/auxio/music/picker/GenreChoiceAdapter.kt new file mode 100644 index 000000000..57f8322ce --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/music/picker/GenreChoiceAdapter.kt @@ -0,0 +1,68 @@ +package org.oxycblt.auxio.music.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.recycler.DialogRecyclerView +import org.oxycblt.auxio.music.Genre +import org.oxycblt.auxio.util.context +import org.oxycblt.auxio.util.inflater + +/** + * An [RecyclerView.Adapter] that displays a list of [Genre] choices. + * @param listener A [ClickableListListener] to bind interactions to. + * @author OxygenCobalt. + */ +class GenreChoiceAdapter(private val listener: ClickableListListener) : + RecyclerView.Adapter() { + private var genres = listOf() + + override fun getItemCount() = genres.size + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = + GenreChoiceViewHolder.new(parent) + + override fun onBindViewHolder(holder: GenreChoiceViewHolder, position: Int) = + holder.bind(genres[position], listener) + + /** + * Immediately update the [Genre] choices. + * @param newGenres The new [Genre]s to show. + */ + fun submitList(newGenres: List) { + if (newGenres != genres) { + genres = newGenres + @Suppress("NotifyDataSetChanged") notifyDataSetChanged() + } + } +} + +/** + * A [DialogRecyclerView.ViewHolder] that displays a smaller variant of a typical [Genre] item, for + * use with [GenreChoiceAdapter]. Use [new] to create an instance. + */ +class GenreChoiceViewHolder(private val binding: ItemPickerChoiceBinding) : + DialogRecyclerView.ViewHolder(binding.root) { + /** + * Bind new data to this instance. + * @param genre The new [Genre] to bind. + * @param listener A [ClickableListListener] to bind interactions to. + */ + fun bind(genre: Genre, listener: ClickableListListener) { + binding.root.setOnClickListener { listener.onClick(genre) } + binding.pickerImage.bind(genre) + binding.pickerName.text = genre.resolveName(binding.context) + } + + companion object { + /** + * Create a new instance. + * @param parent The parent to inflate this instance from. + * @return A new instance. + */ + fun new(parent: View) = + GenreChoiceViewHolder(ItemPickerChoiceBinding.inflate(parent.context.inflater)) + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/music/picker/GenrePlaybackPickerDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/picker/GenrePlaybackPickerDialog.kt new file mode 100644 index 000000000..a5bba8d48 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/music/picker/GenrePlaybackPickerDialog.kt @@ -0,0 +1,66 @@ +package org.oxycblt.auxio.music.picker + +import android.os.Bundle +import android.view.LayoutInflater +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.viewModels +import androidx.navigation.fragment.findNavController +import androidx.navigation.fragment.navArgs +import org.oxycblt.auxio.R +import org.oxycblt.auxio.databinding.DialogMusicPickerBinding +import org.oxycblt.auxio.list.ClickableListListener +import org.oxycblt.auxio.list.Item +import org.oxycblt.auxio.music.Genre +import org.oxycblt.auxio.music.Song +import org.oxycblt.auxio.playback.PlaybackViewModel +import org.oxycblt.auxio.ui.ViewBindingDialogFragment +import org.oxycblt.auxio.util.androidActivityViewModels +import org.oxycblt.auxio.util.collectImmediately + +/** + * A picker [ViewBindingDialogFragment] intended for when [Genre] playback is ambiguous. + * @author Alexander Capehart (OxygenCobalt) + */ +class GenrePlaybackPickerDialog : ViewBindingDialogFragment(), ClickableListListener { + private val pickerModel: PickerViewModel by viewModels() + private val playbackModel: PlaybackViewModel by androidActivityViewModels() + // 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() + // Okay to leak this since the Listener will not be called until after initialization. + private val genreAdapter = GenreChoiceAdapter(@Suppress("LeakingThis") this) + + override fun onCreateBinding(inflater: LayoutInflater) = + DialogMusicPickerBinding.inflate(inflater) + + override fun onConfigDialog(builder: AlertDialog.Builder) { + builder.setTitle(R.string.lbl_genres).setNegativeButton(R.string.lbl_cancel, null) + } + + override fun onBindingCreated(binding: DialogMusicPickerBinding, savedInstanceState: Bundle?) { + binding.pickerRecycler.adapter = genreAdapter + + pickerModel.setItemUid(args.itemUid) + collectImmediately(pickerModel.genreChoices) { genres -> + if (genres.isNotEmpty()) { + // Make sure the genre choices align with any changes in the music library. + genreAdapter.submitList(genres) + } else { + // Not showing any choices, navigate up. + findNavController().navigateUp() + } + } + } + + override fun onDestroyBinding(binding: DialogMusicPickerBinding) { + binding.pickerRecycler.adapter = null + } + + override fun onClick(item: Item) { + // User made a choice, play the given song from that genre. + check(item is Genre) { "Unexpected datatype: ${item::class.simpleName}" } + val song = pickerModel.currentItem.value + check(song is Song) { "Unexpected datatype: ${item::class.simpleName}" } + playbackModel.playFromGenre(song, item) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/oxycblt/auxio/music/picker/PickerViewModel.kt b/app/src/main/java/org/oxycblt/auxio/music/picker/PickerViewModel.kt index f23f9d06a..0fee83bcd 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/picker/PickerViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/picker/PickerViewModel.kt @@ -20,10 +20,7 @@ package org.oxycblt.auxio.music.picker import androidx.lifecycle.ViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import org.oxycblt.auxio.music.Artist -import org.oxycblt.auxio.music.Music -import org.oxycblt.auxio.music.MusicStore -import org.oxycblt.auxio.music.Song +import org.oxycblt.auxio.music.* import org.oxycblt.auxio.util.unlikelyToBeNull /** @@ -32,24 +29,21 @@ import org.oxycblt.auxio.util.unlikelyToBeNull * @author Alexander Capehart (OxygenCobalt) */ class PickerViewModel : ViewModel(), MusicStore.Callback { - // TODO: Refactor - private val musicStore = MusicStore.getInstance() - private val _currentSong = MutableStateFlow(null) - /** - * The current [Song] whose choices are being shown in the picker. Null if there is no [Song]. - */ - val currentSong: StateFlow - get() = _currentSong + private val _currentItem = MutableStateFlow(null) + /** The current item whose artists should be shown in the picker. Null if there is no item. */ + val currentItem: StateFlow get() = _currentItem - private val _currentArtists = MutableStateFlow?>(null) - /** - * The current [Artist] whose choices are being shown in the picker. Null/Empty if there is - * none. - */ - val currentArtists: StateFlow?> - get() = _currentArtists + private val _artistChoices = MutableStateFlow>(listOf()) + /** The current [Artist] choices. Empty if no item is shown in the picker. */ + val artistChoices: StateFlow> + get() = _artistChoices + + private val _genreChoices = MutableStateFlow>(listOf()) + /** The current [Genre] choices. Empty if no item is shown in the picker. */ + val genreChoices: StateFlow> + get() = _genreChoices override fun onCleared() { musicStore.removeCallback(this) @@ -57,37 +51,29 @@ class PickerViewModel : ViewModel(), MusicStore.Callback { override fun onLibraryChanged(library: MusicStore.Library?) { if (library != null) { - // If we are showing any item right now, we will need to refresh it (and any information - // related to it) with the new library in order to prevent stale items from appearing - // in the UI. - val song = _currentSong.value - val artists = _currentArtists.value - if (song != null) { - _currentSong.value = library.sanitize(song) - _currentArtists.value = _currentSong.value?.artists - } else if (artists != null) { - _currentArtists.value = artists.mapNotNull { library.sanitize(it) } - } + refreshChoices() } } /** - * Set a new [currentSong] from it's [Music.UID]. + * Set a new [currentItem] from it's [Music.UID]. * @param uid The [Music.UID] of the [Song] to update to. */ - fun setSongUid(uid: Music.UID) { + fun setItemUid(uid: Music.UID) { val library = unlikelyToBeNull(musicStore.library) - _currentSong.value = library.find(uid) - _currentArtists.value = _currentSong.value?.artists + _currentItem.value = library.find(uid) + refreshChoices() } - /** - * Set a new [currentArtists] list from a list of [Music.UID]'s. - * @param uids The [Music.UID]s of the [Artist]s to [currentArtists] to. - */ - fun setArtistUids(uids: Array) { - val library = unlikelyToBeNull(musicStore.library) - // Map the UIDs to artist instances and filter out the ones that can't be found. - _currentArtists.value = uids.mapNotNull { library.find(it) }.ifEmpty { null } + private fun refreshChoices() { + when (val item = _currentItem.value) { + is Song -> { + _artistChoices.value = item.artists + _genreChoices.value = item.genres + } + is Album -> _artistChoices.value = item.artists + else -> {} + } } + } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt index 26cace816..fd9ebc425 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt @@ -210,7 +210,7 @@ class PlaybackPanelFragment : /** Navigate to one of the currently playing [Song]'s Artists. */ private fun navigateToCurrentArtist() { val song = playbackModel.song.value ?: return - navModel.exploreNavigateTo(song.artists) + navModel.exploreNavigateToParentArtist(song) } /** Navigate to the currently playing [Song]'s albums. */ diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt index 85f8eeff0..14b668428 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt @@ -70,13 +70,21 @@ class PlaybackViewModel(application: Application) : private val _artistPlaybackPickerSong = MutableStateFlow(null) /** - * Flag signaling to open a picker dialog in order to resolve an ambiguous [Artist] choice when + * Flag signaling to open a picker dialog in order to resolve an ambiguous choice when * playing a [Song] from one of it's [Artist]s. * @see playFromArtist */ - val artistPlaybackPickerSong: StateFlow + val artistPickerSong: StateFlow get() = _artistPlaybackPickerSong + private val _genrePlaybackPickerSong = MutableStateFlow(null) + /** + * Flag signaling to open a picker dialog in order to resolve an ambiguous choice when playing + * a [Song] from one of it's [Genre]s. + */ + val genrePickerSong: StateFlow + get() = _genrePlaybackPickerSong + /** * The current audio session ID of the internal player. Null if no [InternalPlayer] is * available. @@ -178,11 +186,18 @@ class PlaybackViewModel(application: Application) : /** * PLay a [Song] from one of it's [Genre]s. * @param song The [Song] to play. - * @param genre The [Genre] to play from. Must be linked to the [Song]. + * @param genre The [Genre] to play from. Must be linked to the [Song]. If null, the user will + * be prompted on what artist to play. Defaults to null. */ - fun playFromGenre(song: Song, genre: Genre) { - check(genre.songs.contains(song)) { "Invalid input: Genre is not linked to song" } - playbackManager.play(song, genre, settings) + fun playFromGenre(song: Song, genre: Genre? = null) { + if (genre != null) { + check(genre.songs.contains(song)) { "Invalid input: Genre is not linked to song" } + playbackManager.play(song, genre, settings) + } else if (song.genres.size == 1) { + playbackManager.play(song, song.genres[0], settings) + } else { + _genrePlaybackPickerSong.value = song + } } /** diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt index 7a586b538..0a7154855 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt @@ -137,11 +137,11 @@ class SearchFragment : ListFragment() { override fun onRealClick(music: Music) { when (music) { is Song -> - when (val mode = Settings(requireContext()).libPlaybackMode) { + when (Settings(requireContext()).libPlaybackMode) { MusicMode.SONGS -> playbackModel.playFromAll(music) MusicMode.ALBUMS -> playbackModel.playFromAlbum(music) MusicMode.ARTISTS -> playbackModel.playFromArtist(music) - else -> error("Unexpected playback mode: $mode") + MusicMode.GENRES -> playbackModel.playFromGenre(music) } is MusicParent -> navModel.exploreNavigateTo(music) } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt b/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt index aac9bded4..7bd6d6b92 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt @@ -113,10 +113,11 @@ class Settings(private val context: Context, private val callback: Callback? = n fun Int.migratePlaybackMode() = when (this) { - // Genre playback mode was removed in 3.0.0 + // Convert PlaybackMode into MusicMode IntegerTable.PLAYBACK_MODE_ALL_SONGS -> MusicMode.SONGS IntegerTable.PLAYBACK_MODE_IN_ARTIST -> MusicMode.ARTISTS IntegerTable.PLAYBACK_MODE_IN_ALBUM -> MusicMode.ALBUMS + IntegerTable.PLAYBACK_MODE_IN_GENRE -> MusicMode.GENRES else -> null } diff --git a/app/src/main/java/org/oxycblt/auxio/ui/NavigationViewModel.kt b/app/src/main/java/org/oxycblt/auxio/ui/NavigationViewModel.kt index ad433a7cd..905862d7d 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/NavigationViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/NavigationViewModel.kt @@ -21,8 +21,10 @@ import androidx.lifecycle.ViewModel import androidx.navigation.NavDirections import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Music +import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.util.logD /** A [ViewModel] that handles complicated navigation functionality. */ @@ -42,14 +44,14 @@ class NavigationViewModel : ViewModel() { val exploreNavigationItem: StateFlow get() = _exploreNavigationItem - private val _exploreNavigationArtists = MutableStateFlow?>(null) + private val _exploreArtistNavigationItem = MutableStateFlow(null) /** - * Variation of [exploreNavigationItem] for situations where the choice of [Artist] to navigate - * to is ambiguous. Only intended for use by MainFragment, as the resolved choice will + * Variation of [exploreNavigationItem] for situations where the choice of parent [Artist] to + * navigate to is ambiguous. Only intended for use by MainFragment, as the resolved choice will * eventually be assigned to [exploreNavigationItem]. */ - val exploreNavigationArtists: StateFlow?> - get() = _exploreNavigationArtists + val exploreArtistNavigationItem: StateFlow + get() = _exploreArtistNavigationItem /** * Navigate to something in the main navigation graph. This can be used by UIs in the explore @@ -89,12 +91,25 @@ class NavigationViewModel : ViewModel() { } /** - * Navigate to an [Artist] out of a list of [Artist]s, like [exploreNavigateTo]. - * @param artists The [Artist]s to navigate to. In the case of multiple artists, the user will - * be prompted with a choice on which [Artist] to navigate to. + * Navigate to one of the parent [Artist]'s of the given [Song]. + * @param song The [Song] to navigate with. If there are multiple parent [Artist]s, + * a picker dialog will be shown. */ - fun exploreNavigateTo(artists: List) { - if (_exploreNavigationArtists.value != null) { + fun exploreNavigateToParentArtist(song: Song) { + exploreNavigateToParentArtistImpl(song, song.artists) + } + + /** + * Navigate to one of the parent [Artist]'s of the given [Album]. + * @param album The [Album] to navigate with. If there are multiple parent [Artist]s, + * a picker dialog will be shown. + */ + fun exploreNavigateToParentArtist(album: Album) { + exploreNavigateToParentArtistImpl(album, album.artists) + } + + private fun exploreNavigateToParentArtistImpl(item: Music, artists: List) { + if (_exploreArtistNavigationItem.value != null) { logD("Already navigating, not doing explore action") return } @@ -103,7 +118,7 @@ class NavigationViewModel : ViewModel() { exploreNavigateTo(artists[0]) } else { logD("Navigating to a choice of ${artists.map { it.rawName }}") - _exploreNavigationArtists.value = artists + _exploreArtistNavigationItem.value = item } } @@ -114,7 +129,7 @@ class NavigationViewModel : ViewModel() { fun finishExploreNavigation() { logD("Finishing explore navigation process") _exploreNavigationItem.value = null - _exploreNavigationArtists.value = null + _exploreArtistNavigationItem.value = null } } diff --git a/app/src/main/res/navigation/nav_main.xml b/app/src/main/res/navigation/nav_main.xml index bfd0dff88..5b39353e2 100644 --- a/app/src/main/res/navigation/nav_main.xml +++ b/app/src/main/res/navigation/nav_main.xml @@ -23,6 +23,9 @@ + @@ -41,8 +44,18 @@ android:label="artist_navigation_picker_dialog" tools:layout="@layout/dialog_music_picker"> + android:name="itemUid" + app:argType="org.oxycblt.auxio.music.Music$UID" /> + + + + diff --git a/app/src/main/res/values-ar-rIQ/strings.xml b/app/src/main/res/values-ar-rIQ/strings.xml index b39639d0f..d042db1a9 100644 --- a/app/src/main/res/values-ar-rIQ/strings.xml +++ b/app/src/main/res/values-ar-rIQ/strings.xml @@ -23,7 +23,7 @@ يعمل الآن تشغيل خلط - تشغيل من جميع الاغاني + تشغيل من جميع الاغاني تشغيل من البوم تشغيل من فنان طابور diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 8d932a3e2..9d1b3e493 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -29,7 +29,7 @@ Právě hraje Přehrát Náhodně - Přehrát ze všech skladeb + Přehrát ze všech skladeb Přehrát z alba Přehrát od umělce Fronta diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index b98f33f58..f61bbe49f 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -15,7 +15,7 @@ Aufsteigend Abspielen Zufällig - Von allen Lieder abspielen + Von allen Lieder abspielen Von Album abspielen Aktuelle Wiedergabe Warteschlange diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 3f501b18b..64b1238fd 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -23,7 +23,7 @@ En reproducción Reproducir Mezcla - Reproducir todo + Reproducir todo Reproducir por álbum Reproducir por artista Cola diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index ad9d34fdb..c5d658a93 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -85,7 +85,7 @@ Upozorenje: Postavljanje pretpojačala na visoke razine može uzrokovati vrhunce tonova u nekim zvučnim zapisima. Kada se reproducira iz zbirke Kada se reproducira iz detalja o predmetu - Reproduciraj od svih pjesama + Reproduciraj od svih pjesama Aktualiziraj glazbu Ponovo učitaj glazbenu biblioteku, koristeći predmemorirane oznake kada je to moguće Mape glazbe diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index ff631265c..6caf259cc 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -95,7 +95,7 @@ Penyesuaian dengan tag Peringatan: Mengubah pre-amp ke nilai positif yang tinggi dapat mengakibatkan puncak pada beberapa trek audio. Putar dari item yang ditampilkan - Putar dari semua lagu + Putar dari semua lagu Tetap mengacak saat memutar lagu baru Jeda pada pengulangan Putar balik sebelum melompat ke belakang diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 1eeb25707..f5ea13fdc 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -23,7 +23,7 @@ Ora in riproduzione Riproduci Mescola - Riproduci da tutte le canzoni + Riproduci da tutte le canzoni Riproduci dal disco Riproduci dall\'artista Coda diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 217658d04..08e2d35e9 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -27,7 +27,7 @@ 지금 재생 중 재생 셔플 - 모든 곡에서 재생 + 모든 곡에서 재생 앨범에서 재생 아티스트에서 재생 대기열 diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index 48b105bc6..7b9d9c40e 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -176,7 +176,7 @@ Muzika nebus įkeliama iš pridėtų aplankų jūs pridėsite. Įtraukti Pašalinti šią eilės dainą - Groti iš visų dainų + Groti iš visų dainų Groti iš parodyto elemento Groti iš albumo Groti iš atlikėjo diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index a203c7270..39cd78f1d 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -17,7 +17,7 @@ Oplopend Afspelen Shuffle - Speel van alle nummers + Speel van alle nummers Speel af van album Speel van artiest Afspeelscherm diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index a6edb271d..56dcd9ffc 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -136,7 +136,7 @@ Equalizer Rozmiar Brak folderów - Odtwórz wszystkie utwory + Odtwórz wszystkie utwory Odtwórz album Zacznij odtawrzanie po podłączeniu słuchawek (może nie działać na wszystkich urządzeniach) Załaduj ponownie bibliotekę diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index ddb9fd676..3599d3f37 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -166,7 +166,7 @@ Ativar cantos arredondados em elementos adicionais da interface do usuário (requer que as capas dos álbuns sejam arredondadas) Modo de normalização de volume (ReplayGain) Reproduzir a partir do item mostrado - Reproduzir de todas as músicas + Reproduzir de todas as músicas Preferir álbum Prefira o álbum se estiver tocando Recarregamento automático diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml index 8f2ff9021..0d4cd4b43 100644 --- a/app/src/main/res/values-pt-rPT/strings.xml +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -189,7 +189,7 @@ Estratégia do ganho de repetição Preferir álbum O pré-amplificador é aplicado ao ajuste existente durante a reprodução - Reproduzir de todas as músicas + Reproduzir de todas as músicas Pausa quando uma música se repete Limpe o estado de reprodução salvo anteriormente (se houver) Restaurar o estado de reprodução diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 77a21769a..3f2092ce1 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -23,7 +23,7 @@ Сейчас играет Играть Перемешать - Играть все треки + Играть все треки Играть альбом Играть исполнителя Очередь diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index deb2c84fb..bdb00ce4e 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -94,7 +94,7 @@ Etiket ile ayarla Uyarı: Ön amfinin yüksek bir pozitif değere değiştirilmesi bazı ses parçalarında pik yapmaya neden olabilir. Gösterilen öğeden çal - Tüm şarkılardan çal + Tüm şarkılardan çal Albümden çal Müzik klasörleri Müzik yalnızca eklediğiniz klasörlerden yüklenecektir. diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index c65e00a16..3f8555185 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -23,7 +23,7 @@ 正在播放 播放 随机 - 从全部歌曲开始播放 + 从全部歌曲开始播放 从专辑开始播放 从艺术家播放 播放队列 diff --git a/app/src/main/res/values/settings.xml b/app/src/main/res/values/settings.xml index a794fe77f..b96505ed8 100644 --- a/app/src/main/res/values/settings.xml +++ b/app/src/main/res/values/settings.xml @@ -99,22 +99,25 @@ - @string/set_playback_mode_all + @string/set_playback_mode_songs @string/set_playback_mode_artist @string/set_playback_mode_album + @string/set_playback_mode_genre @integer/music_mode_songs @integer/music_mode_artist @integer/music_mode_album + @integer/music_mode_genre @string/set_playback_mode_none - @string/set_playback_mode_all + @string/set_playback_mode_songs @string/set_playback_mode_artist @string/set_playback_mode_album + @string/set_playback_mode_genre @@ -122,6 +125,7 @@ @integer/music_mode_songs @integer/music_mode_artist @integer/music_mode_album + @integer/music_mode_genre @@ -141,6 +145,7 @@ 2 -2147483648 + 0xA108 0xA109 0xA10A 0xA10B diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6c72440c7..b2da4d456 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -202,9 +202,10 @@ When playing from the library When playing from item details Play from shown item - Play from all songs + Play from all songs Play from album Play from artist + Play from genre Remember shuffle Keep shuffle on when playing a new song Rewind before skipping back