picker: refactor into module-specific impls

Refactor the weird picker god module into specific sub-impls in
playback and a new navigation package.

I cannot keep this unified. The needs are too different among each
picker. Better to keep it separate, especially in preparation for
the playlist dialogs.
This commit is contained in:
Alexander Capehart 2023-03-25 21:01:21 -06:00
parent 5de1e221ac
commit f2a90bf0af
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
33 changed files with 472 additions and 400 deletions

View file

@ -2,6 +2,9 @@
## dev ## dev
## What's Fixed
- Fixed inconsistent corner radius on widget cover art
## What's Improved ## What's Improved
- Added ability to click on the playback bar to exit the queue view - Added ability to click on the playback bar to exit the queue view

View file

@ -48,6 +48,9 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat
* TODO: Use proper material attributes (Not the weird dimen attributes I currently have) * TODO: Use proper material attributes (Not the weird dimen attributes I currently have)
* TODO: Migrate to material animation system * TODO: Migrate to material animation system
* TODO: Unit testing * TODO: Unit testing
* TODO: Use sealed interface where applicable
* TODO: Fix UID naming
* TODO: Leverage FlexibleListAdapter more in dialogs (Disable item anims)
*/ */
@AndroidEntryPoint @AndroidEntryPoint
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {

View file

@ -41,11 +41,11 @@ import org.oxycblt.auxio.databinding.FragmentMainBinding
import org.oxycblt.auxio.list.selection.SelectionViewModel import org.oxycblt.auxio.list.selection.SelectionViewModel
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.navigation.MainNavigationAction
import org.oxycblt.auxio.navigation.NavigationViewModel
import org.oxycblt.auxio.playback.PlaybackBottomSheetBehavior import org.oxycblt.auxio.playback.PlaybackBottomSheetBehavior
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.playback.queue.QueueBottomSheetBehavior import org.oxycblt.auxio.playback.queue.QueueBottomSheetBehavior
import org.oxycblt.auxio.ui.MainNavigationAction
import org.oxycblt.auxio.ui.NavigationViewModel
import org.oxycblt.auxio.ui.ViewBindingFragment import org.oxycblt.auxio.ui.ViewBindingFragment
import org.oxycblt.auxio.util.* import org.oxycblt.auxio.util.*

View file

@ -44,8 +44,8 @@ import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicMode import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.navigation.NavigationViewModel
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.NavigationViewModel
import org.oxycblt.auxio.util.* import org.oxycblt.auxio.util.*
/** /**

View file

@ -43,8 +43,8 @@ import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.navigation.NavigationViewModel
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.NavigationViewModel
import org.oxycblt.auxio.util.* import org.oxycblt.auxio.util.*
/** /**

View file

@ -39,8 +39,8 @@ import org.oxycblt.auxio.list.ListFragment
import org.oxycblt.auxio.list.Sort import org.oxycblt.auxio.list.Sort
import org.oxycblt.auxio.list.selection.SelectionViewModel import org.oxycblt.auxio.list.selection.SelectionViewModel
import org.oxycblt.auxio.music.* import org.oxycblt.auxio.music.*
import org.oxycblt.auxio.navigation.NavigationViewModel
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.NavigationViewModel
import org.oxycblt.auxio.util.* import org.oxycblt.auxio.util.*
/** /**

View file

@ -39,8 +39,8 @@ import org.oxycblt.auxio.list.ListFragment
import org.oxycblt.auxio.list.Sort import org.oxycblt.auxio.list.Sort
import org.oxycblt.auxio.list.selection.SelectionViewModel import org.oxycblt.auxio.list.selection.SelectionViewModel
import org.oxycblt.auxio.music.* import org.oxycblt.auxio.music.*
import org.oxycblt.auxio.navigation.NavigationViewModel
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.NavigationViewModel
import org.oxycblt.auxio.util.* import org.oxycblt.auxio.util.*
/** /**

View file

@ -66,7 +66,7 @@ class SongDetailDialog : ViewBindingDialogFragment<DialogSongDetailBinding>() {
super.onBindingCreated(binding, savedInstanceState) super.onBindingCreated(binding, savedInstanceState)
binding.detailProperties.adapter = detailAdapter binding.detailProperties.adapter = detailAdapter
// DetailViewModel handles most initialization from the navigation argument. // DetailViewModel handles most initialization from the navigation argument.
detailModel.setSongUid(args.itemUid) detailModel.setSongUid(args.songUid)
collectImmediately(detailModel.currentSong, detailModel.songAudioInfo, ::updateSong) collectImmediately(detailModel.currentSong, detailModel.songAudioInfo, ::updateSong)
} }

View file

@ -52,9 +52,9 @@ import org.oxycblt.auxio.list.Sort
import org.oxycblt.auxio.list.selection.SelectionFragment import org.oxycblt.auxio.list.selection.SelectionFragment
import org.oxycblt.auxio.list.selection.SelectionViewModel import org.oxycblt.auxio.list.selection.SelectionViewModel
import org.oxycblt.auxio.music.* import org.oxycblt.auxio.music.*
import org.oxycblt.auxio.navigation.MainNavigationAction
import org.oxycblt.auxio.navigation.NavigationViewModel
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.MainNavigationAction
import org.oxycblt.auxio.ui.NavigationViewModel
import org.oxycblt.auxio.util.* import org.oxycblt.auxio.util.*
/** /**

View file

@ -37,10 +37,10 @@ import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter
import org.oxycblt.auxio.list.recycler.AlbumViewHolder import org.oxycblt.auxio.list.recycler.AlbumViewHolder
import org.oxycblt.auxio.list.selection.SelectionViewModel import org.oxycblt.auxio.list.selection.SelectionViewModel
import org.oxycblt.auxio.music.* import org.oxycblt.auxio.music.*
import org.oxycblt.auxio.navigation.NavigationViewModel
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.playback.formatDurationMs import org.oxycblt.auxio.playback.formatDurationMs
import org.oxycblt.auxio.playback.secsToMs import org.oxycblt.auxio.playback.secsToMs
import org.oxycblt.auxio.ui.NavigationViewModel
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately
/** /**

View file

@ -38,9 +38,9 @@ import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicMode import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.navigation.NavigationViewModel
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.playback.formatDurationMs import org.oxycblt.auxio.playback.formatDurationMs
import org.oxycblt.auxio.ui.NavigationViewModel
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.nonZeroOrNull import org.oxycblt.auxio.util.nonZeroOrNull

View file

@ -38,9 +38,9 @@ import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicMode import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.navigation.NavigationViewModel
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.playback.formatDurationMs import org.oxycblt.auxio.playback.formatDurationMs
import org.oxycblt.auxio.ui.NavigationViewModel
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD

View file

@ -37,9 +37,9 @@ import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicMode import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.Playlist
import org.oxycblt.auxio.navigation.NavigationViewModel
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.playback.formatDurationMs import org.oxycblt.auxio.playback.formatDurationMs
import org.oxycblt.auxio.ui.NavigationViewModel
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD

View file

@ -40,10 +40,10 @@ import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicMode import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.navigation.NavigationViewModel
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.playback.formatDurationMs import org.oxycblt.auxio.playback.formatDurationMs
import org.oxycblt.auxio.playback.secsToMs import org.oxycblt.auxio.playback.secsToMs
import org.oxycblt.auxio.ui.NavigationViewModel
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately
/** /**

View file

@ -29,8 +29,8 @@ import org.oxycblt.auxio.MainFragmentDirections
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.list.selection.SelectionFragment import org.oxycblt.auxio.list.selection.SelectionFragment
import org.oxycblt.auxio.music.* import org.oxycblt.auxio.music.*
import org.oxycblt.auxio.ui.MainNavigationAction import org.oxycblt.auxio.navigation.MainNavigationAction
import org.oxycblt.auxio.ui.NavigationViewModel import org.oxycblt.auxio.navigation.NavigationViewModel
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.showToast import org.oxycblt.auxio.util.showToast

View file

@ -0,0 +1,44 @@
/*
* Copyright (c) 2023 Auxio Project
* MainNavigationAction.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.navigation
import androidx.navigation.NavDirections
/**
* Represents the possible actions within the main navigation graph. This can be used with
* [NavigationViewModel] to initiate navigation in the main navigation graph from anywhere in the
* app, including outside the main navigation graph.
*
* @author Alexander Capehart (OxygenCobalt)
*/
sealed class MainNavigationAction {
/** Expand the playback panel. */
object OpenPlaybackPanel : MainNavigationAction()
/** Collapse the playback bottom sheet. */
object ClosePlaybackPanel : MainNavigationAction()
/**
* Navigate to the given [NavDirections].
*
* @param directions The [NavDirections] to navigate to. Assumed to be part of the main
* navigation graph.
*/
data class Directions(val directions: NavDirections) : MainNavigationAction()
}

View file

@ -16,10 +16,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.ui package org.oxycblt.auxio.navigation
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.navigation.NavDirections
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
@ -28,7 +27,13 @@ import org.oxycblt.auxio.util.Event
import org.oxycblt.auxio.util.MutableEvent import org.oxycblt.auxio.util.MutableEvent
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
/** A [ViewModel] that handles complicated navigation functionality. */ /**
* A [ViewModel] that handles complicated navigation functionality.
*
* @author Alexander Capehart (OxygenCobalt)
*
* TODO: This whole system is very jankily designed, perhaps it's time for a refactor?
*/
class NavigationViewModel : ViewModel() { class NavigationViewModel : ViewModel() {
private val _mainNavigationAction = MutableEvent<MainNavigationAction>() private val _mainNavigationAction = MutableEvent<MainNavigationAction>()
/** /**
@ -118,26 +123,3 @@ class NavigationViewModel : ViewModel() {
} }
} }
} }
/**
* Represents the possible actions within the main navigation graph. This can be used with
* [NavigationViewModel] to initiate navigation in the main navigation graph from anywhere in the
* app, including outside the main navigation graph.
*
* @author Alexander Capehart (OxygenCobalt)
*/
sealed class MainNavigationAction {
/** Expand the playback panel. */
object OpenPlaybackPanel : MainNavigationAction()
/** Collapse the playback bottom sheet. */
object ClosePlaybackPanel : MainNavigationAction()
/**
* Navigate to the given [NavDirections].
*
* @param directions The [NavDirections] to navigate to. Assumed to be part of the main
* navigation graph.
*/
data class Directions(val directions: NavDirections) : MainNavigationAction()
}

View file

@ -16,32 +16,41 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.picker package org.oxycblt.auxio.navigation.picker
import android.os.Bundle
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.oxycblt.auxio.databinding.DialogMusicPickerBinding import kotlinx.coroutines.flow.StateFlow
import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.ui.NavigationViewModel import org.oxycblt.auxio.navigation.NavigationViewModel
import org.oxycblt.auxio.picker.PickerChoices
import org.oxycblt.auxio.picker.PickerDialogFragment
/** /**
* An [ArtistPickerDialog] intended for when [Artist] navigation is ambiguous. * A [PickerDialogFragment] intended for when [Artist] navigation is ambiguous.
* *
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
@AndroidEntryPoint @AndroidEntryPoint
class ArtistNavigationPickerDialog : ArtistPickerDialog() { class ArtistNavigationPickerDialog : PickerDialogFragment<Artist>() {
private val navModel: NavigationViewModel by activityViewModels() 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 // Information about what Song to show choices for is initially within the navigation arguments
// as UIDs, as that is the only safe way to parcel a Song. // as UIDs, as that is the only safe way to parcel a Song.
private val args: ArtistNavigationPickerDialogArgs by navArgs() private val args: ArtistNavigationPickerDialogArgs by navArgs()
override fun onBindingCreated(binding: DialogMusicPickerBinding, savedInstanceState: Bundle?) { override val titleRes: Int
pickerModel.setItemUid(args.itemUid) get() = R.string.lbl_artists
super.onBindingCreated(binding, savedInstanceState)
override val pickerChoices: StateFlow<PickerChoices<Artist>?>
get() = pickerModel.currentArtistChoices
override fun initChoices() {
pickerModel.setArtistChoiceUid(args.artistUid)
} }
override fun onClick(item: Artist, viewHolder: RecyclerView.ViewHolder) { override fun onClick(item: Artist, viewHolder: RecyclerView.ViewHolder) {

View file

@ -0,0 +1,93 @@
/*
* Copyright (c) 2023 Auxio Project
* NavigationPickerViewModel.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.navigation.picker
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import org.oxycblt.auxio.music.*
import org.oxycblt.auxio.picker.PickerChoices
/**
* A [ViewModel] that stores the current information required for [ArtistNavigationPickerDialog].
*
* @author Alexander Capehart (OxygenCobalt)
*/
@HiltViewModel
class NavigationPickerViewModel @Inject constructor(private val musicRepository: MusicRepository) :
ViewModel(), MusicRepository.UpdateListener {
private val _currentArtistChoices = MutableStateFlow<ArtistNavigationChoices?>(null)
/** The current set of [Artist] choices to show in the picker, or null if to show nothing. */
val currentArtistChoices: StateFlow<PickerChoices<Artist>?>
get() = _currentArtistChoices
init {
musicRepository.addUpdateListener(this)
}
override fun onMusicChanges(changes: MusicRepository.Changes) {
if (!changes.deviceLibrary) return
val deviceLibrary = musicRepository.deviceLibrary ?: return
// Need to sanitize different items depending on the current set of choices.
_currentArtistChoices.value =
when (val choices = _currentArtistChoices.value) {
is ArtistNavigationChoices.FromSong ->
deviceLibrary.findSong(choices.song.uid)?.let {
ArtistNavigationChoices.FromSong(it)
}
is ArtistNavigationChoices.FromAlbum ->
deviceLibrary.findAlbum(choices.album.uid)?.let {
ArtistNavigationChoices.FromAlbum(it)
}
else -> null
}
}
override fun onCleared() {
super.onCleared()
musicRepository.removeUpdateListener(this)
}
/**
* Set the [Music.UID] of the item to show artist choices for.
*
* @param uid The [Music.UID] of the item to show. Must be a [Song] or [Album].
*/
fun setArtistChoiceUid(uid: Music.UID) {
// 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)
else -> null
}
}
private sealed interface ArtistNavigationChoices : PickerChoices<Artist> {
data class FromSong(val song: Song) : ArtistNavigationChoices {
override val choices = song.artists
}
data class FromAlbum(val album: Album) : ArtistNavigationChoices {
override val choices = album.artists
}
}
}

View file

@ -1,90 +0,0 @@
/*
* Copyright (c) 2022 Auxio Project
* ArtistChoiceAdapter.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.picker
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.databinding.ItemPickerChoiceBinding
import org.oxycblt.auxio.list.ClickableListListener
import org.oxycblt.auxio.list.recycler.DialogRecyclerView
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.inflater
/**
* An [RecyclerView.Adapter] that displays a list of [Artist] choices.
*
* @param listener A [ClickableListListener] to bind interactions to.
* @author OxygenCobalt.
*/
class ArtistChoiceAdapter(private val listener: ClickableListListener<Artist>) :
RecyclerView.Adapter<ArtistChoiceViewHolder>() {
private var artists = listOf<Artist>()
override fun getItemCount() = artists.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
ArtistChoiceViewHolder.from(parent)
override fun onBindViewHolder(holder: ArtistChoiceViewHolder, position: Int) =
holder.bind(artists[position], listener)
/**
* Immediately update the [Artist] choices.
*
* @param newArtists The new [Artist]s to show.
*/
fun submitList(newArtists: List<Artist>) {
if (newArtists != artists) {
artists = newArtists
@Suppress("NotifyDataSetChanged") notifyDataSetChanged()
}
}
}
/**
* A [DialogRecyclerView.ViewHolder] that displays a smaller variant of a typical [Artist] item, for
* use with [ArtistChoiceAdapter]. Use [from] to create an instance.
*/
class ArtistChoiceViewHolder(private val binding: ItemPickerChoiceBinding) :
DialogRecyclerView.ViewHolder(binding.root) {
/**
* Bind new data to this instance.
*
* @param artist The new [Artist] to bind.
* @param listener A [ClickableListListener] to bind interactions to.
*/
fun bind(artist: Artist, listener: ClickableListListener<Artist>) {
listener.bind(artist, this)
binding.pickerImage.bind(artist)
binding.pickerName.text = artist.resolveName(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) =
ArtistChoiceViewHolder(ItemPickerChoiceBinding.inflate(parent.context.inflater))
}
}

View file

@ -0,0 +1,87 @@
/*
* Copyright (c) 2023 Auxio Project
* ChoiceAdapter.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.picker
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.databinding.ItemPickerChoiceBinding
import org.oxycblt.auxio.list.ClickableListListener
import org.oxycblt.auxio.list.adapter.FlexibleListAdapter
import org.oxycblt.auxio.list.adapter.SimpleDiffCallback
import org.oxycblt.auxio.list.recycler.DialogRecyclerView
import org.oxycblt.auxio.music.*
import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.inflater
/** A [RecyclerView.Adapter] that shows a list */
class ChoiceAdapter<T : Music>(private val listener: ClickableListListener<T>) :
FlexibleListAdapter<T, ChoiceViewHolder<T>>(ChoiceViewHolder.diffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
ChoiceViewHolder.from<T>(parent)
override fun onBindViewHolder(holder: ChoiceViewHolder<T>, position: Int) =
holder.bind(getItem(position), listener)
}
/**
* A [DialogRecyclerView.ViewHolder] that displays a smaller variant of a typical [T] item, for use
* with [ChoiceAdapter]. Use [from] to create an instance.
*/
class ChoiceViewHolder<T : Music>
private constructor(private val binding: ItemPickerChoiceBinding) :
DialogRecyclerView.ViewHolder(binding.root) {
/**
* Bind new data to this instance.
*
* @param music The new [T] to bind.
* @param listener A [ClickableListListener] to bind interactions to.
*/
fun bind(music: T, listener: ClickableListListener<T>) {
listener.bind(music, this)
// ImageGroup is not generic, so we must downcast to specific types for now.
when (music) {
is Song -> binding.pickerImage.bind(music)
is Album -> binding.pickerImage.bind(music)
is Artist -> binding.pickerImage.bind(music)
is Genre -> binding.pickerImage.bind(music)
is Playlist -> binding.pickerImage.bind(music)
}
binding.pickerName.text = music.resolveName(binding.context)
}
companion object {
/**
* Create a new instance.
*
* @param parent The parent to inflate this instance from.
* @return A new instance.
*/
fun <T : Music> from(parent: View) =
ChoiceViewHolder<T>(ItemPickerChoiceBinding.inflate(parent.context.inflater))
/** Get a comparator that can be used with DiffUtil. */
fun <T : Music> diffCallback() =
object : SimpleDiffCallback<T>() {
override fun areContentsTheSame(oldItem: T, newItem: T) =
oldItem.rawName == newItem.rawName
}
}
}

View file

@ -1,90 +0,0 @@
/*
* Copyright (c) 2022 Auxio Project
* GenreChoiceAdapter.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.picker
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.databinding.ItemPickerChoiceBinding
import org.oxycblt.auxio.list.ClickableListListener
import org.oxycblt.auxio.list.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<Genre>) :
RecyclerView.Adapter<GenreChoiceViewHolder>() {
private var genres = listOf<Genre>()
override fun getItemCount() = genres.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
GenreChoiceViewHolder.from(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 [from] 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<Genre>) {
listener.bind(genre, this)
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 from(parent: View) =
GenreChoiceViewHolder(ItemPickerChoiceBinding.inflate(parent.context.inflater))
}
}

View file

@ -0,0 +1,31 @@
/*
* Copyright (c) 2023 Auxio Project
* PickerChoices.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.picker
import org.oxycblt.auxio.music.Music
/**
* Represents a list of [Music] to show in a picker UI.
*
* @author Alexander Capehart (OxygenCobalt)
*/
interface PickerChoices<T : Music> {
/** The list of choices to show. */
val choices: List<T>
}

View file

@ -1,6 +1,6 @@
/* /*
* Copyright (c) 2022 Auxio Project * Copyright (c) 2023 Auxio Project
* ArtistPickerDialog.kt is part of Auxio. * PickerDialogFragment.kt is part of Auxio.
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -21,45 +21,53 @@ package org.oxycblt.auxio.picker
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.StateFlow
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogMusicPickerBinding import org.oxycblt.auxio.databinding.DialogMusicPickerBinding
import org.oxycblt.auxio.list.ClickableListListener import org.oxycblt.auxio.list.ClickableListListener
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.list.adapter.UpdateInstructions
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.ui.ViewBindingDialogFragment import org.oxycblt.auxio.ui.ViewBindingDialogFragment
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately
/** /**
* The base class for dialogs that implements common behavior across all [Artist] pickers. These are * A [ViewBindingDialogFragment] that acts as the base for a "picker" UI, shown when a given choice
* shown whenever what to do with an item's [Artist] is ambiguous, as there are multiple [Artist]'s * is ambiguous.
* to choose from.
* *
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
@AndroidEntryPoint abstract class PickerDialogFragment<T : Music> :
abstract class ArtistPickerDialog : ViewBindingDialogFragment<DialogMusicPickerBinding>(), ClickableListListener<T> {
ViewBindingDialogFragment<DialogMusicPickerBinding>(), ClickableListListener<Artist> {
protected val pickerModel: PickerViewModel by viewModels()
// Okay to leak this since the Listener will not be called until after initialization. // Okay to leak this since the Listener will not be called until after initialization.
private val artistAdapter = ArtistChoiceAdapter(@Suppress("LeakingThis") this) private val choiceAdapter = ChoiceAdapter(@Suppress("LeakingThis") this)
/** The string resource to use in the dialog title. */
abstract val titleRes: Int
/** The [StateFlow] of choices to show in the picker. */
abstract val pickerChoices: StateFlow<PickerChoices<T>?>
/** Called when the choice list should be initialized from the stored arguments. */
abstract fun initChoices()
override fun onCreateBinding(inflater: LayoutInflater) = override fun onCreateBinding(inflater: LayoutInflater) =
DialogMusicPickerBinding.inflate(inflater) DialogMusicPickerBinding.inflate(inflater)
override fun onConfigDialog(builder: AlertDialog.Builder) { override fun onConfigDialog(builder: AlertDialog.Builder) {
builder.setTitle(R.string.lbl_artists).setNegativeButton(R.string.lbl_cancel, null) builder.setTitle(titleRes).setNegativeButton(R.string.lbl_cancel, null)
} }
override fun onBindingCreated(binding: DialogMusicPickerBinding, savedInstanceState: Bundle?) { override fun onBindingCreated(binding: DialogMusicPickerBinding, savedInstanceState: Bundle?) {
binding.pickerRecycler.adapter = artistAdapter binding.pickerRecycler.apply {
itemAnimator = null
adapter = choiceAdapter
}
collectImmediately(pickerModel.artistChoices) { artists -> initChoices()
if (artists.isNotEmpty()) { collectImmediately(pickerChoices) { item ->
// Make sure the artist choices align with any changes in the music library. if (item != null) {
artistAdapter.submitList(artists) // Make sure the choices align with any changes in the music library.
choiceAdapter.update(item.choices, UpdateInstructions.Diff)
} else { } else {
// Not showing any choices, navigate up. // Not showing any choices, navigate up.
findNavController().navigateUp() findNavController().navigateUp()
@ -71,7 +79,7 @@ abstract class ArtistPickerDialog :
binding.pickerRecycler.adapter = null binding.pickerRecycler.adapter = null
} }
override fun onClick(item: Artist, viewHolder: RecyclerView.ViewHolder) { override fun onClick(item: T, viewHolder: RecyclerView.ViewHolder) {
findNavController().navigateUp() findNavController().navigateUp()
} }
} }

View file

@ -1,87 +0,0 @@
/*
* Copyright (c) 2022 Auxio Project
* PickerViewModel.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.picker
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import org.oxycblt.auxio.music.*
/**
* a [ViewModel] that manages the current music picker state. Make it so that the dialogs just
* contain the music themselves and then exit if the library changes.
*
* @author Alexander Capehart (OxygenCobalt)
*/
@HiltViewModel
class PickerViewModel @Inject constructor(private val musicRepository: MusicRepository) :
ViewModel(), MusicRepository.UpdateListener {
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 _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
init {
musicRepository.addUpdateListener(this)
}
override fun onCleared() {
musicRepository.removeUpdateListener(this)
}
override fun onMusicChanges(changes: MusicRepository.Changes) {
if (changes.deviceLibrary && musicRepository.deviceLibrary != null) {
refreshChoices()
}
}
/**
* Set a new [currentItem] from it's [Music.UID].
*
* @param uid The [Music.UID] of the [Song] to update to.
*/
fun setItemUid(uid: Music.UID) {
_currentItem.value = musicRepository.find(uid)
refreshChoices()
}
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

@ -26,9 +26,9 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentPlaybackBarBinding import org.oxycblt.auxio.databinding.FragmentPlaybackBarBinding
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.resolveNames import org.oxycblt.auxio.music.resolveNames
import org.oxycblt.auxio.navigation.MainNavigationAction
import org.oxycblt.auxio.navigation.NavigationViewModel
import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.ui.MainNavigationAction
import org.oxycblt.auxio.ui.NavigationViewModel
import org.oxycblt.auxio.ui.ViewBindingFragment import org.oxycblt.auxio.ui.ViewBindingFragment
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.getAttrColorCompat import org.oxycblt.auxio.util.getAttrColorCompat

View file

@ -36,10 +36,10 @@ import org.oxycblt.auxio.databinding.FragmentPlaybackPanelBinding
import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.resolveNames import org.oxycblt.auxio.music.resolveNames
import org.oxycblt.auxio.navigation.MainNavigationAction
import org.oxycblt.auxio.navigation.NavigationViewModel
import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.playback.ui.StyledSeekBar import org.oxycblt.auxio.playback.ui.StyledSeekBar
import org.oxycblt.auxio.ui.MainNavigationAction
import org.oxycblt.auxio.ui.NavigationViewModel
import org.oxycblt.auxio.ui.ViewBindingFragment import org.oxycblt.auxio.ui.ViewBindingFragment
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.showToast import org.oxycblt.auxio.util.showToast

View file

@ -16,18 +16,19 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.picker package org.oxycblt.auxio.playback.picker
import android.os.Bundle
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.oxycblt.auxio.databinding.DialogMusicPickerBinding import kotlinx.coroutines.flow.StateFlow
import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.picker.PickerChoices
import org.oxycblt.auxio.picker.PickerDialogFragment
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.util.requireIs
import org.oxycblt.auxio.util.unlikelyToBeNull import org.oxycblt.auxio.util.unlikelyToBeNull
/** /**
@ -36,21 +37,27 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
@AndroidEntryPoint @AndroidEntryPoint
class ArtistPlaybackPickerDialog : ArtistPickerDialog() { class ArtistPlaybackPickerDialog : PickerDialogFragment<Artist>() {
private val playbackModel: PlaybackViewModel by activityViewModels() 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 // Information about what Song to show choices for is initially within the navigation arguments
// as UIDs, as that is the only safe way to parcel a Song. // as UIDs, as that is the only safe way to parcel a Song.
private val args: ArtistPlaybackPickerDialogArgs by navArgs() private val args: ArtistPlaybackPickerDialogArgs by navArgs()
override fun onBindingCreated(binding: DialogMusicPickerBinding, savedInstanceState: Bundle?) { override val titleRes: Int
pickerModel.setItemUid(args.itemUid) get() = R.string.lbl_artists
super.onBindingCreated(binding, savedInstanceState)
override val pickerChoices: StateFlow<PickerChoices<Artist>?>
get() = pickerModel.currentArtistChoices
override fun initChoices() {
pickerModel.setArtistChoiceUid(args.artistUid)
} }
override fun onClick(item: Artist, viewHolder: RecyclerView.ViewHolder) { override fun onClick(item: Artist, viewHolder: RecyclerView.ViewHolder) {
super.onClick(item, viewHolder) super.onClick(item, viewHolder)
// User made a choice, play the given song from that artist. // User made a choice, play the given song from that artist.
val song = requireIs<Song>(unlikelyToBeNull(pickerModel.currentItem.value)) val song = unlikelyToBeNull(pickerModel.currentArtistChoices.value).song
playbackModel.playFromArtist(song, item) playbackModel.playFromArtist(song, item)
} }
} }

View file

@ -16,26 +16,20 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.picker package org.oxycblt.auxio.playback.picker
import android.os.Bundle
import android.view.LayoutInflater
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.StateFlow
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogMusicPickerBinding
import org.oxycblt.auxio.list.ClickableListListener
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.picker.PickerChoices
import org.oxycblt.auxio.picker.PickerDialogFragment
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.ViewBindingDialogFragment import org.oxycblt.auxio.ui.ViewBindingDialogFragment
import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.requireIs
import org.oxycblt.auxio.util.unlikelyToBeNull import org.oxycblt.auxio.util.unlikelyToBeNull
/** /**
@ -44,45 +38,27 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
@AndroidEntryPoint @AndroidEntryPoint
class GenrePlaybackPickerDialog : class GenrePlaybackPickerDialog : PickerDialogFragment<Genre>() {
ViewBindingDialogFragment<DialogMusicPickerBinding>(), ClickableListListener<Genre> {
private val pickerModel: PickerViewModel by viewModels()
private val playbackModel: PlaybackViewModel by activityViewModels() 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 // Information about what Song to show choices for is initially within the navigation arguments
// as UIDs, as that is the only safe way to parcel a Song. // as UIDs, as that is the only safe way to parcel a Song.
private val args: GenrePlaybackPickerDialogArgs by navArgs() 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) = override val titleRes: Int
DialogMusicPickerBinding.inflate(inflater) get() = R.string.lbl_genres
override fun onConfigDialog(builder: AlertDialog.Builder) { override val pickerChoices: StateFlow<PickerChoices<Genre>?>
builder.setTitle(R.string.lbl_genres).setNegativeButton(R.string.lbl_cancel, null) get() = pickerModel.currentGenreChoices
}
override fun onBindingCreated(binding: DialogMusicPickerBinding, savedInstanceState: Bundle?) { override fun initChoices() {
binding.pickerRecycler.adapter = genreAdapter pickerModel.setGenreChoiceUid(args.genreUid)
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: Genre, viewHolder: RecyclerView.ViewHolder) { override fun onClick(item: Genre, viewHolder: RecyclerView.ViewHolder) {
super.onClick(item, viewHolder)
// User made a choice, play the given song from that genre. // User made a choice, play the given song from that genre.
val song = requireIs<Song>(unlikelyToBeNull(pickerModel.currentItem.value)) val song = unlikelyToBeNull(pickerModel.currentGenreChoices.value).song
playbackModel.playFromGenre(song, item) playbackModel.playFromGenre(song, item)
} }
} }

View file

@ -0,0 +1,96 @@
/*
* Copyright (c) 2023 Auxio Project
* PlaybackPickerViewModel.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.playback.picker
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import org.oxycblt.auxio.music.*
import org.oxycblt.auxio.picker.PickerChoices
/**
* A [ViewModel] that stores the choices shown in the playback picker dialogs.
*
* @author OxygenCobalt (Alexander Capehart)
*/
@HiltViewModel
class PlaybackPickerViewModel @Inject constructor(private val musicRepository: MusicRepository) :
ViewModel(), MusicRepository.UpdateListener {
private val _currentArtistChoices = MutableStateFlow<ArtistPlaybackChoices?>(null)
/** The current set of [Artist] choices to show in the picker, or null if to show nothing. */
val currentArtistChoices: StateFlow<ArtistPlaybackChoices?>
get() = _currentArtistChoices
private val _currentGenreChoices = MutableStateFlow<GenrePlaybackChoices?>(null)
/** The current set of [Genre] choices to show in the picker, or null if to show nothing. */
val currentGenreChoices: StateFlow<GenrePlaybackChoices?>
get() = _currentGenreChoices
init {
musicRepository.addUpdateListener(this)
}
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) }
}
}
override fun onCleared() {
super.onCleared()
musicRepository.removeUpdateListener(this)
}
/**
* Set the [Music.UID] of the item to show [Artist] choices for.
*
* @param uid The [Music.UID] of the item to show. Must be a [Song].
*/
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) }
}
}
data class ArtistPlaybackChoices(val song: Song) : PickerChoices<Artist> {
override val choices = song.artists
}
data class GenrePlaybackChoices(val song: Song) : PickerChoices<Genre> {
override val choices = song.genres
}

View file

@ -37,8 +37,8 @@ import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.list.ListFragment import org.oxycblt.auxio.list.ListFragment
import org.oxycblt.auxio.list.selection.SelectionViewModel import org.oxycblt.auxio.list.selection.SelectionViewModel
import org.oxycblt.auxio.music.* import org.oxycblt.auxio.music.*
import org.oxycblt.auxio.navigation.NavigationViewModel
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.NavigationViewModel
import org.oxycblt.auxio.util.* import org.oxycblt.auxio.util.*
/** /**

View file

@ -30,31 +30,31 @@
<dialog <dialog
android:id="@+id/artist_playback_picker_dialog" android:id="@+id/artist_playback_picker_dialog"
android:name="org.oxycblt.auxio.picker.ArtistPlaybackPickerDialog" android:name="org.oxycblt.auxio.playback.picker.ArtistPlaybackPickerDialog"
android:label="artist_playback_picker_dialog" android:label="artist_playback_picker_dialog"
tools:layout="@layout/dialog_music_picker"> tools:layout="@layout/dialog_music_picker">
<argument <argument
android:name="itemUid" android:name="artistUid"
app:argType="org.oxycblt.auxio.music.Music$UID" /> app:argType="org.oxycblt.auxio.music.Music$UID" />
</dialog> </dialog>
<dialog <dialog
android:id="@+id/artist_navigation_picker_dialog" android:id="@+id/artist_navigation_picker_dialog"
android:name="org.oxycblt.auxio.picker.ArtistNavigationPickerDialog" android:name="org.oxycblt.auxio.navigation.picker.ArtistNavigationPickerDialog"
android:label="artist_navigation_picker_dialog" android:label="artist_navigation_picker_dialog"
tools:layout="@layout/dialog_music_picker"> tools:layout="@layout/dialog_music_picker">
<argument <argument
android:name="itemUid" android:name="artistUid"
app:argType="org.oxycblt.auxio.music.Music$UID" /> app:argType="org.oxycblt.auxio.music.Music$UID" />
</dialog> </dialog>
<dialog <dialog
android:id="@+id/genre_playback_picker_dialog" android:id="@+id/genre_playback_picker_dialog"
android:name="org.oxycblt.auxio.picker.GenrePlaybackPickerDialog" android:name="org.oxycblt.auxio.playback.picker.GenrePlaybackPickerDialog"
android:label="genre_playback_picker_dialog" android:label="genre_playback_picker_dialog"
tools:layout="@layout/dialog_music_picker"> tools:layout="@layout/dialog_music_picker">
<argument <argument
android:name="itemUid" android:name="genreUid"
app:argType="org.oxycblt.auxio.music.Music$UID" /> app:argType="org.oxycblt.auxio.music.Music$UID" />
</dialog> </dialog>
@ -64,7 +64,7 @@
android:label="song_detail_dialog" android:label="song_detail_dialog"
tools:layout="@layout/dialog_song_detail"> tools:layout="@layout/dialog_song_detail">
<argument <argument
android:name="itemUid" android:name="songUid"
app:argType="org.oxycblt.auxio.music.Music$UID" /> app:argType="org.oxycblt.auxio.music.Music$UID" />
</dialog> </dialog>