detail: make genre view use collapsing toolbar

This commit is contained in:
Alexander Capehart 2024-07-20 13:00:10 -06:00
parent 0eb3ede8ec
commit 6ea7233626
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
3 changed files with 35 additions and 150 deletions

View file

@ -653,7 +653,6 @@ constructor(
val list = mutableListOf<Item>() val list = mutableListOf<Item>()
// Genre is guaranteed to always have artists and songs. // Genre is guaranteed to always have artists and songs.
val artistHeader = BasicHeader(R.string.lbl_artists) val artistHeader = BasicHeader(R.string.lbl_artists)
list.add(Divider(artistHeader))
list.add(artistHeader) list.add(artistHeader)
list.addAll(GENRE_ARTIST_SORT.artists(genre.artists)) list.addAll(GENRE_ARTIST_SORT.artists(genre.artists))

View file

@ -19,22 +19,15 @@
package org.oxycblt.auxio.detail package org.oxycblt.auxio.detail
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import androidx.core.view.isVisible
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.GridLayoutManager
import com.google.android.material.transition.MaterialSharedAxis import com.google.android.material.transition.MaterialSharedAxis
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentDetailBinding import org.oxycblt.auxio.databinding.FragmentDetail2Binding
import org.oxycblt.auxio.detail.header.DetailHeaderAdapter
import org.oxycblt.auxio.detail.header.GenreDetailHeaderAdapter
import org.oxycblt.auxio.detail.list.DetailListAdapter
import org.oxycblt.auxio.detail.list.GenreDetailListAdapter import org.oxycblt.auxio.detail.list.GenreDetailListAdapter
import org.oxycblt.auxio.list.Divider
import org.oxycblt.auxio.list.Header
import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.list.ListFragment import org.oxycblt.auxio.list.ListFragment
import org.oxycblt.auxio.list.ListViewModel import org.oxycblt.auxio.list.ListViewModel
@ -51,10 +44,9 @@ import org.oxycblt.auxio.playback.PlaybackDecision
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.util.collect import org.oxycblt.auxio.util.collect
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.getPlural
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.navigateSafe import org.oxycblt.auxio.util.navigateSafe
import org.oxycblt.auxio.util.overrideOnOverflowMenuClick
import org.oxycblt.auxio.util.setFullWidthLookup
import org.oxycblt.auxio.util.showToast import org.oxycblt.auxio.util.showToast
import org.oxycblt.auxio.util.unlikelyToBeNull import org.oxycblt.auxio.util.unlikelyToBeNull
@ -64,18 +56,15 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
@AndroidEntryPoint @AndroidEntryPoint
class GenreDetailFragment : class GenreDetailFragment : DetailFragment<Genre, Music>() {
ListFragment<Music, FragmentDetailBinding>(),
DetailHeaderAdapter.Listener,
DetailListAdapter.Listener<Music> {
private val detailModel: DetailViewModel by activityViewModels() private val detailModel: DetailViewModel by activityViewModels()
override val listModel: ListViewModel by activityViewModels() override val listModel: ListViewModel by activityViewModels()
override val musicModel: MusicViewModel by activityViewModels() override val musicModel: MusicViewModel by activityViewModels()
override val playbackModel: PlaybackViewModel by activityViewModels() override val playbackModel: PlaybackViewModel by activityViewModels()
// Information about what genre to display is initially within the navigation arguments // Information about what genre to display is initially within the navigation arguments
// as a UID, as that is the only safe way to parcel an genre. // as a UID, as that is the only safe way to parcel an genre.
private val args: GenreDetailFragmentArgs by navArgs() private val args: GenreDetailFragmentArgs by navArgs()
private val genreHeaderAdapter = GenreDetailHeaderAdapter(this)
private val genreListAdapter = GenreDetailListAdapter(this) private val genreListAdapter = GenreDetailListAdapter(this)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -86,43 +75,15 @@ class GenreDetailFragment :
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false) reenterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false)
} }
override fun onCreateBinding(inflater: LayoutInflater) = FragmentDetailBinding.inflate(inflater) override fun getDetailListAdapter() = genreListAdapter
override fun getSelectionToolbar(binding: FragmentDetailBinding) = override fun onBindingCreated(binding: FragmentDetail2Binding, savedInstanceState: Bundle?) {
binding.detailSelectionToolbar
override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) {
super.onBindingCreated(binding, savedInstanceState) super.onBindingCreated(binding, savedInstanceState)
// --- UI SETUP ---
binding.detailNormalToolbar.apply {
setNavigationOnClickListener { findNavController().navigateUp() }
setOnMenuItemClickListener(this@GenreDetailFragment)
overrideOnOverflowMenuClick {
listModel.openMenu(
R.menu.detail_parent, unlikelyToBeNull(detailModel.currentGenre.value))
}
}
binding.detailRecycler.apply {
adapter = ConcatAdapter(genreHeaderAdapter, genreListAdapter)
(layoutManager as GridLayoutManager).setFullWidthLookup {
if (it != 0) {
val item =
detailModel.genreSongList.value.getOrElse(it - 1) {
return@setFullWidthLookup false
}
item is Divider || item is Header
} else {
true
}
}
}
// --- VIEWMODEL SETUP --- // --- VIEWMODEL SETUP ---
// DetailViewModel handles most initialization from the navigation argument. // DetailViewModel handles most initialization from the navigation argument.
detailModel.setGenre(args.genreUid) detailModel.setGenre(args.genreUid)
collectImmediately(detailModel.currentGenre, ::updatePlaylist) collectImmediately(detailModel.currentGenre, ::updateGenre)
collectImmediately(detailModel.genreSongList, ::updateList) collectImmediately(detailModel.genreSongList, ::updateList)
collect(detailModel.toShow.flow, ::handleShow) collect(detailModel.toShow.flow, ::handleShow)
collect(listModel.menu.flow, ::handleMenu) collect(listModel.menu.flow, ::handleMenu)
@ -134,10 +95,8 @@ class GenreDetailFragment :
collect(playbackModel.playbackDecision.flow, ::handlePlaybackDecision) collect(playbackModel.playbackDecision.flow, ::handlePlaybackDecision)
} }
override fun onDestroyBinding(binding: FragmentDetailBinding) { override fun onDestroyBinding(binding: FragmentDetail2Binding) {
super.onDestroyBinding(binding) super.onDestroyBinding(binding)
binding.detailNormalToolbar.setOnMenuItemClickListener(null)
binding.detailRecycler.adapter = null
// Avoid possible race conditions that could cause a bad replace instruction to be consumed // Avoid possible race conditions that could cause a bad replace instruction to be consumed
// during list initialization and crash the app. Could happen if the user is fast enough. // during list initialization and crash the app. Could happen if the user is fast enough.
detailModel.genreSongInstructions.consume() detailModel.genreSongInstructions.consume()
@ -151,6 +110,10 @@ class GenreDetailFragment :
} }
} }
override fun onOpenParentMenu() {
listModel.openMenu(R.menu.detail_parent, unlikelyToBeNull(detailModel.currentGenre.value))
}
override fun onOpenMenu(item: Music) { override fun onOpenMenu(item: Music) {
when (item) { when (item) {
is Artist -> listModel.openMenu(R.menu.parent, item) is Artist -> listModel.openMenu(R.menu.parent, item)
@ -159,26 +122,37 @@ class GenreDetailFragment :
} }
} }
override fun onPlay() {
playbackModel.play(unlikelyToBeNull(detailModel.currentGenre.value))
}
override fun onShuffle() {
playbackModel.shuffle(unlikelyToBeNull(detailModel.currentGenre.value))
}
override fun onOpenSortMenu() { override fun onOpenSortMenu() {
findNavController().navigateSafe(GenreDetailFragmentDirections.sort()) findNavController().navigateSafe(GenreDetailFragmentDirections.sort())
} }
private fun updatePlaylist(genre: Genre?) { private fun updateGenre(genre: Genre?) {
if (genre == null) { if (genre == null) {
logD("No genre to show, navigating away") logD("No genre to show, navigating away")
findNavController().navigateUp() findNavController().navigateUp()
return return
} }
requireBinding().detailNormalToolbar.title = genre.name.resolve(requireContext()) val binding = requireBinding()
genreHeaderAdapter.setParent(genre) val context = requireContext()
val name = genre.name.resolve(context)
binding.detailToolbarTitle.text = name
binding.detailCover.bind(genre)
binding.detailType.text = context.getString(R.string.lbl_genre)
binding.detailName.text = genre.name.resolve(context)
// Nothing about a genre is applicable to the sub-head text.
binding.detailSubhead.isVisible = false
// The song and artist count of the genre maps to the info text.
binding.detailInfo.text =
context.getString(
R.string.fmt_two,
context.getPlural(R.plurals.fmt_artist_count, genre.artists.size),
context.getPlural(R.plurals.fmt_song_count, genre.songs.size))
binding.detailPlayButton.setOnClickListener {
playbackModel.play(unlikelyToBeNull(detailModel.currentGenre.value))
}
binding.detailShuffleButton.setOnClickListener {
playbackModel.shuffle(unlikelyToBeNull(detailModel.currentGenre.value))
}
} }
private fun updateList(list: List<Item>) { private fun updateList(list: List<Item>) {

View file

@ -1,88 +0,0 @@
/*
* Copyright (c) 2023 Auxio Project
* GenreDetailHeaderAdapter.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.detail.header
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.ItemDetailHeaderBinding
import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.getPlural
import org.oxycblt.auxio.util.inflater
/**
* A [DetailHeaderAdapter] that shows [Genre] information.
*
* @param listener [DetailHeaderAdapter.Listener] to bind interactions to.
* @author Alexander Capehart (OxygenCobalt)
*/
class GenreDetailHeaderAdapter(private val listener: Listener) :
DetailHeaderAdapter<Genre, GenreDetailHeaderViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
GenreDetailHeaderViewHolder.from(parent)
override fun onBindHeader(holder: GenreDetailHeaderViewHolder, parent: Genre) =
holder.bind(parent, listener)
}
/**
* A [RecyclerView.ViewHolder] that displays the [Genre] header in the detail view. Use [from] to
* create an instance.
*
* @author Alexander Capehart (OxygenCobalt)
*/
class GenreDetailHeaderViewHolder
private constructor(private val binding: ItemDetailHeaderBinding) :
RecyclerView.ViewHolder(binding.root) {
/**
* Bind new data to this instance.
*
* @param genre The new [Genre] to bind.
* @param listener A [DetailHeaderAdapter.Listener] to bind interactions to.
*/
fun bind(genre: Genre, listener: DetailHeaderAdapter.Listener) {
binding.detailCover.bind(genre)
binding.detailType.text = binding.context.getString(R.string.lbl_genre)
binding.detailName.text = genre.name.resolve(binding.context)
// Nothing about a genre is applicable to the sub-head text.
binding.detailSubhead.isVisible = false
// The song and artist count of the genre maps to the info text.
binding.detailInfo.text =
binding.context.getString(
R.string.fmt_two,
binding.context.getPlural(R.plurals.fmt_artist_count, genre.artists.size),
binding.context.getPlural(R.plurals.fmt_song_count, genre.songs.size))
binding.detailPlayButton.setOnClickListener { listener.onPlay() }
binding.detailShuffleButton.setOnClickListener { listener.onShuffle() }
}
companion object {
/**
* Create a new instance.
*
* @param parent The parent to inflate this instance from.
* @return A new instance.
*/
fun from(parent: View) =
GenreDetailHeaderViewHolder(ItemDetailHeaderBinding.inflate(parent.context.inflater))
}
}