playback: re-add play from genre

Re-add the "Play from genre" setting.

It's much easier to implement this now than prior with the picker
framework, so may as well retain consistency.
This commit is contained in:
Alexander Capehart 2022-12-27 16:24:52 -07:00
parent cce7b766d7
commit 195ea074ca
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
41 changed files with 306 additions and 131 deletions

View file

@ -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"
}
}

View file

@ -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 */

View file

@ -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<Artist>?) {
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 =

View file

@ -128,14 +128,14 @@ class AlbumDetailFragment : ListFragment<FragmentDetailBinding>(), 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<FragmentDetailBinding>(), 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<FragmentDetailBinding>(), AlbumDetailAd
findNavController().navigateUp()
return
}
requireBinding().detailToolbar.title = album.resolveName(requireContext())
}

View file

@ -124,16 +124,16 @@ class ArtistDetailFragment : ListFragment<FragmentDetailBinding>(), 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)

View file

@ -124,15 +124,16 @@ class GenreDetailFragment : ListFragment<FragmentDetailBinding>(), 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}")
}

View file

@ -52,7 +52,7 @@ class SongDetailDialog : ViewBindingDialogFragment<DialogSongDetailBinding>() {
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)
}

View file

@ -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()

View file

@ -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)
}
}

View file

@ -90,7 +90,7 @@ abstract class ListFragment<VB : ViewBinding> : SelectionFragment<VB>(), 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<VB : ViewBinding> : SelectionFragment<VB>(), 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")

View file

@ -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.
*/

View file

@ -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)
}

View file

@ -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.

View file

@ -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)
}
}

View file

@ -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<GenreChoiceViewHolder>() {
private var genres = listOf<Genre>()
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<Genre>) {
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))
}
}

View file

@ -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<DialogMusicPickerBinding>(), 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)
}
}

View file

@ -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<Song?>(null)
/**
* The current [Song] whose choices are being shown in the picker. Null if there is no [Song].
*/
val currentSong: StateFlow<Song?>
get() = _currentSong
private val _currentItem = MutableStateFlow<Music?>(null)
/** The current item whose artists should be shown in the picker. Null if there is no item. */
val currentItem: StateFlow<Music?> get() = _currentItem
private val _currentArtists = MutableStateFlow<List<Artist>?>(null)
/**
* The current [Artist] whose choices are being shown in the picker. Null/Empty if there is
* none.
*/
val currentArtists: StateFlow<List<Artist>?>
get() = _currentArtists
private val _artistChoices = MutableStateFlow<List<Artist>>(listOf())
/** The current [Artist] choices. Empty if no item is shown in the picker. */
val artistChoices: StateFlow<List<Artist>>
get() = _artistChoices
private val _genreChoices = MutableStateFlow<List<Genre>>(listOf())
/** The current [Genre] choices. Empty if no item is shown in the picker. */
val genreChoices: StateFlow<List<Genre>>
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<Music.UID>) {
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<Artist>(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 -> {}
}
}
}

View file

@ -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. */

View file

@ -70,13 +70,21 @@ class PlaybackViewModel(application: Application) :
private val _artistPlaybackPickerSong = MutableStateFlow<Song?>(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<Song?>
val artistPickerSong: StateFlow<Song?>
get() = _artistPlaybackPickerSong
private val _genrePlaybackPickerSong = MutableStateFlow<Song?>(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<Song?>
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
}
}
/**

View file

@ -137,11 +137,11 @@ class SearchFragment : ListFragment<FragmentSearchBinding>() {
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)
}

View file

@ -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
}

View file

@ -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<Music?>
get() = _exploreNavigationItem
private val _exploreNavigationArtists = MutableStateFlow<List<Artist>?>(null)
private val _exploreArtistNavigationItem = MutableStateFlow<Music?>(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<List<Artist>?>
get() = _exploreNavigationArtists
val exploreArtistNavigationItem: StateFlow<Music?>
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<Artist>) {
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<Artist>) {
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
}
}

View file

@ -23,6 +23,9 @@
<action
android:id="@+id/action_pick_navigation_artist"
app:destination="@id/artist_navigation_picker_dialog" />
<action
android:id="@+id/action_pick_playback_genre"
app:destination="@id/genre_playback_picker_dialog" />
</fragment>
<dialog
@ -31,7 +34,7 @@
android:label="artist_playback_picker_dialog"
tools:layout="@layout/dialog_music_picker">
<argument
android:name="songUid"
android:name="itemUid"
app:argType="org.oxycblt.auxio.music.Music$UID" />
</dialog>
@ -41,8 +44,18 @@
android:label="artist_navigation_picker_dialog"
tools:layout="@layout/dialog_music_picker">
<argument
android:name="artistUids"
app:argType="org.oxycblt.auxio.music.Music$UID[]" />
android:name="itemUid"
app:argType="org.oxycblt.auxio.music.Music$UID" />
</dialog>
<dialog
android:id="@+id/genre_playback_picker_dialog"
android:name="org.oxycblt.auxio.music.picker.GenrePlaybackPickerDialog"
android:label="genre_playback_picker_dialog"
tools:layout="@layout/dialog_music_picker">
<argument
android:name="itemUid"
app:argType="org.oxycblt.auxio.music.Music$UID" />
</dialog>
<dialog
@ -51,7 +64,7 @@
android:label="song_detail_dialog"
tools:layout="@layout/dialog_song_detail">
<argument
android:name="songUid"
android:name="itemUid"
app:argType="org.oxycblt.auxio.music.Music$UID" />
</dialog>

View file

@ -23,7 +23,7 @@
<string name="lbl_playback">يعمل الآن</string>
<string name="lbl_play">تشغيل</string>
<string name="lbl_shuffle">خلط</string>
<string name="set_playback_mode_all">تشغيل من جميع الاغاني</string>
<string name="set_playback_mode_songs">تشغيل من جميع الاغاني</string>
<string name="set_playback_mode_album">تشغيل من البوم</string>
<string name="set_playback_mode_artist">تشغيل من فنان</string>
<string name="lbl_queue">طابور</string>

View file

@ -29,7 +29,7 @@
<string name="lbl_playback">Právě hraje</string>
<string name="lbl_play">Přehrát</string>
<string name="lbl_shuffle">Náhodně</string>
<string name="set_playback_mode_all">Přehrát ze všech skladeb</string>
<string name="set_playback_mode_songs">Přehrát ze všech skladeb</string>
<string name="set_playback_mode_album">Přehrát z alba</string>
<string name="set_playback_mode_artist">Přehrát od umělce</string>
<string name="lbl_queue">Fronta</string>

View file

@ -15,7 +15,7 @@
<string name="lbl_sort_asc">Aufsteigend</string>
<string name="lbl_play">Abspielen</string>
<string name="lbl_shuffle">Zufällig</string>
<string name="set_playback_mode_all">Von allen Lieder abspielen</string>
<string name="set_playback_mode_songs">Von allen Lieder abspielen</string>
<string name="set_playback_mode_album">Von Album abspielen</string>
<string name="lbl_playback">Aktuelle Wiedergabe</string>
<string name="lbl_queue">Warteschlange</string>

View file

@ -23,7 +23,7 @@
<string name="lbl_playback">En reproducción</string>
<string name="lbl_play">Reproducir</string>
<string name="lbl_shuffle">Mezcla</string>
<string name="set_playback_mode_all">Reproducir todo</string>
<string name="set_playback_mode_songs">Reproducir todo</string>
<string name="set_playback_mode_album">Reproducir por álbum</string>
<string name="set_playback_mode_artist">Reproducir por artista</string>
<string name="lbl_queue">Cola</string>

View file

@ -85,7 +85,7 @@
<string name="set_pre_amp_warning">Upozorenje: Postavljanje pretpojačala na visoke razine može uzrokovati vrhunce tonova u nekim zvučnim zapisima.</string>
<string name="set_library_song_playback_mode">Kada se reproducira iz zbirke</string>
<string name="set_detail_song_playback_mode">Kada se reproducira iz detalja o predmetu</string>
<string name="set_playback_mode_all">Reproduciraj od svih pjesama</string>
<string name="set_playback_mode_songs">Reproduciraj od svih pjesama</string>
<string name="set_reindex">Aktualiziraj glazbu</string>
<string name="set_reindex_desc">Ponovo učitaj glazbenu biblioteku, koristeći predmemorirane oznake kada je to moguće</string>
<string name="set_dirs">Mape glazbe</string>

View file

@ -95,7 +95,7 @@
<string name="set_pre_amp_with">Penyesuaian dengan tag</string>
<string name="set_pre_amp_warning">Peringatan: Mengubah pre-amp ke nilai positif yang tinggi dapat mengakibatkan puncak pada beberapa trek audio.</string>
<string name="set_playback_mode_none">Putar dari item yang ditampilkan</string>
<string name="set_playback_mode_all">Putar dari semua lagu</string>
<string name="set_playback_mode_songs">Putar dari semua lagu</string>
<string name="set_keep_shuffle_desc">Tetap mengacak saat memutar lagu baru</string>
<string name="set_repeat_pause">Jeda pada pengulangan</string>
<string name="set_rewind_prev">Putar balik sebelum melompat ke belakang</string>

View file

@ -23,7 +23,7 @@
<string name="lbl_playback">Ora in riproduzione</string>
<string name="lbl_play">Riproduci</string>
<string name="lbl_shuffle">Mescola</string>
<string name="set_playback_mode_all">Riproduci da tutte le canzoni</string>
<string name="set_playback_mode_songs">Riproduci da tutte le canzoni</string>
<string name="set_playback_mode_album">Riproduci dal disco</string>
<string name="set_playback_mode_artist">Riproduci dall\'artista</string>
<string name="lbl_queue">Coda</string>

View file

@ -27,7 +27,7 @@
<string name="lbl_playback">지금 재생 중</string>
<string name="lbl_play">재생</string>
<string name="lbl_shuffle">셔플</string>
<string name="set_playback_mode_all">모든 곡에서 재생</string>
<string name="set_playback_mode_songs">모든 곡에서 재생</string>
<string name="set_playback_mode_album">앨범에서 재생</string>
<string name="set_playback_mode_artist">아티스트에서 재생</string>
<string name="lbl_queue">대기열</string>

View file

@ -176,7 +176,7 @@
<string name="set_dirs_mode_exclude_desc">Muzika <b>nebus</b> įkeliama iš pridėtų aplankų jūs pridėsite.</string>
<string name="set_dirs_mode_include">Įtraukti</string>
<string name="desc_clear_queue_item">Pašalinti šią eilės dainą</string>
<string name="set_playback_mode_all">Groti iš visų dainų</string>
<string name="set_playback_mode_songs">Groti iš visų dainų</string>
<string name="set_playback_mode_none">Groti iš parodyto elemento</string>
<string name="set_playback_mode_album">Groti iš albumo</string>
<string name="set_playback_mode_artist">Groti iš atlikėjo</string>

View file

@ -17,7 +17,7 @@
<string name="lbl_sort_asc">Oplopend</string>
<string name="lbl_play">Afspelen</string>
<string name="lbl_shuffle">Shuffle</string>
<string name="set_playback_mode_all">Speel van alle nummers </string>
<string name="set_playback_mode_songs">Speel van alle nummers </string>
<string name="set_playback_mode_album">Speel af van album </string>
<string name="set_playback_mode_artist">Speel van artiest </string>
<string name="lbl_playback">Afspeelscherm</string>

View file

@ -136,7 +136,7 @@
<string name="lbl_equalizer">Equalizer</string>
<string name="lbl_size">Rozmiar</string>
<string name="err_no_dirs">Brak folderów</string>
<string name="set_playback_mode_all">Odtwórz wszystkie utwory</string>
<string name="set_playback_mode_songs">Odtwórz wszystkie utwory</string>
<string name="set_playback_mode_album">Odtwórz album</string>
<string name="set_headset_autoplay_desc">Zacznij odtawrzanie po podłączeniu słuchawek (może nie działać na wszystkich urządzeniach)</string>
<string name="set_reindex">Załaduj ponownie bibliotekę</string>

View file

@ -166,7 +166,7 @@
<string name="set_round_mode_desc">Ativar cantos arredondados em elementos adicionais da interface do usuário (requer que as capas dos álbuns sejam arredondadas)</string>
<string name="set_replay_gain">Modo de normalização de volume (ReplayGain)</string>
<string name="set_playback_mode_none">Reproduzir a partir do item mostrado</string>
<string name="set_playback_mode_all">Reproduzir de todas as músicas</string>
<string name="set_playback_mode_songs">Reproduzir de todas as músicas</string>
<string name="set_replay_gain_album">Preferir álbum</string>
<string name="set_replay_gain_dynamic">Prefira o álbum se estiver tocando</string>
<string name="set_observing">Recarregamento automático</string>

View file

@ -189,7 +189,7 @@
<string name="set_replay_gain">Estratégia do ganho de repetição</string>
<string name="set_replay_gain_album">Preferir álbum</string>
<string name="set_pre_amp_desc">O pré-amplificador é aplicado ao ajuste existente durante a reprodução</string>
<string name="set_playback_mode_all">Reproduzir de todas as músicas</string>
<string name="set_playback_mode_songs">Reproduzir de todas as músicas</string>
<string name="set_repeat_pause_desc">Pausa quando uma música se repete</string>
<string name="set_wipe_desc">Limpe o estado de reprodução salvo anteriormente (se houver)</string>
<string name="set_restore_state">Restaurar o estado de reprodução</string>

View file

@ -23,7 +23,7 @@
<string name="lbl_playback">Сейчас играет</string>
<string name="lbl_play">Играть</string>
<string name="lbl_shuffle">Перемешать</string>
<string name="set_playback_mode_all">Играть все треки</string>
<string name="set_playback_mode_songs">Играть все треки</string>
<string name="set_playback_mode_album">Играть альбом</string>
<string name="set_playback_mode_artist">Играть исполнителя</string>
<string name="lbl_queue">Очередь</string>

View file

@ -94,7 +94,7 @@
<string name="set_pre_amp_with">Etiket ile ayarla</string>
<string name="set_pre_amp_warning">Uyarı: Ön amfinin yüksek bir pozitif değere değiştirilmesi bazı ses parçalarında pik yapmaya neden olabilir.</string>
<string name="set_playback_mode_none">Gösterilen öğeden çal</string>
<string name="set_playback_mode_all">Tüm şarkılardan çal</string>
<string name="set_playback_mode_songs">Tüm şarkılardan çal</string>
<string name="set_playback_mode_album">Albümden çal</string>
<string name="set_dirs">Müzik klasörleri</string>
<string name="set_dirs_mode_include_desc">Müzik <b>yalnızca</b> eklediğiniz klasörlerden yüklenecektir.</string>

View file

@ -23,7 +23,7 @@
<string name="lbl_playback">正在播放</string>
<string name="lbl_play">播放</string>
<string name="lbl_shuffle">随机</string>
<string name="set_playback_mode_all">从全部歌曲开始播放</string>
<string name="set_playback_mode_songs">从全部歌曲开始播放</string>
<string name="set_playback_mode_album">从专辑开始播放</string>
<string name="set_playback_mode_artist">从艺术家播放</string>
<string name="lbl_queue">播放队列</string>

View file

@ -99,22 +99,25 @@
</integer-array>
<string-array name="entries_library_song_playback_mode">
<item>@string/set_playback_mode_all</item>
<item>@string/set_playback_mode_songs</item>
<item>@string/set_playback_mode_artist</item>
<item>@string/set_playback_mode_album</item>
<item>@string/set_playback_mode_genre</item>
</string-array>
<integer-array name="values_library_song_playback_mode">
<item>@integer/music_mode_songs</item>
<item>@integer/music_mode_artist</item>
<item>@integer/music_mode_album</item>
<item>@integer/music_mode_genre</item>
</integer-array>
<string-array name="entries_detail_song_playback_mode">
<item>@string/set_playback_mode_none</item>
<item>@string/set_playback_mode_all</item>
<item>@string/set_playback_mode_songs</item>
<item>@string/set_playback_mode_artist</item>
<item>@string/set_playback_mode_album</item>
<item>@string/set_playback_mode_genre</item>
</string-array>
<integer-array name="values_detail_song_playback_mode">
@ -122,6 +125,7 @@
<item>@integer/music_mode_songs</item>
<item>@integer/music_mode_artist</item>
<item>@integer/music_mode_album</item>
<item>@integer/music_mode_genre</item>
</integer-array>
<string-array name="entries_replay_gain">
@ -141,6 +145,7 @@
<integer name="theme_dark">2</integer>
<integer name="music_mode_none">-2147483648</integer>
<integer name="music_mode_genre">0xA108</integer>
<integer name="music_mode_artist">0xA109</integer>
<integer name="music_mode_album">0xA10A</integer>
<integer name="music_mode_songs">0xA10B</integer>

View file

@ -202,9 +202,10 @@
<string name="set_library_song_playback_mode">When playing from the library</string>
<string name="set_detail_song_playback_mode">When playing from item details</string>
<string name="set_playback_mode_none">Play from shown item</string>
<string name="set_playback_mode_all">Play from all songs</string>
<string name="set_playback_mode_songs">Play from all songs</string>
<string name="set_playback_mode_album">Play from album</string>
<string name="set_playback_mode_artist">Play from artist</string>
<string name="set_playback_mode_genre">Play from genre</string>
<string name="set_keep_shuffle">Remember shuffle</string>
<string name="set_keep_shuffle_desc">Keep shuffle on when playing a new song</string>
<string name="set_rewind_prev">Rewind before skipping back</string>