diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt index 0e1de5d35..452b050cd 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt @@ -653,7 +653,6 @@ constructor( val list = mutableListOf() // Genre is guaranteed to always have artists and songs. val artistHeader = BasicHeader(R.string.lbl_artists) - list.add(Divider(artistHeader)) list.add(artistHeader) list.addAll(GENRE_ARTIST_SORT.artists(genre.artists)) diff --git a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt index 07e15faa5..8938328e9 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt @@ -19,22 +19,15 @@ package org.oxycblt.auxio.detail import android.os.Bundle -import android.view.LayoutInflater +import androidx.core.view.isVisible import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs -import androidx.recyclerview.widget.ConcatAdapter -import androidx.recyclerview.widget.GridLayoutManager import com.google.android.material.transition.MaterialSharedAxis import dagger.hilt.android.AndroidEntryPoint import org.oxycblt.auxio.R -import org.oxycblt.auxio.databinding.FragmentDetailBinding -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.databinding.FragmentDetail2Binding 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.ListFragment 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.util.collect import org.oxycblt.auxio.util.collectImmediately +import org.oxycblt.auxio.util.getPlural import org.oxycblt.auxio.util.logD 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.unlikelyToBeNull @@ -64,18 +56,15 @@ import org.oxycblt.auxio.util.unlikelyToBeNull * @author Alexander Capehart (OxygenCobalt) */ @AndroidEntryPoint -class GenreDetailFragment : - ListFragment(), - DetailHeaderAdapter.Listener, - DetailListAdapter.Listener { +class GenreDetailFragment : DetailFragment() { private val detailModel: DetailViewModel by activityViewModels() override val listModel: ListViewModel by activityViewModels() override val musicModel: MusicViewModel by activityViewModels() override val playbackModel: PlaybackViewModel by activityViewModels() + // 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. private val args: GenreDetailFragmentArgs by navArgs() - private val genreHeaderAdapter = GenreDetailHeaderAdapter(this) private val genreListAdapter = GenreDetailListAdapter(this) override fun onCreate(savedInstanceState: Bundle?) { @@ -86,43 +75,15 @@ class GenreDetailFragment : reenterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false) } - override fun onCreateBinding(inflater: LayoutInflater) = FragmentDetailBinding.inflate(inflater) + override fun getDetailListAdapter() = genreListAdapter - override fun getSelectionToolbar(binding: FragmentDetailBinding) = - binding.detailSelectionToolbar - - override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) { + override fun onBindingCreated(binding: FragmentDetail2Binding, savedInstanceState: Bundle?) { 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 --- // DetailViewModel handles most initialization from the navigation argument. detailModel.setGenre(args.genreUid) - collectImmediately(detailModel.currentGenre, ::updatePlaylist) + collectImmediately(detailModel.currentGenre, ::updateGenre) collectImmediately(detailModel.genreSongList, ::updateList) collect(detailModel.toShow.flow, ::handleShow) collect(listModel.menu.flow, ::handleMenu) @@ -134,10 +95,8 @@ class GenreDetailFragment : collect(playbackModel.playbackDecision.flow, ::handlePlaybackDecision) } - override fun onDestroyBinding(binding: FragmentDetailBinding) { + override fun onDestroyBinding(binding: FragmentDetail2Binding) { 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 // during list initialization and crash the app. Could happen if the user is fast enough. 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) { when (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() { findNavController().navigateSafe(GenreDetailFragmentDirections.sort()) } - private fun updatePlaylist(genre: Genre?) { + private fun updateGenre(genre: Genre?) { if (genre == null) { logD("No genre to show, navigating away") findNavController().navigateUp() return } - requireBinding().detailNormalToolbar.title = genre.name.resolve(requireContext()) - genreHeaderAdapter.setParent(genre) + val binding = requireBinding() + 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) { diff --git a/app/src/main/java/org/oxycblt/auxio/detail/header/GenreDetailHeaderAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/header/GenreDetailHeaderAdapter.kt deleted file mode 100644 index 42e2b4f09..000000000 --- a/app/src/main/java/org/oxycblt/auxio/detail/header/GenreDetailHeaderAdapter.kt +++ /dev/null @@ -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 . - */ - -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() { - 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)) - } -}