detail: add selection
Add selection to the detail views.
This commit is contained in:
parent
32d01f2027
commit
813daed644
24 changed files with 277 additions and 327 deletions
|
@ -48,6 +48,10 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat
|
|||
*
|
||||
* TODO: Migrate to material animation system
|
||||
*
|
||||
* TODO: Re-document project
|
||||
*
|
||||
* TODO: Unit testing
|
||||
*
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
|
|
@ -32,7 +32,7 @@ import org.oxycblt.auxio.R
|
|||
import org.oxycblt.auxio.databinding.FragmentDetailBinding
|
||||
import org.oxycblt.auxio.detail.recycler.AlbumDetailAdapter
|
||||
import org.oxycblt.auxio.list.Item
|
||||
import org.oxycblt.auxio.list.MenuFragment
|
||||
import org.oxycblt.auxio.list.ListFragment
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.Music
|
||||
|
@ -53,7 +53,7 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
|
|||
* A fragment that shows information for a particular [Album].
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class AlbumDetailFragment : MenuFragment<FragmentDetailBinding>() {
|
||||
class AlbumDetailFragment : ListFragment<FragmentDetailBinding>() {
|
||||
private val detailModel: DetailViewModel by activityViewModels()
|
||||
|
||||
private val args: AlbumDetailFragmentArgs by navArgs()
|
||||
|
@ -64,7 +64,7 @@ class AlbumDetailFragment : MenuFragment<FragmentDetailBinding>() {
|
|||
AlbumDetailAdapter.Callback(
|
||||
::handleClick,
|
||||
::handleOpenItemMenu,
|
||||
{},
|
||||
::handleSelect,
|
||||
::handlePlay,
|
||||
::handleShuffle,
|
||||
::handleOpenSortMenu,
|
||||
|
@ -81,7 +81,7 @@ class AlbumDetailFragment : MenuFragment<FragmentDetailBinding>() {
|
|||
override fun onCreateBinding(inflater: LayoutInflater) = FragmentDetailBinding.inflate(inflater)
|
||||
|
||||
override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) {
|
||||
detailModel.setAlbumUid(args.albumUid)
|
||||
setupSelectionToolbar(binding.detailSelectionToolbar)
|
||||
|
||||
binding.detailToolbar.apply {
|
||||
inflateMenu(R.menu.menu_album_detail)
|
||||
|
@ -96,11 +96,14 @@ class AlbumDetailFragment : MenuFragment<FragmentDetailBinding>() {
|
|||
|
||||
// -- VIEWMODEL SETUP ---
|
||||
|
||||
detailModel.setAlbumUid(args.albumUid)
|
||||
|
||||
collectImmediately(detailModel.currentAlbum, ::updateItem)
|
||||
collectImmediately(detailModel.albumData, detailAdapter::submitList)
|
||||
collectImmediately(
|
||||
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
||||
collect(navModel.exploreNavigationItem, ::handleNavigation)
|
||||
collectImmediately(selectionModel.selected, ::handleSelection)
|
||||
}
|
||||
|
||||
override fun onDestroyBinding(binding: FragmentDetailBinding) {
|
||||
|
@ -108,13 +111,13 @@ class AlbumDetailFragment : MenuFragment<FragmentDetailBinding>() {
|
|||
binding.detailRecycler.adapter = null
|
||||
}
|
||||
|
||||
private fun handleClick(item: Item) {
|
||||
check(item is Song) { "Unexpected datatype: ${item::class.simpleName}" }
|
||||
override fun onRealClick(music: Music) {
|
||||
check(music is Song) { "Unexpected datatype: ${music::class.java}"}
|
||||
when (settings.detailPlaybackMode) {
|
||||
null,
|
||||
MusicMode.ALBUMS -> playbackModel.playFromAlbum(item)
|
||||
MusicMode.SONGS -> playbackModel.playFromAll(item)
|
||||
MusicMode.ARTISTS -> playbackModel.playFromArtist(item)
|
||||
MusicMode.ALBUMS -> playbackModel.playFromAlbum(music)
|
||||
MusicMode.SONGS -> playbackModel.playFromAll(music)
|
||||
MusicMode.ARTISTS -> playbackModel.playFromArtist(music)
|
||||
else -> error("Unexpected playback mode: ${settings.detailPlaybackMode}")
|
||||
}
|
||||
}
|
||||
|
@ -179,6 +182,26 @@ class AlbumDetailFragment : MenuFragment<FragmentDetailBinding>() {
|
|||
requireBinding().detailToolbar.title = album.resolveName(requireContext())
|
||||
}
|
||||
|
||||
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
|
||||
val binding = requireBinding()
|
||||
|
||||
for (item in binding.detailToolbar.menu.children) {
|
||||
// If there is no playback going in, any queue additions will be wiped as soon as
|
||||
// something is played. Disable these actions when playback is going on so that
|
||||
// it isn't possible to add anything during that time.
|
||||
if (item.itemId == R.id.action_play_next || item.itemId == R.id.action_queue_add) {
|
||||
item.isEnabled = song != null
|
||||
}
|
||||
}
|
||||
|
||||
if (parent is Album && parent == unlikelyToBeNull(detailModel.currentAlbum.value)) {
|
||||
detailAdapter.setPlayingItem(song, isPlaying)
|
||||
} else {
|
||||
// Clear the ViewHolders if the mode isn't ALL_SONGS
|
||||
detailAdapter.setPlayingItem(null, isPlaying)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleNavigation(item: Music?) {
|
||||
val binding = requireBinding()
|
||||
when (item) {
|
||||
|
@ -221,7 +244,7 @@ class AlbumDetailFragment : MenuFragment<FragmentDetailBinding>() {
|
|||
}
|
||||
}
|
||||
|
||||
/** Scroll to an [song]. */
|
||||
/** Scroll to a [song]. */
|
||||
private fun scrollToItem(song: Song) {
|
||||
// Calculate where the item for the currently played song is
|
||||
val pos = detailModel.albumData.value.indexOf(song)
|
||||
|
@ -241,24 +264,9 @@ class AlbumDetailFragment : MenuFragment<FragmentDetailBinding>() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
|
||||
val binding = requireBinding()
|
||||
|
||||
for (item in binding.detailToolbar.menu.children) {
|
||||
// If there is no playback going in, any queue additions will be wiped as soon as
|
||||
// something is played. Disable these actions when playback is going on so that
|
||||
// it isn't possible to add anything during that time.
|
||||
if (item.itemId == R.id.action_play_next || item.itemId == R.id.action_queue_add) {
|
||||
item.isEnabled = song != null
|
||||
}
|
||||
}
|
||||
|
||||
if (parent is Album && parent == unlikelyToBeNull(detailModel.currentAlbum.value)) {
|
||||
detailAdapter.updateIndicator(song, isPlaying)
|
||||
} else {
|
||||
// Clear the ViewHolders if the mode isn't ALL_SONGS
|
||||
detailAdapter.updateIndicator(null, isPlaying)
|
||||
}
|
||||
private fun handleSelection(selected: List<Music>) {
|
||||
detailAdapter.setSelectedItems(selected)
|
||||
requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -279,4 +287,5 @@ class AlbumDetailFragment : MenuFragment<FragmentDetailBinding>() {
|
|||
snapPreference: Int
|
||||
): Int = (boxStart + (boxEnd - boxStart) / 2) - (viewStart + (viewEnd - viewStart) / 2)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ import org.oxycblt.auxio.databinding.FragmentDetailBinding
|
|||
import org.oxycblt.auxio.detail.recycler.ArtistDetailAdapter
|
||||
import org.oxycblt.auxio.detail.recycler.DetailAdapter
|
||||
import org.oxycblt.auxio.list.Item
|
||||
import org.oxycblt.auxio.list.MenuFragment
|
||||
import org.oxycblt.auxio.list.ListFragment
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.Music
|
||||
|
@ -50,7 +50,7 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
|
|||
* A fragment that shows information for a particular [Artist].
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class ArtistDetailFragment : MenuFragment<FragmentDetailBinding>() {
|
||||
class ArtistDetailFragment : ListFragment<FragmentDetailBinding>() {
|
||||
private val detailModel: DetailViewModel by activityViewModels()
|
||||
|
||||
private val args: ArtistDetailFragmentArgs by navArgs()
|
||||
|
@ -61,7 +61,7 @@ class ArtistDetailFragment : MenuFragment<FragmentDetailBinding>() {
|
|||
DetailAdapter.Callback(
|
||||
::handleClick,
|
||||
::handleOpenItemMenu,
|
||||
{},
|
||||
::handleSelect,
|
||||
::handlePlay,
|
||||
::handleShuffle,
|
||||
::handleOpenSortMenu))
|
||||
|
@ -77,7 +77,7 @@ class ArtistDetailFragment : MenuFragment<FragmentDetailBinding>() {
|
|||
override fun onCreateBinding(inflater: LayoutInflater) = FragmentDetailBinding.inflate(inflater)
|
||||
|
||||
override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) {
|
||||
detailModel.setArtistUid(args.artistUid)
|
||||
setupSelectionToolbar(binding.detailSelectionToolbar)
|
||||
|
||||
binding.detailToolbar.apply {
|
||||
inflateMenu(R.menu.menu_genre_artist_detail)
|
||||
|
@ -92,11 +92,14 @@ class ArtistDetailFragment : MenuFragment<FragmentDetailBinding>() {
|
|||
|
||||
// --- VIEWMODEL SETUP ---
|
||||
|
||||
detailModel.setArtistUid(args.artistUid)
|
||||
|
||||
collectImmediately(detailModel.currentArtist, ::updateItem)
|
||||
collectImmediately(detailModel.artistData, detailAdapter::submitList)
|
||||
collectImmediately(
|
||||
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
||||
collect(navModel.exploreNavigationItem, ::handleNavigation)
|
||||
collectImmediately(selectionModel.selected, ::handleSelection)
|
||||
}
|
||||
|
||||
override fun onDestroyBinding(binding: FragmentDetailBinding) {
|
||||
|
@ -104,21 +107,21 @@ class ArtistDetailFragment : MenuFragment<FragmentDetailBinding>() {
|
|||
binding.detailRecycler.adapter = null
|
||||
}
|
||||
|
||||
private fun handleClick(item: Item) {
|
||||
when (item) {
|
||||
override fun onRealClick(music: Music) {
|
||||
when (music) {
|
||||
is Song -> {
|
||||
when (settings.detailPlaybackMode) {
|
||||
null ->
|
||||
playbackModel.playFromArtist(
|
||||
item, unlikelyToBeNull(detailModel.currentArtist.value))
|
||||
MusicMode.SONGS -> playbackModel.playFromAll(item)
|
||||
MusicMode.ALBUMS -> playbackModel.playFromAlbum(item)
|
||||
MusicMode.ARTISTS -> playbackModel.playFromArtist(item)
|
||||
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: ${settings.detailPlaybackMode}")
|
||||
}
|
||||
}
|
||||
is Album -> navModel.exploreNavigateTo(item)
|
||||
else -> error("Unexpected datatype: ${item::class.simpleName}")
|
||||
is Album -> navModel.exploreNavigateTo(music)
|
||||
else -> error("Unexpected datatype: ${music::class.simpleName}")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -180,6 +183,20 @@ class ArtistDetailFragment : MenuFragment<FragmentDetailBinding>() {
|
|||
requireBinding().detailToolbar.title = artist.resolveName(requireContext())
|
||||
}
|
||||
|
||||
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
|
||||
var item: Item? = null
|
||||
|
||||
if (parent is Album) {
|
||||
item = parent
|
||||
}
|
||||
|
||||
if (parent is Artist && parent == unlikelyToBeNull(detailModel.currentArtist.value)) {
|
||||
item = song
|
||||
}
|
||||
|
||||
detailAdapter.setPlayingItem(item, isPlaying)
|
||||
}
|
||||
|
||||
private fun handleNavigation(item: Music?) {
|
||||
val binding = requireBinding()
|
||||
|
||||
|
@ -210,17 +227,8 @@ class ArtistDetailFragment : MenuFragment<FragmentDetailBinding>() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
|
||||
var item: Item? = null
|
||||
|
||||
if (parent is Album) {
|
||||
item = parent
|
||||
}
|
||||
|
||||
if (parent is Artist && parent == unlikelyToBeNull(detailModel.currentArtist.value)) {
|
||||
item = song
|
||||
}
|
||||
|
||||
detailAdapter.updateIndicator(item, isPlaying)
|
||||
private fun handleSelection(selected: List<Music>) {
|
||||
detailAdapter.setSelectedItems(selected)
|
||||
requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ import org.oxycblt.auxio.databinding.FragmentDetailBinding
|
|||
import org.oxycblt.auxio.detail.recycler.DetailAdapter
|
||||
import org.oxycblt.auxio.detail.recycler.GenreDetailAdapter
|
||||
import org.oxycblt.auxio.list.Item
|
||||
import org.oxycblt.auxio.list.MenuFragment
|
||||
import org.oxycblt.auxio.list.ListFragment
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.Genre
|
||||
|
@ -51,7 +51,7 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
|
|||
* A fragment that shows information for a particular [Genre].
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class GenreDetailFragment : MenuFragment<FragmentDetailBinding>() {
|
||||
class GenreDetailFragment : ListFragment<FragmentDetailBinding>() {
|
||||
private val detailModel: DetailViewModel by activityViewModels()
|
||||
|
||||
private val args: GenreDetailFragmentArgs by navArgs()
|
||||
|
@ -62,10 +62,11 @@ class GenreDetailFragment : MenuFragment<FragmentDetailBinding>() {
|
|||
DetailAdapter.Callback(
|
||||
::handleClick,
|
||||
::handleOpenItemMenu,
|
||||
{},
|
||||
::handleSelect,
|
||||
::handlePlay,
|
||||
::handleShuffle,
|
||||
::handleOpenSortMenu))
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
|
||||
|
@ -77,7 +78,7 @@ class GenreDetailFragment : MenuFragment<FragmentDetailBinding>() {
|
|||
override fun onCreateBinding(inflater: LayoutInflater) = FragmentDetailBinding.inflate(inflater)
|
||||
|
||||
override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) {
|
||||
detailModel.setGenreUid(args.genreUid)
|
||||
setupSelectionToolbar(binding.detailSelectionToolbar)
|
||||
|
||||
binding.detailToolbar.apply {
|
||||
inflateMenu(R.menu.menu_genre_artist_detail)
|
||||
|
@ -92,11 +93,14 @@ class GenreDetailFragment : MenuFragment<FragmentDetailBinding>() {
|
|||
|
||||
// --- VIEWMODEL SETUP ---
|
||||
|
||||
detailModel.setGenreUid(args.genreUid)
|
||||
|
||||
collectImmediately(detailModel.currentGenre, ::handleItemChange)
|
||||
collectImmediately(detailModel.genreData, detailAdapter::submitList)
|
||||
collectImmediately(
|
||||
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
||||
collect(navModel.exploreNavigationItem, ::handleNavigation)
|
||||
collectImmediately(selectionModel.selected, ::handleSelection)
|
||||
}
|
||||
|
||||
override fun onDestroyBinding(binding: FragmentDetailBinding) {
|
||||
|
@ -117,20 +121,20 @@ class GenreDetailFragment : MenuFragment<FragmentDetailBinding>() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleClick(item: Item) {
|
||||
when (item) {
|
||||
is Artist -> navModel.exploreNavigateTo(item)
|
||||
override fun onRealClick(music: Music) {
|
||||
when (music) {
|
||||
is Artist -> navModel.exploreNavigateTo(music)
|
||||
is Song ->
|
||||
when (settings.detailPlaybackMode) {
|
||||
null ->
|
||||
playbackModel.playFromGenre(
|
||||
item, unlikelyToBeNull(detailModel.currentGenre.value))
|
||||
MusicMode.SONGS -> playbackModel.playFromAll(item)
|
||||
MusicMode.ALBUMS -> playbackModel.playFromAlbum(item)
|
||||
MusicMode.ARTISTS -> playbackModel.playFromArtist(item)
|
||||
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: ${settings.detailPlaybackMode}")
|
||||
}
|
||||
else -> error("Unexpected datatype: ${item::class.simpleName}")
|
||||
else -> error("Unexpected datatype: ${music::class.simpleName}")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -177,6 +181,21 @@ class GenreDetailFragment : MenuFragment<FragmentDetailBinding>() {
|
|||
requireBinding().detailToolbar.title = genre.resolveName(requireContext())
|
||||
}
|
||||
|
||||
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
|
||||
var item: Item? = null
|
||||
|
||||
if (parent is Artist) {
|
||||
item = parent
|
||||
}
|
||||
|
||||
if (parent is Genre && parent.uid == unlikelyToBeNull(detailModel.currentGenre.value).uid) {
|
||||
item = song
|
||||
}
|
||||
|
||||
detailAdapter.setPlayingItem(item, isPlaying)
|
||||
}
|
||||
|
||||
|
||||
private fun handleNavigation(item: Music?) {
|
||||
when (item) {
|
||||
is Song -> {
|
||||
|
@ -201,17 +220,8 @@ class GenreDetailFragment : MenuFragment<FragmentDetailBinding>() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
|
||||
var item: Item? = null
|
||||
|
||||
if (parent is Artist) {
|
||||
item = parent
|
||||
}
|
||||
|
||||
if (parent is Genre && parent.uid == unlikelyToBeNull(detailModel.currentGenre.value).uid) {
|
||||
item = song
|
||||
}
|
||||
|
||||
detailAdapter.updateIndicator(item, isPlaying)
|
||||
private fun handleSelection(selected: List<Music>) {
|
||||
detailAdapter.setSelectedItems(selected)
|
||||
requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ import org.oxycblt.auxio.databinding.ItemDetailBinding
|
|||
import org.oxycblt.auxio.databinding.ItemDiscHeaderBinding
|
||||
import org.oxycblt.auxio.detail.DiscHeader
|
||||
import org.oxycblt.auxio.list.Item
|
||||
import org.oxycblt.auxio.list.recycler.PlayingIndicatorAdapter
|
||||
import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter
|
||||
import org.oxycblt.auxio.list.recycler.SimpleItemCallback
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Song
|
||||
|
@ -41,7 +41,7 @@ import org.oxycblt.auxio.util.inflater
|
|||
* An adapter for displaying [Album] information and it's children.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class AlbumDetailAdapter(private val callback: AlbumDetailAdapter.Callback) :
|
||||
class AlbumDetailAdapter(private val callback: Callback) :
|
||||
DetailAdapter(callback, DIFFER) {
|
||||
|
||||
override fun getItemViewType(position: Int) =
|
||||
|
@ -180,7 +180,7 @@ class DiscHeaderViewHolder(private val binding: ItemDiscHeaderBinding) :
|
|||
}
|
||||
|
||||
private class AlbumSongViewHolder private constructor(private val binding: ItemAlbumSongBinding) :
|
||||
PlayingIndicatorAdapter.ViewHolder(binding.root) {
|
||||
SelectionIndicatorAdapter.ViewHolder(binding.root) {
|
||||
fun bind(item: Song, callback: AlbumDetailAdapter.Callback) {
|
||||
// Hide the track number view if the song does not have a track.
|
||||
if (item.track != null) {
|
||||
|
@ -215,6 +215,10 @@ private class AlbumSongViewHolder private constructor(private val binding: ItemA
|
|||
binding.songTrackBg.isPlaying = isPlaying
|
||||
}
|
||||
|
||||
override fun updateSelectionIndicator(isSelected: Boolean) {
|
||||
binding.root.isActivated = isSelected
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val VIEW_TYPE = IntegerTable.VIEW_TYPE_ALBUM_SONG
|
||||
|
||||
|
|
|
@ -28,7 +28,9 @@ import org.oxycblt.auxio.databinding.ItemParentBinding
|
|||
import org.oxycblt.auxio.databinding.ItemSongBinding
|
||||
import org.oxycblt.auxio.list.Item
|
||||
import org.oxycblt.auxio.list.ItemMenuCallback
|
||||
import org.oxycblt.auxio.list.ItemSelectCallback
|
||||
import org.oxycblt.auxio.list.recycler.PlayingIndicatorAdapter
|
||||
import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter
|
||||
import org.oxycblt.auxio.list.recycler.SimpleItemCallback
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
|
@ -153,14 +155,21 @@ private class ArtistDetailViewHolder private constructor(private val binding: It
|
|||
}
|
||||
|
||||
private class ArtistAlbumViewHolder private constructor(private val binding: ItemParentBinding) :
|
||||
PlayingIndicatorAdapter.ViewHolder(binding.root) {
|
||||
fun bind(item: Album, callback: ItemMenuCallback) {
|
||||
SelectionIndicatorAdapter.ViewHolder(binding.root) {
|
||||
fun bind(item: Album, callback: ItemSelectCallback) {
|
||||
binding.parentImage.bind(item)
|
||||
binding.parentName.text = item.resolveName(binding.context)
|
||||
binding.parentInfo.text =
|
||||
item.date?.resolveDate(binding.context) ?: binding.context.getString(R.string.def_date)
|
||||
binding.parentMenu.setOnClickListener { callback.onOpenMenu(item, it) }
|
||||
binding.root.setOnClickListener { callback.onClick(item) }
|
||||
binding.root.apply {
|
||||
setOnClickListener { callback.onClick(item) }
|
||||
setOnLongClickListener {
|
||||
callback.onSelect(item)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {
|
||||
|
@ -168,6 +177,10 @@ private class ArtistAlbumViewHolder private constructor(private val binding: Ite
|
|||
binding.parentImage.isPlaying = isPlaying
|
||||
}
|
||||
|
||||
override fun updateSelectionIndicator(isSelected: Boolean) {
|
||||
binding.root.isActivated = isSelected
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val VIEW_TYPE = IntegerTable.VIEW_TYPE_ARTIST_ALBUM
|
||||
|
||||
|
@ -183,13 +196,20 @@ private class ArtistAlbumViewHolder private constructor(private val binding: Ite
|
|||
}
|
||||
|
||||
private class ArtistSongViewHolder private constructor(private val binding: ItemSongBinding) :
|
||||
PlayingIndicatorAdapter.ViewHolder(binding.root) {
|
||||
fun bind(item: Song, callback: ItemMenuCallback) {
|
||||
SelectionIndicatorAdapter.ViewHolder(binding.root) {
|
||||
fun bind(item: Song, callback: ItemSelectCallback) {
|
||||
binding.songAlbumCover.bind(item)
|
||||
binding.songName.text = item.resolveName(binding.context)
|
||||
binding.songInfo.text = item.album.resolveName(binding.context)
|
||||
binding.songMenu.setOnClickListener { callback.onOpenMenu(item, it) }
|
||||
binding.root.setOnClickListener { callback.onClick(item) }
|
||||
binding.root.apply {
|
||||
setOnClickListener { callback.onClick(item) }
|
||||
setOnLongClickListener {
|
||||
callback.onSelect(item)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {
|
||||
|
@ -197,6 +217,10 @@ private class ArtistSongViewHolder private constructor(private val binding: Item
|
|||
binding.songAlbumCover.isPlaying = isPlaying
|
||||
}
|
||||
|
||||
override fun updateSelectionIndicator(isSelected: Boolean) {
|
||||
binding.root.isActivated = isSelected
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val VIEW_TYPE = IntegerTable.VIEW_TYPE_ARTIST_SONG
|
||||
|
||||
|
|
|
@ -29,17 +29,14 @@ import org.oxycblt.auxio.detail.SortHeader
|
|||
import org.oxycblt.auxio.list.Header
|
||||
import org.oxycblt.auxio.list.Item
|
||||
import org.oxycblt.auxio.list.ItemSelectCallback
|
||||
import org.oxycblt.auxio.list.recycler.AuxioRecyclerView
|
||||
import org.oxycblt.auxio.list.recycler.HeaderViewHolder
|
||||
import org.oxycblt.auxio.list.recycler.PlayingIndicatorAdapter
|
||||
import org.oxycblt.auxio.list.recycler.SimpleItemCallback
|
||||
import org.oxycblt.auxio.list.recycler.*
|
||||
import org.oxycblt.auxio.util.context
|
||||
import org.oxycblt.auxio.util.inflater
|
||||
|
||||
abstract class DetailAdapter(
|
||||
private val callback: Callback,
|
||||
diffCallback: DiffUtil.ItemCallback<Item>
|
||||
) : PlayingIndicatorAdapter<RecyclerView.ViewHolder>(), AuxioRecyclerView.SpanSizeLookup {
|
||||
) : SelectionIndicatorAdapter<RecyclerView.ViewHolder>(), AuxioRecyclerView.SpanSizeLookup {
|
||||
@Suppress("LeakingThis") override fun getItemCount() = differ.currentList.size
|
||||
|
||||
override fun getItemViewType(position: Int) =
|
||||
|
|
|
@ -45,7 +45,8 @@ import org.oxycblt.auxio.home.list.AlbumListFragment
|
|||
import org.oxycblt.auxio.home.list.ArtistListFragment
|
||||
import org.oxycblt.auxio.home.list.GenreListFragment
|
||||
import org.oxycblt.auxio.home.list.SongListFragment
|
||||
import org.oxycblt.auxio.list.SelectionFragment
|
||||
import org.oxycblt.auxio.list.ListFragment
|
||||
import org.oxycblt.auxio.list.selection.SelectionToolbarOverlay
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.Genre
|
||||
|
@ -63,7 +64,7 @@ import org.oxycblt.auxio.util.*
|
|||
* respective item.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class HomeFragment : SelectionFragment<FragmentHomeBinding>() {
|
||||
class HomeFragment : ListFragment<FragmentHomeBinding>() {
|
||||
private val homeModel: HomeViewModel by androidActivityViewModels()
|
||||
private val musicModel: MusicViewModel by activityViewModels()
|
||||
|
||||
|
@ -96,7 +97,7 @@ class HomeFragment : SelectionFragment<FragmentHomeBinding>() {
|
|||
|
||||
override fun onBindingCreated(binding: FragmentHomeBinding, savedInstanceState: Bundle?) {
|
||||
binding.homeAppbar.addOnOffsetChangedListener { _, it -> handleAppBarAnimation(it) }
|
||||
setupOverlay(binding.homeToolbarOverlay)
|
||||
setupSelectionToolbar(binding.homeSelectionToolbar)
|
||||
binding.homeToolbar.setOnMenuItemClickListener {
|
||||
handleHomeMenuItem(it)
|
||||
true
|
||||
|
@ -171,7 +172,7 @@ class HomeFragment : SelectionFragment<FragmentHomeBinding>() {
|
|||
val binding = requireBinding()
|
||||
val range = binding.homeAppbar.totalScrollRange
|
||||
|
||||
binding.homeToolbarOverlay.alpha =
|
||||
binding.homeSelectionToolbar.alpha =
|
||||
1f - (abs(verticalOffset.toFloat()) / (range.toFloat() / 2))
|
||||
|
||||
binding.homeContent.updatePadding(
|
||||
|
@ -182,8 +183,6 @@ class HomeFragment : SelectionFragment<FragmentHomeBinding>() {
|
|||
when (item.itemId) {
|
||||
R.id.action_search -> {
|
||||
logD("Navigating to search")
|
||||
// Reset selection (navigating to another selectable screen)
|
||||
selectionModel.consume()
|
||||
initAxisTransitions(MaterialSharedAxis.Z)
|
||||
findNavController().navigate(HomeFragmentDirections.actionShowSearch())
|
||||
}
|
||||
|
@ -382,15 +381,13 @@ class HomeFragment : SelectionFragment<FragmentHomeBinding>() {
|
|||
else -> return
|
||||
}
|
||||
|
||||
// Reset selection (navigating to another selectable screen)
|
||||
selectionModel.consume()
|
||||
initAxisTransitions(MaterialSharedAxis.X)
|
||||
findNavController().navigate(action)
|
||||
}
|
||||
|
||||
private fun updateSelection(selected: List<Music>) {
|
||||
val binding = requireBinding()
|
||||
if (binding.homeToolbarOverlay.updateSelectionAmount(selected.size) &&
|
||||
if (binding.homeSelectionToolbar.updateSelectionAmount(selected.size) &&
|
||||
selected.isNotEmpty()) {
|
||||
logD("Significant selection occurred, expanding AppBar")
|
||||
// Significant enough change where we want to expand the RecyclerView
|
||||
|
@ -409,7 +406,7 @@ class HomeFragment : SelectionFragment<FragmentHomeBinding>() {
|
|||
}
|
||||
|
||||
private fun setupTabs(binding: FragmentHomeBinding) {
|
||||
val toolbarParams = binding.homeToolbarOverlay.layoutParams as AppBarLayout.LayoutParams
|
||||
val toolbarParams = binding.homeSelectionToolbar.layoutParams as AppBarLayout.LayoutParams
|
||||
if (homeModel.tabs.size == 1) {
|
||||
// A single tab makes the tab layout redundant, hide it and disable the collapsing
|
||||
// behavior.
|
||||
|
|
|
@ -28,7 +28,7 @@ import org.oxycblt.auxio.R
|
|||
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
|
||||
import org.oxycblt.auxio.home.HomeViewModel
|
||||
import org.oxycblt.auxio.list.*
|
||||
import org.oxycblt.auxio.list.SelectionFragment
|
||||
import org.oxycblt.auxio.list.ListFragment
|
||||
import org.oxycblt.auxio.list.recycler.AlbumViewHolder
|
||||
import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter
|
||||
import org.oxycblt.auxio.list.recycler.SyncListDiffer
|
||||
|
@ -45,7 +45,7 @@ import org.oxycblt.auxio.util.collectImmediately
|
|||
* A [HomeListFragment] for showing a list of [Album]s.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class AlbumListFragment : SelectionFragment<FragmentHomeListBinding>() {
|
||||
class AlbumListFragment : ListFragment<FragmentHomeListBinding>() {
|
||||
private val homeModel: HomeViewModel by activityViewModels()
|
||||
|
||||
private val homeAdapter =
|
||||
|
@ -69,7 +69,7 @@ class AlbumListFragment : SelectionFragment<FragmentHomeListBinding>() {
|
|||
}
|
||||
|
||||
collectImmediately(homeModel.albums, homeAdapter::replaceList)
|
||||
collectImmediately(selectionModel.selected, homeAdapter::setSelected)
|
||||
collectImmediately(selectionModel.selected, homeAdapter::setSelectedItems)
|
||||
collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
||||
}
|
||||
|
||||
|
@ -78,7 +78,7 @@ class AlbumListFragment : SelectionFragment<FragmentHomeListBinding>() {
|
|||
binding.homeRecycler.adapter = null
|
||||
}
|
||||
|
||||
override fun onClick(music: Music) {
|
||||
override fun onRealClick(music: Music) {
|
||||
check(music is Album) { "Unexpected datatype: ${music::class.java}" }
|
||||
navModel.exploreNavigateTo(music)
|
||||
}
|
||||
|
@ -128,10 +128,10 @@ class AlbumListFragment : SelectionFragment<FragmentHomeListBinding>() {
|
|||
}
|
||||
private fun updatePlayback(parent: MusicParent?, isPlaying: Boolean) {
|
||||
if (parent is Album) {
|
||||
homeAdapter.updateIndicator(parent, isPlaying)
|
||||
homeAdapter.setPlayingItem(parent, isPlaying)
|
||||
} else {
|
||||
// Ignore playback not from albums
|
||||
homeAdapter.updateIndicator(null, isPlaying)
|
||||
homeAdapter.setPlayingItem(null, isPlaying)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ import org.oxycblt.auxio.R
|
|||
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
|
||||
import org.oxycblt.auxio.home.HomeViewModel
|
||||
import org.oxycblt.auxio.list.*
|
||||
import org.oxycblt.auxio.list.SelectionFragment
|
||||
import org.oxycblt.auxio.list.ListFragment
|
||||
import org.oxycblt.auxio.list.recycler.ArtistViewHolder
|
||||
import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter
|
||||
import org.oxycblt.auxio.list.recycler.SyncListDiffer
|
||||
|
@ -43,7 +43,7 @@ import org.oxycblt.auxio.util.nonZeroOrNull
|
|||
* A [HomeListFragment] for showing a list of [Artist]s.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class ArtistListFragment : SelectionFragment<FragmentHomeListBinding>() {
|
||||
class ArtistListFragment : ListFragment<FragmentHomeListBinding>() {
|
||||
private val homeModel: HomeViewModel by activityViewModels()
|
||||
|
||||
private val homeAdapter =
|
||||
|
@ -64,7 +64,7 @@ class ArtistListFragment : SelectionFragment<FragmentHomeListBinding>() {
|
|||
}
|
||||
|
||||
collectImmediately(homeModel.artists, homeAdapter::replaceList)
|
||||
collectImmediately(selectionModel.selected, homeAdapter::setSelected)
|
||||
collectImmediately(selectionModel.selected, homeAdapter::setSelectedItems)
|
||||
collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
||||
}
|
||||
|
||||
|
@ -92,7 +92,7 @@ class ArtistListFragment : SelectionFragment<FragmentHomeListBinding>() {
|
|||
}
|
||||
}
|
||||
|
||||
override fun onClick(music: Music) {
|
||||
override fun onRealClick(music: Music) {
|
||||
check(music is Artist) { "Unexpected datatype: ${music::class.java}" }
|
||||
navModel.exploreNavigateTo(music)
|
||||
}
|
||||
|
@ -104,10 +104,10 @@ class ArtistListFragment : SelectionFragment<FragmentHomeListBinding>() {
|
|||
|
||||
private fun updatePlayback(parent: MusicParent?, isPlaying: Boolean) {
|
||||
if (parent is Artist) {
|
||||
homeAdapter.updateIndicator(parent, isPlaying)
|
||||
homeAdapter.setPlayingItem(parent, isPlaying)
|
||||
} else {
|
||||
// Ignore playback not from artists
|
||||
homeAdapter.updateIndicator(null, isPlaying)
|
||||
homeAdapter.setPlayingItem(null, isPlaying)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ import org.oxycblt.auxio.R
|
|||
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
|
||||
import org.oxycblt.auxio.home.HomeViewModel
|
||||
import org.oxycblt.auxio.list.*
|
||||
import org.oxycblt.auxio.list.SelectionFragment
|
||||
import org.oxycblt.auxio.list.ListFragment
|
||||
import org.oxycblt.auxio.list.recycler.GenreViewHolder
|
||||
import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter
|
||||
import org.oxycblt.auxio.list.recycler.SyncListDiffer
|
||||
|
@ -42,7 +42,7 @@ import org.oxycblt.auxio.util.collectImmediately
|
|||
* A [HomeListFragment] for showing a list of [Genre]s.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class GenreListFragment : SelectionFragment<FragmentHomeListBinding>() {
|
||||
class GenreListFragment : ListFragment<FragmentHomeListBinding>() {
|
||||
private val homeModel: HomeViewModel by activityViewModels()
|
||||
|
||||
private val homeAdapter =
|
||||
|
@ -63,7 +63,7 @@ class GenreListFragment : SelectionFragment<FragmentHomeListBinding>() {
|
|||
}
|
||||
|
||||
collectImmediately(homeModel.genres, homeAdapter::replaceList)
|
||||
collectImmediately(selectionModel.selected, homeAdapter::setSelected)
|
||||
collectImmediately(selectionModel.selected, homeAdapter::setSelectedItems)
|
||||
collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
||||
}
|
||||
|
||||
|
@ -91,7 +91,7 @@ class GenreListFragment : SelectionFragment<FragmentHomeListBinding>() {
|
|||
}
|
||||
}
|
||||
|
||||
override fun onClick(music: Music) {
|
||||
override fun onRealClick(music: Music) {
|
||||
check(music is Genre) { "Unexpected datatype: ${music::class.java}" }
|
||||
navModel.exploreNavigateTo(music)
|
||||
}
|
||||
|
@ -103,10 +103,10 @@ class GenreListFragment : SelectionFragment<FragmentHomeListBinding>() {
|
|||
|
||||
private fun updatePlayback(parent: MusicParent?, isPlaying: Boolean) {
|
||||
if (parent is Genre) {
|
||||
homeAdapter.updateIndicator(parent, isPlaying)
|
||||
homeAdapter.setPlayingItem(parent, isPlaying)
|
||||
} else {
|
||||
// Ignore playback not from genres
|
||||
homeAdapter.updateIndicator(null, isPlaying)
|
||||
homeAdapter.setPlayingItem(null, isPlaying)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ import org.oxycblt.auxio.R
|
|||
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
|
||||
import org.oxycblt.auxio.home.HomeViewModel
|
||||
import org.oxycblt.auxio.list.*
|
||||
import org.oxycblt.auxio.list.SelectionFragment
|
||||
import org.oxycblt.auxio.list.ListFragment
|
||||
import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter
|
||||
import org.oxycblt.auxio.list.recycler.SongViewHolder
|
||||
import org.oxycblt.auxio.list.recycler.SyncListDiffer
|
||||
|
@ -47,7 +47,7 @@ import org.oxycblt.auxio.util.context
|
|||
* A [HomeListFragment] for showing a list of [Song]s.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class SongListFragment : SelectionFragment<FragmentHomeListBinding>() {
|
||||
class SongListFragment : ListFragment<FragmentHomeListBinding>() {
|
||||
private val homeModel: HomeViewModel by activityViewModels()
|
||||
|
||||
private val homeAdapter =
|
||||
|
@ -73,7 +73,7 @@ class SongListFragment : SelectionFragment<FragmentHomeListBinding>() {
|
|||
}
|
||||
|
||||
collectImmediately(homeModel.songs, homeAdapter::replaceList)
|
||||
collectImmediately(selectionModel.selected, homeAdapter::setSelected)
|
||||
collectImmediately(selectionModel.selected, homeAdapter::setSelectedItems)
|
||||
collectImmediately(
|
||||
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
||||
}
|
||||
|
@ -125,7 +125,7 @@ class SongListFragment : SelectionFragment<FragmentHomeListBinding>() {
|
|||
}
|
||||
}
|
||||
|
||||
override fun onClick(music: Music) {
|
||||
override fun onRealClick(music: Music) {
|
||||
check(music is Song) { "Unexpected datatype: ${music::class.java}" }
|
||||
when (settings.libPlaybackMode) {
|
||||
MusicMode.SONGS -> playbackModel.playFromAll(music)
|
||||
|
@ -142,10 +142,10 @@ class SongListFragment : SelectionFragment<FragmentHomeListBinding>() {
|
|||
|
||||
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
|
||||
if (parent == null) {
|
||||
homeAdapter.updateIndicator(song, isPlaying)
|
||||
homeAdapter.setPlayingItem(song, isPlaying)
|
||||
} else {
|
||||
// Ignore playback that is not from all songs
|
||||
homeAdapter.updateIndicator(null, isPlaying)
|
||||
homeAdapter.setPlayingItem(null, isPlaying)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -39,18 +39,3 @@ open class ItemSelectCallback(
|
|||
onOpenMenu: (Item, View) -> Unit,
|
||||
val onSelect: (Item) -> Unit
|
||||
) : ItemMenuCallback(onClick, onOpenMenu)
|
||||
|
||||
/** An interface for detecting if an item has been clicked once. */
|
||||
interface ItemClickListener {
|
||||
/** Called when an item is clicked once. */
|
||||
fun onItemClick(item: Item)
|
||||
}
|
||||
|
||||
/** An interface for detecting if an item has had it's menu opened. */
|
||||
interface MenuItemListener : ItemClickListener {
|
||||
/** Called when an item is long-clicked. */
|
||||
fun onSelect(item: Item) {}
|
||||
|
||||
/** Called when an item desires to open a menu relating to it. */
|
||||
fun onOpenMenu(item: Item, anchor: View)
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
|
||||
package org.oxycblt.auxio.list
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.annotation.MenuRes
|
||||
|
@ -25,10 +26,9 @@ import androidx.fragment.app.activityViewModels
|
|||
import androidx.viewbinding.ViewBinding
|
||||
import org.oxycblt.auxio.MainFragmentDirections
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.Genre
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.list.selection.SelectionToolbarOverlay
|
||||
import org.oxycblt.auxio.list.selection.SelectionViewModel
|
||||
import org.oxycblt.auxio.music.*
|
||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||
import org.oxycblt.auxio.shared.MainNavigationAction
|
||||
import org.oxycblt.auxio.shared.NavigationViewModel
|
||||
|
@ -37,17 +37,67 @@ import org.oxycblt.auxio.util.androidActivityViewModels
|
|||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.util.showToast
|
||||
|
||||
/**
|
||||
* A fragment capable of creating menus. Automatically keeps track of and disposes of menus,
|
||||
* preventing UI issues and memory leaks.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
abstract class MenuFragment<T : ViewBinding> : ViewBindingFragment<T>() {
|
||||
abstract class ListFragment<VB : ViewBinding> : ViewBindingFragment<VB>() {
|
||||
protected val selectionModel: SelectionViewModel by activityViewModels()
|
||||
private var currentMenu: PopupMenu? = null
|
||||
|
||||
protected val playbackModel: PlaybackViewModel by androidActivityViewModels()
|
||||
protected val navModel: NavigationViewModel by activityViewModels()
|
||||
|
||||
override fun onDestroyBinding(binding: VB) {
|
||||
super.onDestroyBinding(binding)
|
||||
currentMenu?.dismiss()
|
||||
currentMenu = null
|
||||
}
|
||||
|
||||
fun setupSelectionToolbar(toolbar: SelectionToolbarOverlay) {
|
||||
toolbar.apply {
|
||||
setOnSelectionCancelListener { selectionModel.consume() }
|
||||
setOnMenuItemClickListener {
|
||||
handleSelectionMenuItem(it)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Handle a media item with a selection. */
|
||||
private fun handleSelectionMenuItem(item: MenuItem) {
|
||||
when (item.itemId) {
|
||||
R.id.action_play_next -> {
|
||||
playbackModel.playNext(selectionModel.consume())
|
||||
requireContext().showToast(R.string.lng_queue_added)
|
||||
}
|
||||
R.id.action_queue_add -> {
|
||||
playbackModel.addToQueue(selectionModel.consume())
|
||||
requireContext().showToast(R.string.lng_queue_added)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when an item is clicked by the user and was not selected by [handleClick]. This can be
|
||||
* optionally implemented if [handleClick] is used.
|
||||
*/
|
||||
open fun onRealClick(music: Music) {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
/** Provided implementation of an item click callback that handles selection. */
|
||||
protected fun handleClick(item: Item) {
|
||||
check(item is Music) { "Unexpected datatype: ${item::class.simpleName}" }
|
||||
if (selectionModel.selected.value.isNotEmpty()) {
|
||||
selectionModel.select(item)
|
||||
} else {
|
||||
onRealClick(item)
|
||||
}
|
||||
}
|
||||
|
||||
/** Provided implementation of an item selection callback. */
|
||||
protected fun handleSelect(item: Item) {
|
||||
check(item is Music) { "Unexpected datatype: ${item::class.simpleName}" }
|
||||
selectionModel.select(item)
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the given menu in context of [song]. Assumes that the menu is only composed of common
|
||||
* [Song] options.
|
||||
|
@ -207,10 +257,4 @@ abstract class MenuFragment<T : ViewBinding> : ViewBindingFragment<T>() {
|
|||
show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyBinding(binding: T) {
|
||||
super.onDestroyBinding(binding)
|
||||
currentMenu?.dismiss()
|
||||
currentMenu = null
|
||||
}
|
||||
}
|
|
@ -1,70 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2022 Auxio Project
|
||||
*
|
||||
* 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.list
|
||||
|
||||
import android.view.MenuItem
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.util.showToast
|
||||
|
||||
abstract class SelectionFragment<VB : ViewBinding> : MenuFragment<VB>() {
|
||||
protected val selectionModel: SelectionViewModel by activityViewModels()
|
||||
|
||||
open fun onClick(music: Music) {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
protected fun setupOverlay(overlay: SelectionToolbarOverlay) {
|
||||
overlay.apply {
|
||||
setOnSelectionCancelListener { selectionModel.consume() }
|
||||
setOnMenuItemClickListener {
|
||||
handleSelectionMenuItem(it)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSelectionMenuItem(item: MenuItem) {
|
||||
when (item.itemId) {
|
||||
R.id.action_play_next -> {
|
||||
playbackModel.playNext(selectionModel.consume())
|
||||
requireContext().showToast(R.string.lng_queue_added)
|
||||
}
|
||||
R.id.action_queue_add -> {
|
||||
playbackModel.addToQueue(selectionModel.consume())
|
||||
requireContext().showToast(R.string.lng_queue_added)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected fun handleClick(item: Item) {
|
||||
check(item is Music) { "Unexpected datatype: ${item::class.simpleName}" }
|
||||
if (selectionModel.selected.value.isNotEmpty()) {
|
||||
selectionModel.select(item)
|
||||
} else {
|
||||
onClick(item)
|
||||
}
|
||||
}
|
||||
|
||||
protected fun handleSelect(item: Item) {
|
||||
check(item is Music) { "Unexpected datatype: ${item::class.simpleName}" }
|
||||
selectionModel.select(item)
|
||||
}
|
||||
}
|
|
@ -44,7 +44,12 @@ abstract class PlayingIndicatorAdapter<VH : RecyclerView.ViewHolder> : RecyclerV
|
|||
|
||||
abstract val currentList: List<Item>
|
||||
|
||||
fun updateIndicator(item: Item?, isPlaying: Boolean) {
|
||||
/**
|
||||
* Update the currently playing item in the list.
|
||||
* @param item The item being played, null if nothing is being played.
|
||||
* @param isPlaying Whether playback is ongoing or paused.
|
||||
*/
|
||||
fun setPlayingItem(item: Item?, isPlaying: Boolean) {
|
||||
var updatedItem = false
|
||||
|
||||
if (currentItem != item) {
|
||||
|
|
|
@ -37,7 +37,10 @@ abstract class SelectionIndicatorAdapter<VH : RecyclerView.ViewHolder> :
|
|||
}
|
||||
}
|
||||
|
||||
fun setSelected(items: List<Music>) {
|
||||
/**
|
||||
* Update the list of selected [items] within the adapter.
|
||||
*/
|
||||
fun setSelectedItems(items: List<Music>) {
|
||||
val oldSelectedItems = selectedItems
|
||||
val newSelectedItems = items.toSet()
|
||||
if (newSelectedItems == oldSelectedItems) {
|
||||
|
|
|
@ -26,7 +26,6 @@ import org.oxycblt.auxio.databinding.ItemParentBinding
|
|||
import org.oxycblt.auxio.databinding.ItemSongBinding
|
||||
import org.oxycblt.auxio.list.Header
|
||||
import org.oxycblt.auxio.list.ItemSelectCallback
|
||||
import org.oxycblt.auxio.list.MenuItemListener
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.Genre
|
||||
|
@ -41,20 +40,6 @@ import org.oxycblt.auxio.util.inflater
|
|||
*/
|
||||
class SongViewHolder private constructor(private val binding: ItemSongBinding) :
|
||||
SelectionIndicatorAdapter.ViewHolder(binding.root) {
|
||||
fun bind(item: Song, listener: MenuItemListener) {
|
||||
binding.songAlbumCover.bind(item)
|
||||
binding.songName.text = item.resolveName(binding.context)
|
||||
binding.songInfo.text = item.resolveArtistContents(binding.context)
|
||||
|
||||
binding.songMenu.setOnClickListener { listener.onOpenMenu(item, it) }
|
||||
binding.root.apply {
|
||||
setOnClickListener { listener.onItemClick(item) }
|
||||
setOnLongClickListener {
|
||||
listener.onSelect(item)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun bind(item: Song, callback: ItemSelectCallback) {
|
||||
binding.songAlbumCover.bind(item)
|
||||
|
@ -100,21 +85,6 @@ class SongViewHolder private constructor(private val binding: ItemSongBinding) :
|
|||
class AlbumViewHolder private constructor(private val binding: ItemParentBinding) :
|
||||
SelectionIndicatorAdapter.ViewHolder(binding.root) {
|
||||
|
||||
fun bind(item: Album, listener: MenuItemListener) {
|
||||
binding.parentImage.bind(item)
|
||||
binding.parentName.text = item.resolveName(binding.context)
|
||||
binding.parentInfo.text = item.resolveArtistContents(binding.context)
|
||||
|
||||
binding.parentMenu.setOnClickListener { listener.onOpenMenu(item, it) }
|
||||
binding.root.apply {
|
||||
setOnClickListener { listener.onItemClick(item) }
|
||||
setOnLongClickListener {
|
||||
listener.onSelect(item)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun bind(item: Album, callback: ItemSelectCallback) {
|
||||
binding.parentImage.bind(item)
|
||||
binding.parentName.text = item.resolveName(binding.context)
|
||||
|
@ -161,31 +131,6 @@ class AlbumViewHolder private constructor(private val binding: ItemParentBinding
|
|||
class ArtistViewHolder private constructor(private val binding: ItemParentBinding) :
|
||||
SelectionIndicatorAdapter.ViewHolder(binding.root) {
|
||||
|
||||
fun bind(item: Artist, listener: MenuItemListener) {
|
||||
binding.parentImage.bind(item)
|
||||
binding.parentName.text = item.resolveName(binding.context)
|
||||
|
||||
binding.parentInfo.text =
|
||||
if (item.songs.isNotEmpty()) {
|
||||
binding.context.getString(
|
||||
R.string.fmt_two,
|
||||
binding.context.getPlural(R.plurals.fmt_album_count, item.albums.size),
|
||||
binding.context.getPlural(R.plurals.fmt_song_count, item.songs.size))
|
||||
} else {
|
||||
// Artist has no songs, only display an album count.
|
||||
binding.context.getPlural(R.plurals.fmt_album_count, item.albums.size)
|
||||
}
|
||||
|
||||
binding.parentMenu.setOnClickListener { listener.onOpenMenu(item, it) }
|
||||
binding.root.apply {
|
||||
setOnClickListener { listener.onItemClick(item) }
|
||||
setOnLongClickListener {
|
||||
listener.onSelect(item)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun bind(item: Artist, callback: ItemSelectCallback) {
|
||||
binding.parentImage.bind(item)
|
||||
binding.parentName.text = item.resolveName(binding.context)
|
||||
|
@ -242,25 +187,6 @@ class ArtistViewHolder private constructor(private val binding: ItemParentBindin
|
|||
class GenreViewHolder private constructor(private val binding: ItemParentBinding) :
|
||||
SelectionIndicatorAdapter.ViewHolder(binding.root) {
|
||||
|
||||
fun bind(item: Genre, listener: MenuItemListener) {
|
||||
binding.parentImage.bind(item)
|
||||
binding.parentName.text = item.resolveName(binding.context)
|
||||
binding.parentInfo.text =
|
||||
binding.context.getString(
|
||||
R.string.fmt_two,
|
||||
binding.context.getPlural(R.plurals.fmt_artist_count, item.artists.size),
|
||||
binding.context.getPlural(R.plurals.fmt_song_count, item.songs.size))
|
||||
|
||||
binding.parentMenu.setOnClickListener { listener.onOpenMenu(item, it) }
|
||||
binding.root.apply {
|
||||
setOnClickListener { listener.onItemClick(item) }
|
||||
setOnLongClickListener {
|
||||
listener.onSelect(item)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun bind(item: Genre, callback: ItemSelectCallback) {
|
||||
binding.parentImage.bind(item)
|
||||
binding.parentName.text = item.resolveName(binding.context)
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.list
|
||||
package org.oxycblt.auxio.list.selection
|
||||
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.Context
|
||||
|
@ -43,6 +43,10 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
|||
MaterialToolbar(context).apply {
|
||||
setNavigationIcon(R.drawable.ic_close_24)
|
||||
inflateMenu(R.menu.menu_selection_actions)
|
||||
|
||||
if (isInEditMode) {
|
||||
isInvisible = true
|
||||
}
|
||||
}
|
||||
|
||||
private var fadeThroughAnimator: ValueAnimator? = null
|
|
@ -15,7 +15,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.list
|
||||
package org.oxycblt.auxio.list.selection
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
@ -46,9 +46,7 @@ class SelectionViewModel : ViewModel() {
|
|||
}
|
||||
|
||||
/** Clear and return all selected items. */
|
||||
fun consume(): List<Music> {
|
||||
return _selected.value.also { _selected.value = listOf() }
|
||||
}
|
||||
fun consume() = _selected.value.also { _selected.value = listOf() }
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
|
@ -32,7 +32,7 @@ import org.oxycblt.auxio.R
|
|||
import org.oxycblt.auxio.databinding.FragmentSearchBinding
|
||||
import org.oxycblt.auxio.list.Item
|
||||
import org.oxycblt.auxio.list.ItemSelectCallback
|
||||
import org.oxycblt.auxio.list.SelectionFragment
|
||||
import org.oxycblt.auxio.list.ListFragment
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.Genre
|
||||
|
@ -48,7 +48,7 @@ import org.oxycblt.auxio.util.*
|
|||
* better keyboard logic, recycler updating, and chips
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class SearchFragment : SelectionFragment<FragmentSearchBinding>() {
|
||||
class SearchFragment : ListFragment<FragmentSearchBinding>() {
|
||||
|
||||
// SearchViewModel is only scoped to this Fragment
|
||||
private val searchModel: SearchViewModel by androidViewModels()
|
||||
|
@ -73,7 +73,7 @@ class SearchFragment : SelectionFragment<FragmentSearchBinding>() {
|
|||
override fun onCreateBinding(inflater: LayoutInflater) = FragmentSearchBinding.inflate(inflater)
|
||||
|
||||
override fun onBindingCreated(binding: FragmentSearchBinding, savedInstanceState: Bundle?) {
|
||||
setupOverlay(binding.searchToolbarOverlay)
|
||||
setupSelectionToolbar(binding.searchSelectionToolbar)
|
||||
|
||||
binding.searchToolbar.apply {
|
||||
val itemIdToSelect =
|
||||
|
@ -124,7 +124,7 @@ class SearchFragment : SelectionFragment<FragmentSearchBinding>() {
|
|||
binding.searchRecycler.adapter = null
|
||||
}
|
||||
|
||||
override fun onClick(music: Music) {
|
||||
override fun onRealClick(music: Music) {
|
||||
when (music) {
|
||||
is Song ->
|
||||
when (settings.libPlaybackMode) {
|
||||
|
@ -138,8 +138,6 @@ class SearchFragment : SelectionFragment<FragmentSearchBinding>() {
|
|||
}
|
||||
|
||||
private fun handleSearchNavigateUp() {
|
||||
// Reset selection (navigating to another selectable screen)
|
||||
selectionModel.consume()
|
||||
// Drop keyboard as it's no longer needed
|
||||
imm.hide()
|
||||
findNavController().navigateUp()
|
||||
|
@ -176,7 +174,7 @@ class SearchFragment : SelectionFragment<FragmentSearchBinding>() {
|
|||
}
|
||||
|
||||
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
|
||||
searchAdapter.updateIndicator(parent ?: song, isPlaying)
|
||||
searchAdapter.setPlayingItem(parent ?: song, isPlaying)
|
||||
}
|
||||
|
||||
private fun handleNavigation(item: Music?) {
|
||||
|
@ -190,17 +188,13 @@ class SearchFragment : SelectionFragment<FragmentSearchBinding>() {
|
|||
}
|
||||
|
||||
findNavController().navigate(action)
|
||||
|
||||
// Reset selection (navigating to another selectable screen)
|
||||
selectionModel.consume()
|
||||
|
||||
// Drop keyboard as it's no longer needed
|
||||
imm.hide()
|
||||
}
|
||||
|
||||
private fun updateSelection(selected: List<Music>) {
|
||||
searchAdapter.setSelected(selected)
|
||||
if (requireBinding().searchToolbarOverlay.updateSelectionAmount(selected.size) &&
|
||||
searchAdapter.setSelectedItems(selected)
|
||||
if (requireBinding().searchSelectionToolbar.updateSelectionAmount(selected.size) &&
|
||||
selected.isNotEmpty()) {
|
||||
imm.hide()
|
||||
}
|
||||
|
|
|
@ -13,11 +13,19 @@
|
|||
app:liftOnScroll="true"
|
||||
app:liftOnScrollTargetViewId="@id/detail_recycler">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/detail_toolbar"
|
||||
<org.oxycblt.auxio.list.selection.SelectionToolbarOverlay
|
||||
android:id="@+id/detail_selection_toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:navigationIcon="@drawable/ic_back_24" />
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/detail_toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:navigationIcon="@drawable/ic_back_24" />
|
||||
|
||||
</org.oxycblt.auxio.list.selection.SelectionToolbarOverlay>
|
||||
|
||||
|
||||
</org.oxycblt.auxio.detail.DetailAppBarLayout>
|
||||
|
||||
|
|
|
@ -13,8 +13,8 @@
|
|||
style="@style/Widget.Auxio.AppBarLayout"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<org.oxycblt.auxio.list.SelectionToolbarOverlay
|
||||
android:id="@+id/home_toolbar_overlay"
|
||||
<org.oxycblt.auxio.list.selection.SelectionToolbarOverlay
|
||||
android:id="@+id/home_selection_toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
|
@ -26,7 +26,7 @@
|
|||
app:menu="@menu/menu_home"
|
||||
app:title="@string/info_app_name" />
|
||||
|
||||
</org.oxycblt.auxio.list.SelectionToolbarOverlay>
|
||||
</org.oxycblt.auxio.list.selection.SelectionToolbarOverlay>
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/home_tabs"
|
||||
|
|
|
@ -12,8 +12,8 @@
|
|||
app:liftOnScroll="true"
|
||||
app:liftOnScrollTargetViewId="@id/search_recycler">
|
||||
|
||||
<org.oxycblt.auxio.list.SelectionToolbarOverlay
|
||||
android:id="@+id/search_toolbar_overlay"
|
||||
<org.oxycblt.auxio.list.selection.SelectionToolbarOverlay
|
||||
android:id="@+id/search_selection_toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
|
@ -49,7 +49,7 @@
|
|||
|
||||
</com.google.android.material.appbar.MaterialToolbar>
|
||||
|
||||
</org.oxycblt.auxio.list.SelectionToolbarOverlay>
|
||||
</org.oxycblt.auxio.list.selection.SelectionToolbarOverlay>
|
||||
|
||||
</org.oxycblt.auxio.shared.AuxioAppBarLayout>
|
||||
|
||||
|
|
Loading…
Reference in a new issue