detail: separate header from list data
Separate the header information into it's own adapter, and concatenate it with the prior adapter. This way, it can be updated separately without a jarring list update.
This commit is contained in:
parent
f57b5dfc81
commit
9cc75a0e11
33 changed files with 570 additions and 490 deletions
|
@ -37,18 +37,12 @@ object IntegerTable {
|
|||
const val VIEW_TYPE_BASIC_HEADER = 0xA004
|
||||
/** SortHeaderViewHolder */
|
||||
const val VIEW_TYPE_SORT_HEADER = 0xA005
|
||||
/** AlbumDetailViewHolder */
|
||||
const val VIEW_TYPE_ALBUM_DETAIL = 0xA006
|
||||
/** AlbumSongViewHolder */
|
||||
const val VIEW_TYPE_ALBUM_SONG = 0xA007
|
||||
/** ArtistDetailViewHolder */
|
||||
const val VIEW_TYPE_ARTIST_DETAIL = 0xA008
|
||||
/** ArtistAlbumViewHolder */
|
||||
const val VIEW_TYPE_ARTIST_ALBUM = 0xA009
|
||||
/** ArtistSongViewHolder */
|
||||
const val VIEW_TYPE_ARTIST_SONG = 0xA00A
|
||||
/** GenreDetailViewHolder */
|
||||
const val VIEW_TYPE_GENRE_DETAIL = 0xA00B
|
||||
/** DiscHeaderViewHolder */
|
||||
const val VIEW_TYPE_DISC_HEADER = 0xA00C
|
||||
/** "Music playback" notification code */
|
||||
|
|
|
@ -25,12 +25,15 @@ import android.view.View
|
|||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import androidx.recyclerview.widget.ConcatAdapter
|
||||
import androidx.recyclerview.widget.LinearSmoothScroller
|
||||
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.recycler.AlbumDetailAdapter
|
||||
import org.oxycblt.auxio.detail.header.AlbumDetailHeaderAdapter
|
||||
import org.oxycblt.auxio.detail.list.AlbumDetailListAdapter
|
||||
import org.oxycblt.auxio.detail.list.DetailListAdapter
|
||||
import org.oxycblt.auxio.list.Item
|
||||
import org.oxycblt.auxio.list.ListFragment
|
||||
import org.oxycblt.auxio.list.Sort
|
||||
|
@ -49,10 +52,15 @@ import org.oxycblt.auxio.util.*
|
|||
* A [ListFragment] that shows information about an [Album].
|
||||
*
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*
|
||||
* TODO: Split up list and header adapters, and then work from there. Header item works fine. Make
|
||||
* sure that other pos-dependent code functions
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class AlbumDetailFragment :
|
||||
ListFragment<Song, FragmentDetailBinding>(), AlbumDetailAdapter.Listener {
|
||||
ListFragment<Song, FragmentDetailBinding>(),
|
||||
AlbumDetailHeaderAdapter.Listener,
|
||||
DetailListAdapter.Listener<Song> {
|
||||
private val detailModel: DetailViewModel by activityViewModels()
|
||||
override val navModel: NavigationViewModel by activityViewModels()
|
||||
override val playbackModel: PlaybackViewModel by activityViewModels()
|
||||
|
@ -60,7 +68,8 @@ class AlbumDetailFragment :
|
|||
// Information about what album to display is initially within the navigation arguments
|
||||
// as a UID, as that is the only safe way to parcel an album.
|
||||
private val args: AlbumDetailFragmentArgs by navArgs()
|
||||
private val detailAdapter = AlbumDetailAdapter(this)
|
||||
private val albumHeaderAdapter = AlbumDetailHeaderAdapter(this)
|
||||
private val albumListAdapter = AlbumDetailListAdapter(this)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
@ -87,7 +96,7 @@ class AlbumDetailFragment :
|
|||
setOnMenuItemClickListener(this@AlbumDetailFragment)
|
||||
}
|
||||
|
||||
binding.detailRecycler.adapter = detailAdapter
|
||||
binding.detailRecycler.adapter = ConcatAdapter(albumHeaderAdapter, albumListAdapter)
|
||||
|
||||
// -- VIEWMODEL SETUP ---
|
||||
// DetailViewModel handles most initialization from the navigation argument.
|
||||
|
@ -185,14 +194,15 @@ class AlbumDetailFragment :
|
|||
return
|
||||
}
|
||||
requireBinding().detailToolbar.title = album.resolveName(requireContext())
|
||||
albumHeaderAdapter.setParent(album)
|
||||
}
|
||||
|
||||
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
|
||||
if (parent is Album && parent == unlikelyToBeNull(detailModel.currentAlbum.value)) {
|
||||
detailAdapter.setPlaying(song, isPlaying)
|
||||
albumListAdapter.setPlaying(song, isPlaying)
|
||||
} else {
|
||||
// Clear the ViewHolders if the mode isn't ALL_SONGS
|
||||
detailAdapter.setPlaying(null, isPlaying)
|
||||
albumListAdapter.setPlaying(null, isPlaying)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -277,11 +287,11 @@ class AlbumDetailFragment :
|
|||
}
|
||||
|
||||
private fun updateList(list: List<Item>) {
|
||||
detailAdapter.update(list, detailModel.albumInstructions.consume())
|
||||
albumListAdapter.update(list, detailModel.albumInstructions.consume())
|
||||
}
|
||||
|
||||
private fun updateSelection(selected: List<Music>) {
|
||||
detailAdapter.setSelected(selected.toSet())
|
||||
albumListAdapter.setSelected(selected.toSet())
|
||||
requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,12 +25,15 @@ import android.view.View
|
|||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import androidx.recyclerview.widget.ConcatAdapter
|
||||
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.recycler.ArtistDetailAdapter
|
||||
import org.oxycblt.auxio.detail.recycler.DetailAdapter
|
||||
import org.oxycblt.auxio.detail.header.ArtistDetailHeaderAdapter
|
||||
import org.oxycblt.auxio.detail.header.DetailHeaderAdapter
|
||||
import org.oxycblt.auxio.detail.list.ArtistDetailListAdapter
|
||||
import org.oxycblt.auxio.detail.list.DetailListAdapter
|
||||
import org.oxycblt.auxio.list.Item
|
||||
import org.oxycblt.auxio.list.ListFragment
|
||||
import org.oxycblt.auxio.list.Sort
|
||||
|
@ -51,7 +54,9 @@ import org.oxycblt.auxio.util.*
|
|||
*/
|
||||
@AndroidEntryPoint
|
||||
class ArtistDetailFragment :
|
||||
ListFragment<Music, FragmentDetailBinding>(), DetailAdapter.Listener<Music> {
|
||||
ListFragment<Music, FragmentDetailBinding>(),
|
||||
DetailHeaderAdapter.Listener,
|
||||
DetailListAdapter.Listener<Music> {
|
||||
private val detailModel: DetailViewModel by activityViewModels()
|
||||
override val navModel: NavigationViewModel by activityViewModels()
|
||||
override val playbackModel: PlaybackViewModel by activityViewModels()
|
||||
|
@ -59,7 +64,8 @@ class ArtistDetailFragment :
|
|||
// Information about what artist to display is initially within the navigation arguments
|
||||
// as a UID, as that is the only safe way to parcel an artist.
|
||||
private val args: ArtistDetailFragmentArgs by navArgs()
|
||||
private val detailAdapter = ArtistDetailAdapter(this)
|
||||
private val artistHeaderAdapter = ArtistDetailHeaderAdapter(this)
|
||||
private val artistListAdapter = ArtistDetailListAdapter(this)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
@ -86,7 +92,7 @@ class ArtistDetailFragment :
|
|||
setOnMenuItemClickListener(this@ArtistDetailFragment)
|
||||
}
|
||||
|
||||
binding.detailRecycler.adapter = detailAdapter
|
||||
binding.detailRecycler.adapter = ConcatAdapter(artistHeaderAdapter, artistListAdapter)
|
||||
|
||||
// --- VIEWMODEL SETUP ---
|
||||
// DetailViewModel handles most initialization from the navigation argument.
|
||||
|
@ -194,8 +200,8 @@ class ArtistDetailFragment :
|
|||
findNavController().navigateUp()
|
||||
return
|
||||
}
|
||||
|
||||
requireBinding().detailToolbar.title = artist.resolveName(requireContext())
|
||||
artistHeaderAdapter.setParent(artist)
|
||||
}
|
||||
|
||||
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
|
||||
|
@ -210,7 +216,7 @@ class ArtistDetailFragment :
|
|||
else -> null
|
||||
}
|
||||
|
||||
detailAdapter.setPlaying(playingItem, isPlaying)
|
||||
artistListAdapter.setPlaying(playingItem, isPlaying)
|
||||
}
|
||||
|
||||
private fun handleNavigation(item: Music?) {
|
||||
|
@ -249,11 +255,11 @@ class ArtistDetailFragment :
|
|||
}
|
||||
|
||||
private fun updateList(list: List<Item>) {
|
||||
detailAdapter.update(list, detailModel.artistInstructions.consume())
|
||||
artistListAdapter.update(list, detailModel.artistInstructions.consume())
|
||||
}
|
||||
|
||||
private fun updateSelection(selected: List<Music>) {
|
||||
detailAdapter.setSelected(selected.toSet())
|
||||
artistListAdapter.setSelected(selected.toSet())
|
||||
requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ import kotlinx.coroutines.flow.StateFlow
|
|||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.yield
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.detail.recycler.SortHeader
|
||||
import org.oxycblt.auxio.detail.list.SortHeader
|
||||
import org.oxycblt.auxio.list.BasicHeader
|
||||
import org.oxycblt.auxio.list.Item
|
||||
import org.oxycblt.auxio.list.Sort
|
||||
|
@ -270,7 +270,7 @@ constructor(
|
|||
|
||||
private fun refreshAlbumList(album: Album, replace: Boolean = false) {
|
||||
logD("Refreshing album data")
|
||||
val list = mutableListOf<Item>(album)
|
||||
val list = mutableListOf<Item>()
|
||||
list.add(SortHeader(R.string.lbl_songs))
|
||||
val instructions =
|
||||
if (replace) {
|
||||
|
@ -302,7 +302,7 @@ constructor(
|
|||
|
||||
private fun refreshArtistList(artist: Artist, replace: Boolean = false) {
|
||||
logD("Refreshing artist data")
|
||||
val list = mutableListOf<Item>(artist)
|
||||
val list = mutableListOf<Item>()
|
||||
val albums = Sort(Sort.Mode.ByDate, Sort.Direction.DESCENDING).albums(artist.albums)
|
||||
|
||||
val byReleaseGroup =
|
||||
|
@ -351,7 +351,7 @@ constructor(
|
|||
|
||||
private fun refreshGenreList(genre: Genre, replace: Boolean = false) {
|
||||
logD("Refreshing genre data")
|
||||
val list = mutableListOf<Item>(genre)
|
||||
val list = mutableListOf<Item>()
|
||||
// Genre is guaranteed to always have artists and songs.
|
||||
list.add(BasicHeader(R.string.lbl_artists))
|
||||
list.addAll(genre.artists)
|
||||
|
|
|
@ -25,12 +25,15 @@ import android.view.View
|
|||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import androidx.recyclerview.widget.ConcatAdapter
|
||||
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.recycler.DetailAdapter
|
||||
import org.oxycblt.auxio.detail.recycler.GenreDetailAdapter
|
||||
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.list.Item
|
||||
import org.oxycblt.auxio.list.ListFragment
|
||||
import org.oxycblt.auxio.list.Sort
|
||||
|
@ -52,7 +55,9 @@ import org.oxycblt.auxio.util.*
|
|||
*/
|
||||
@AndroidEntryPoint
|
||||
class GenreDetailFragment :
|
||||
ListFragment<Music, FragmentDetailBinding>(), DetailAdapter.Listener<Music> {
|
||||
ListFragment<Music, FragmentDetailBinding>(),
|
||||
DetailHeaderAdapter.Listener,
|
||||
DetailListAdapter.Listener<Music> {
|
||||
private val detailModel: DetailViewModel by activityViewModels()
|
||||
override val navModel: NavigationViewModel by activityViewModels()
|
||||
override val playbackModel: PlaybackViewModel by activityViewModels()
|
||||
|
@ -60,7 +65,8 @@ class GenreDetailFragment :
|
|||
// 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 detailAdapter = GenreDetailAdapter(this)
|
||||
private val genreHeaderAdapter = GenreDetailHeaderAdapter(this)
|
||||
private val genreListAdapter = GenreDetailListAdapter(this)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
@ -85,7 +91,7 @@ class GenreDetailFragment :
|
|||
setOnMenuItemClickListener(this@GenreDetailFragment)
|
||||
}
|
||||
|
||||
binding.detailRecycler.adapter = detailAdapter
|
||||
binding.detailRecycler.adapter = ConcatAdapter(genreHeaderAdapter, genreListAdapter)
|
||||
|
||||
// --- VIEWMODEL SETUP ---
|
||||
// DetailViewModel handles most initialization from the navigation argument.
|
||||
|
@ -191,8 +197,8 @@ class GenreDetailFragment :
|
|||
findNavController().navigateUp()
|
||||
return
|
||||
}
|
||||
|
||||
requireBinding().detailToolbar.title = genre.resolveName(requireContext())
|
||||
genreHeaderAdapter.setParent(genre)
|
||||
}
|
||||
|
||||
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
|
||||
|
@ -204,7 +210,7 @@ class GenreDetailFragment :
|
|||
if (parent is Genre && parent.uid == unlikelyToBeNull(detailModel.currentGenre.value).uid) {
|
||||
playingMusic = song
|
||||
}
|
||||
detailAdapter.setPlaying(playingMusic, isPlaying)
|
||||
genreListAdapter.setPlaying(playingMusic, isPlaying)
|
||||
}
|
||||
|
||||
private fun handleNavigation(item: Music?) {
|
||||
|
@ -232,11 +238,11 @@ class GenreDetailFragment :
|
|||
}
|
||||
|
||||
private fun updateList(list: List<Item>) {
|
||||
detailAdapter.update(list, detailModel.genreInstructions.consume())
|
||||
genreListAdapter.update(list, detailModel.genreInstructions.consume())
|
||||
}
|
||||
|
||||
private fun updateSelection(selected: List<Music>) {
|
||||
detailAdapter.setSelected(selected.toSet())
|
||||
genreListAdapter.setSelected(selected.toSet())
|
||||
requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,8 +29,8 @@ import androidx.navigation.fragment.navArgs
|
|||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.DialogSongDetailBinding
|
||||
import org.oxycblt.auxio.detail.recycler.SongProperty
|
||||
import org.oxycblt.auxio.detail.recycler.SongPropertyAdapter
|
||||
import org.oxycblt.auxio.detail.list.SongProperty
|
||||
import org.oxycblt.auxio.detail.list.SongPropertyAdapter
|
||||
import org.oxycblt.auxio.list.adapter.UpdateInstructions
|
||||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.music.Song
|
||||
|
|
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Auxio Project
|
||||
* AlbumDetailHeaderAdapter.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.recyclerview.widget.RecyclerView
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.ItemDetailHeaderBinding
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.resolveNames
|
||||
import org.oxycblt.auxio.playback.formatDurationMs
|
||||
import org.oxycblt.auxio.util.context
|
||||
import org.oxycblt.auxio.util.getPlural
|
||||
import org.oxycblt.auxio.util.inflater
|
||||
|
||||
/**
|
||||
* A [DetailHeaderAdapter] that shows [Album] information.
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
class AlbumDetailHeaderAdapter(private val listener: Listener) :
|
||||
DetailHeaderAdapter<Album, AlbumDetailHeaderViewHolder>() {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
||||
AlbumDetailHeaderViewHolder.from(parent)
|
||||
|
||||
override fun onBindHeader(holder: AlbumDetailHeaderViewHolder, parent: Album) =
|
||||
holder.bind(parent, listener)
|
||||
|
||||
/** An extended listener for [DetailHeaderAdapter] implementations. */
|
||||
interface Listener : DetailHeaderAdapter.Listener {
|
||||
|
||||
/**
|
||||
* Called when the artist name in the [Album] header was clicked, requesting navigation to
|
||||
* it's parent artist.
|
||||
*/
|
||||
fun onNavigateToParentArtist()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A [RecyclerView.ViewHolder] that displays the [Album] header in the detail view. Use [from] to
|
||||
* create an instance.
|
||||
*
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
class AlbumDetailHeaderViewHolder
|
||||
private constructor(private val binding: ItemDetailHeaderBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
/**
|
||||
* Bind new data to this instance.
|
||||
*
|
||||
* @param album The new [Album] to bind.
|
||||
* @param listener A [AlbumDetailHeaderAdapter.Listener] to bind interactions to.
|
||||
*/
|
||||
fun bind(album: Album, listener: AlbumDetailHeaderAdapter.Listener) {
|
||||
binding.detailCover.bind(album)
|
||||
|
||||
// The type text depends on the release type (Album, EP, Single, etc.)
|
||||
binding.detailType.text = binding.context.getString(album.releaseType.stringRes)
|
||||
|
||||
binding.detailName.text = album.resolveName(binding.context)
|
||||
|
||||
// Artist name maps to the subhead text
|
||||
binding.detailSubhead.apply {
|
||||
text = album.artists.resolveNames(context)
|
||||
|
||||
// Add a QoL behavior where navigation to the artist will occur if the artist
|
||||
// name is pressed.
|
||||
setOnClickListener { listener.onNavigateToParentArtist() }
|
||||
}
|
||||
|
||||
// Date, song count, and duration map to the info text
|
||||
binding.detailInfo.apply {
|
||||
// Fall back to a friendlier "No date" text if the album doesn't have date information
|
||||
val date = album.dates?.resolveDate(context) ?: context.getString(R.string.def_date)
|
||||
val songCount = context.getPlural(R.plurals.fmt_song_count, album.songs.size)
|
||||
val duration = album.durationMs.formatDurationMs(true)
|
||||
text = context.getString(R.string.fmt_three, date, songCount, duration)
|
||||
}
|
||||
|
||||
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) =
|
||||
AlbumDetailHeaderViewHolder(ItemDetailHeaderBinding.inflate(parent.context.inflater))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Auxio Project
|
||||
* ArtistDetailHeaderAdapter.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.Artist
|
||||
import org.oxycblt.auxio.music.resolveNames
|
||||
import org.oxycblt.auxio.util.context
|
||||
import org.oxycblt.auxio.util.getPlural
|
||||
import org.oxycblt.auxio.util.inflater
|
||||
|
||||
/**
|
||||
* A [DetailHeaderAdapter] that shows [Artist] information.
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
class ArtistDetailHeaderAdapter(private val listener: Listener) :
|
||||
DetailHeaderAdapter<Artist, ArtistDetailHeaderViewHolder>() {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
||||
ArtistDetailHeaderViewHolder.from(parent)
|
||||
override fun onBindHeader(holder: ArtistDetailHeaderViewHolder, parent: Artist) =
|
||||
holder.bind(parent, listener)
|
||||
}
|
||||
|
||||
/**
|
||||
* A [RecyclerView.ViewHolder] that displays the [Artist] header in the detail view. Use [from] to
|
||||
* create an instance.
|
||||
*
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
class ArtistDetailHeaderViewHolder
|
||||
private constructor(private val binding: ItemDetailHeaderBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
/**
|
||||
* Bind new data to this instance.
|
||||
*
|
||||
* @param artist The new [Artist] to bind.
|
||||
* @param listener A [DetailHeaderAdapter.Listener] to bind interactions to.
|
||||
*/
|
||||
fun bind(artist: Artist, listener: DetailHeaderAdapter.Listener) {
|
||||
binding.detailCover.bind(artist)
|
||||
binding.detailType.text = binding.context.getString(R.string.lbl_artist)
|
||||
binding.detailName.text = artist.resolveName(binding.context)
|
||||
|
||||
if (artist.songs.isNotEmpty()) {
|
||||
// Information about the artist's genre(s) map to the sub-head text
|
||||
binding.detailSubhead.apply {
|
||||
isVisible = true
|
||||
text = artist.genres.resolveNames(context)
|
||||
}
|
||||
|
||||
// Song and album counts map to the info
|
||||
binding.detailInfo.text =
|
||||
binding.context.getString(
|
||||
R.string.fmt_two,
|
||||
binding.context.getPlural(R.plurals.fmt_album_count, artist.albums.size),
|
||||
binding.context.getPlural(R.plurals.fmt_song_count, artist.songs.size))
|
||||
|
||||
// In the case that this header used to he configured to have no songs,
|
||||
// we want to reset the visibility of all information that was hidden.
|
||||
binding.detailPlayButton.isVisible = true
|
||||
binding.detailShuffleButton.isVisible = true
|
||||
} else {
|
||||
// The artist does not have any songs, so hide functionality that makes no sense.
|
||||
// ex. Play and Shuffle, Song Counts, and Genre Information.
|
||||
// Artists are always guaranteed to have albums however, so continue to show those.
|
||||
binding.detailSubhead.isVisible = false
|
||||
binding.detailInfo.text =
|
||||
binding.context.getPlural(R.plurals.fmt_album_count, artist.albums.size)
|
||||
binding.detailPlayButton.isVisible = false
|
||||
binding.detailShuffleButton.isVisible = false
|
||||
}
|
||||
|
||||
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) =
|
||||
ArtistDetailHeaderViewHolder(ItemDetailHeaderBinding.inflate(parent.context.inflater))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Auxio Project
|
||||
* DetailHeaderAdapter.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 androidx.recyclerview.widget.RecyclerView
|
||||
import org.oxycblt.auxio.music.MusicParent
|
||||
|
||||
/**
|
||||
* A [RecyclerView.Adapter] that implements shared behavior between each parent header view.
|
||||
*
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
abstract class DetailHeaderAdapter<T : MusicParent, VH : RecyclerView.ViewHolder> :
|
||||
RecyclerView.Adapter<VH>() {
|
||||
private var currentParent: T? = null
|
||||
final override fun getItemCount() = 1
|
||||
final override fun onBindViewHolder(holder: VH, position: Int) =
|
||||
onBindHeader(holder, requireNotNull(currentParent))
|
||||
|
||||
/**
|
||||
* Bind the created header [RecyclerView.ViewHolder] with the current [parent].
|
||||
* @param holder The [RecyclerView.ViewHolder] to bind.
|
||||
* @param parent The current [MusicParent] to bind.
|
||||
*/
|
||||
abstract fun onBindHeader(holder: VH, parent: T)
|
||||
|
||||
/**
|
||||
* Update the [MusicParent] shown in the header.
|
||||
* @param parent The new [MusicParent] to show.
|
||||
*/
|
||||
fun setParent(parent: T) {
|
||||
currentParent = parent
|
||||
notifyItemChanged(0, PAYLOAD_UPDATE_HEADER)
|
||||
}
|
||||
|
||||
/** An extended listener for [DetailHeaderAdapter] implementations. */
|
||||
interface Listener {
|
||||
/**
|
||||
* Called when the play button in a detail header is pressed, requesting that the current
|
||||
* item should be played.
|
||||
*/
|
||||
fun onPlay()
|
||||
|
||||
/**
|
||||
* Called when the shuffle button in a detail header is pressed, requesting that the current
|
||||
* item should be shuffled
|
||||
*/
|
||||
fun onShuffle()
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val PAYLOAD_UPDATE_HEADER = Any()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* 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.detail.list.DetailListAdapter
|
||||
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.
|
||||
* @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 [DetailListAdapter.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.resolveName(binding.context)
|
||||
// Nothing about a genre is applicable to the sub-head text.
|
||||
binding.detailSubhead.isVisible = false
|
||||
// The song 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))
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* Copyright (c) 2021 Auxio Project
|
||||
* AlbumDetailAdapter.kt is part of Auxio.
|
||||
* AlbumDetailListAdapter.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
|
||||
|
@ -16,7 +16,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.detail.recycler
|
||||
package org.oxycblt.auxio.detail.list
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
|
@ -26,7 +26,6 @@ import androidx.recyclerview.widget.RecyclerView
|
|||
import org.oxycblt.auxio.IntegerTable
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.ItemAlbumSongBinding
|
||||
import org.oxycblt.auxio.databinding.ItemDetailBinding
|
||||
import org.oxycblt.auxio.databinding.ItemDiscHeaderBinding
|
||||
import org.oxycblt.auxio.list.Item
|
||||
import org.oxycblt.auxio.list.SelectableListListener
|
||||
|
@ -34,37 +33,22 @@ import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter
|
|||
import org.oxycblt.auxio.list.adapter.SimpleDiffCallback
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.music.areRawNamesTheSame
|
||||
import org.oxycblt.auxio.music.metadata.Disc
|
||||
import org.oxycblt.auxio.music.resolveNames
|
||||
import org.oxycblt.auxio.playback.formatDurationMs
|
||||
import org.oxycblt.auxio.util.context
|
||||
import org.oxycblt.auxio.util.getPlural
|
||||
import org.oxycblt.auxio.util.inflater
|
||||
|
||||
/**
|
||||
* An [DetailAdapter] implementing the header and sub-items for the [Album] detail view.
|
||||
* An [DetailListAdapter] implementing the header and sub-items for the [Album] detail view.
|
||||
*
|
||||
* @param listener A [Listener] to bind interactions to.
|
||||
* @param listener A [DetailListAdapter.Listener] to bind interactions to.
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
class AlbumDetailAdapter(private val listener: Listener) : DetailAdapter(listener, DIFF_CALLBACK) {
|
||||
/**
|
||||
* An extension to [DetailAdapter.Listener] that enables interactions specific to the album
|
||||
* detail view.
|
||||
*/
|
||||
interface Listener : DetailAdapter.Listener<Song> {
|
||||
/**
|
||||
* Called when the artist name in the [Album] header was clicked, requesting navigation to
|
||||
* it's parent artist.
|
||||
*/
|
||||
fun onNavigateToParentArtist()
|
||||
}
|
||||
|
||||
class AlbumDetailListAdapter(private val listener: Listener<Song>) :
|
||||
DetailListAdapter(listener, DIFF_CALLBACK) {
|
||||
override fun getItemViewType(position: Int) =
|
||||
when (getItem(position)) {
|
||||
// Support the Album header, sub-headers for each disc, and special album songs.
|
||||
is Album -> AlbumDetailViewHolder.VIEW_TYPE
|
||||
// Support sub-headers for each disc, and special album songs.
|
||||
is Disc -> DiscViewHolder.VIEW_TYPE
|
||||
is Song -> AlbumSongViewHolder.VIEW_TYPE
|
||||
else -> super.getItemViewType(position)
|
||||
|
@ -72,7 +56,6 @@ class AlbumDetailAdapter(private val listener: Listener) : DetailAdapter(listene
|
|||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
||||
when (viewType) {
|
||||
AlbumDetailViewHolder.VIEW_TYPE -> AlbumDetailViewHolder.from(parent)
|
||||
DiscViewHolder.VIEW_TYPE -> DiscViewHolder.from(parent)
|
||||
AlbumSongViewHolder.VIEW_TYPE -> AlbumSongViewHolder.from(parent)
|
||||
else -> super.onCreateViewHolder(parent, viewType)
|
||||
|
@ -81,7 +64,6 @@ class AlbumDetailAdapter(private val listener: Listener) : DetailAdapter(listene
|
|||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
super.onBindViewHolder(holder, position)
|
||||
when (val item = getItem(position)) {
|
||||
is Album -> (holder as AlbumDetailViewHolder).bind(item, listener)
|
||||
is Disc -> (holder as DiscViewHolder).bind(item)
|
||||
is Song -> (holder as AlbumSongViewHolder).bind(item, listener)
|
||||
}
|
||||
|
@ -100,91 +82,16 @@ class AlbumDetailAdapter(private val listener: Listener) : DetailAdapter(listene
|
|||
/** A comparator that can be used with DiffUtil. */
|
||||
val DIFF_CALLBACK =
|
||||
object : SimpleDiffCallback<Item>() {
|
||||
override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
|
||||
return when {
|
||||
oldItem is Album && newItem is Album ->
|
||||
AlbumDetailViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem)
|
||||
override fun areContentsTheSame(oldItem: Item, newItem: Item) =
|
||||
when {
|
||||
oldItem is Disc && newItem is Disc ->
|
||||
DiscViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem)
|
||||
oldItem is Song && newItem is Song ->
|
||||
AlbumSongViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem)
|
||||
|
||||
// Fall back to DetailAdapter's differ to handle other headers.
|
||||
else -> DetailAdapter.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem)
|
||||
else -> DetailListAdapter.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A [RecyclerView.ViewHolder] that displays the [Album] header in the detail view. Use [from] to
|
||||
* create an instance.
|
||||
*
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
private class AlbumDetailViewHolder private constructor(private val binding: ItemDetailBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
/**
|
||||
* Bind new data to this instance.
|
||||
*
|
||||
* @param album The new [Album] to bind.
|
||||
* @param listener A [AlbumDetailAdapter.Listener] to bind interactions to.
|
||||
*/
|
||||
fun bind(album: Album, listener: AlbumDetailAdapter.Listener) {
|
||||
binding.detailCover.bind(album)
|
||||
|
||||
// The type text depends on the release type (Album, EP, Single, etc.)
|
||||
binding.detailType.text = binding.context.getString(album.releaseType.stringRes)
|
||||
|
||||
binding.detailName.text = album.resolveName(binding.context)
|
||||
|
||||
// Artist name maps to the subhead text
|
||||
binding.detailSubhead.apply {
|
||||
text = album.artists.resolveNames(context)
|
||||
|
||||
// Add a QoL behavior where navigation to the artist will occur if the artist
|
||||
// name is pressed.
|
||||
setOnClickListener { listener.onNavigateToParentArtist() }
|
||||
}
|
||||
|
||||
// Date, song count, and duration map to the info text
|
||||
binding.detailInfo.apply {
|
||||
// Fall back to a friendlier "No date" text if the album doesn't have date information
|
||||
val date = album.dates?.resolveDate(context) ?: context.getString(R.string.def_date)
|
||||
val songCount = context.getPlural(R.plurals.fmt_song_count, album.songs.size)
|
||||
val duration = album.durationMs.formatDurationMs(true)
|
||||
text = context.getString(R.string.fmt_three, date, songCount, duration)
|
||||
}
|
||||
|
||||
binding.detailPlayButton.setOnClickListener { listener.onPlay() }
|
||||
binding.detailShuffleButton.setOnClickListener { listener.onShuffle() }
|
||||
}
|
||||
|
||||
companion object {
|
||||
/** A unique ID for this [RecyclerView.ViewHolder] type. */
|
||||
const val VIEW_TYPE = IntegerTable.VIEW_TYPE_ALBUM_DETAIL
|
||||
|
||||
/**
|
||||
* Create a new instance.
|
||||
*
|
||||
* @param parent The parent to inflate this instance from.
|
||||
* @return A new instance.
|
||||
*/
|
||||
fun from(parent: View) =
|
||||
AlbumDetailViewHolder(ItemDetailBinding.inflate(parent.context.inflater))
|
||||
|
||||
/** A comparator that can be used with DiffUtil. */
|
||||
val DIFF_CALLBACK =
|
||||
object : SimpleDiffCallback<Album>() {
|
||||
override fun areContentsTheSame(oldItem: Album, newItem: Album) =
|
||||
oldItem.rawName == newItem.rawName &&
|
||||
oldItem.artists.areRawNamesTheSame(newItem.artists) &&
|
||||
oldItem.dates == newItem.dates &&
|
||||
oldItem.songs.size == newItem.songs.size &&
|
||||
oldItem.durationMs == newItem.durationMs &&
|
||||
oldItem.releaseType == newItem.releaseType
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* Copyright (c) 2021 Auxio Project
|
||||
* ArtistDetailAdapter.kt is part of Auxio.
|
||||
* ArtistDetailListAdapter.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
|
||||
|
@ -16,15 +16,13 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.detail.recycler
|
||||
package org.oxycblt.auxio.detail.list
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.oxycblt.auxio.IntegerTable
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.ItemDetailBinding
|
||||
import org.oxycblt.auxio.databinding.ItemParentBinding
|
||||
import org.oxycblt.auxio.databinding.ItemSongBinding
|
||||
import org.oxycblt.auxio.list.Item
|
||||
|
@ -33,21 +31,19 @@ import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter
|
|||
import org.oxycblt.auxio.list.adapter.SimpleDiffCallback
|
||||
import org.oxycblt.auxio.music.*
|
||||
import org.oxycblt.auxio.util.context
|
||||
import org.oxycblt.auxio.util.getPlural
|
||||
import org.oxycblt.auxio.util.inflater
|
||||
|
||||
/**
|
||||
* A [DetailAdapter] implementing the header and sub-items for the [Artist] detail view.
|
||||
* A [DetailListAdapter] implementing the header and sub-items for the [Artist] detail view.
|
||||
*
|
||||
* @param listener A [DetailAdapter.Listener] to bind interactions to.
|
||||
* @param listener A [DetailListAdapter.Listener] to bind interactions to.
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
class ArtistDetailAdapter(private val listener: Listener<Music>) :
|
||||
DetailAdapter(listener, DIFF_CALLBACK) {
|
||||
class ArtistDetailListAdapter(private val listener: Listener<Music>) :
|
||||
DetailListAdapter(listener, DIFF_CALLBACK) {
|
||||
override fun getItemViewType(position: Int) =
|
||||
when (getItem(position)) {
|
||||
// Support an artist header, and special artist albums/songs.
|
||||
is Artist -> ArtistDetailViewHolder.VIEW_TYPE
|
||||
// Support a special artist albums/songs.
|
||||
is Album -> ArtistAlbumViewHolder.VIEW_TYPE
|
||||
is Song -> ArtistSongViewHolder.VIEW_TYPE
|
||||
else -> super.getItemViewType(position)
|
||||
|
@ -55,7 +51,6 @@ class ArtistDetailAdapter(private val listener: Listener<Music>) :
|
|||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
||||
when (viewType) {
|
||||
ArtistDetailViewHolder.VIEW_TYPE -> ArtistDetailViewHolder.from(parent)
|
||||
ArtistAlbumViewHolder.VIEW_TYPE -> ArtistAlbumViewHolder.from(parent)
|
||||
ArtistSongViewHolder.VIEW_TYPE -> ArtistSongViewHolder.from(parent)
|
||||
else -> super.onCreateViewHolder(parent, viewType)
|
||||
|
@ -65,7 +60,6 @@ class ArtistDetailAdapter(private val listener: Listener<Music>) :
|
|||
super.onBindViewHolder(holder, position)
|
||||
// Re-binding an item with new data and not just a changed selection/playing state.
|
||||
when (val item = getItem(position)) {
|
||||
is Artist -> (holder as ArtistDetailViewHolder).bind(item, listener)
|
||||
is Album -> (holder as ArtistAlbumViewHolder).bind(item, listener)
|
||||
is Song -> (holder as ArtistSongViewHolder).bind(item, listener)
|
||||
}
|
||||
|
@ -83,96 +77,14 @@ class ArtistDetailAdapter(private val listener: Listener<Music>) :
|
|||
/** A comparator that can be used with DiffUtil. */
|
||||
val DIFF_CALLBACK =
|
||||
object : SimpleDiffCallback<Item>() {
|
||||
override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
|
||||
return when {
|
||||
oldItem is Artist && newItem is Artist ->
|
||||
ArtistDetailViewHolder.DIFF_CALLBACK.areContentsTheSame(
|
||||
oldItem, newItem)
|
||||
override fun areContentsTheSame(oldItem: Item, newItem: Item) =
|
||||
when {
|
||||
oldItem is Album && newItem is Album ->
|
||||
ArtistAlbumViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem)
|
||||
oldItem is Song && newItem is Song ->
|
||||
ArtistSongViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem)
|
||||
else -> DetailAdapter.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem)
|
||||
else -> DetailListAdapter.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A [RecyclerView.ViewHolder] that displays the [Artist] header in the detail view. Use [from] to
|
||||
* create an instance.
|
||||
*
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
private class ArtistDetailViewHolder private constructor(private val binding: ItemDetailBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
/**
|
||||
* Bind new data to this instance.
|
||||
*
|
||||
* @param artist The new [Artist] to bind.
|
||||
* @param listener A [DetailAdapter.Listener] to bind interactions to.
|
||||
*/
|
||||
fun bind(artist: Artist, listener: DetailAdapter.Listener<*>) {
|
||||
binding.detailCover.bind(artist)
|
||||
binding.detailType.text = binding.context.getString(R.string.lbl_artist)
|
||||
binding.detailName.text = artist.resolveName(binding.context)
|
||||
|
||||
if (artist.songs.isNotEmpty()) {
|
||||
// Information about the artist's genre(s) map to the sub-head text
|
||||
binding.detailSubhead.apply {
|
||||
isVisible = true
|
||||
text = artist.genres.resolveNames(context)
|
||||
}
|
||||
|
||||
// Song and album counts map to the info
|
||||
binding.detailInfo.text =
|
||||
binding.context.getString(
|
||||
R.string.fmt_two,
|
||||
binding.context.getPlural(R.plurals.fmt_album_count, artist.albums.size),
|
||||
binding.context.getPlural(R.plurals.fmt_song_count, artist.songs.size))
|
||||
|
||||
// In the case that this header used to he configured to have no songs,
|
||||
// we want to reset the visibility of all information that was hidden.
|
||||
binding.detailPlayButton.isVisible = true
|
||||
binding.detailShuffleButton.isVisible = true
|
||||
} else {
|
||||
// The artist does not have any songs, so hide functionality that makes no sense.
|
||||
// ex. Play and Shuffle, Song Counts, and Genre Information.
|
||||
// Artists are always guaranteed to have albums however, so continue to show those.
|
||||
binding.detailSubhead.isVisible = false
|
||||
binding.detailInfo.text =
|
||||
binding.context.getPlural(R.plurals.fmt_album_count, artist.albums.size)
|
||||
binding.detailPlayButton.isVisible = false
|
||||
binding.detailShuffleButton.isVisible = false
|
||||
}
|
||||
|
||||
binding.detailPlayButton.setOnClickListener { listener.onPlay() }
|
||||
binding.detailShuffleButton.setOnClickListener { listener.onShuffle() }
|
||||
}
|
||||
|
||||
companion object {
|
||||
/** A unique ID for this [RecyclerView.ViewHolder] type. */
|
||||
const val VIEW_TYPE = IntegerTable.VIEW_TYPE_ARTIST_DETAIL
|
||||
|
||||
/**
|
||||
* Create a new instance.
|
||||
*
|
||||
* @param parent The parent to inflate this instance from.
|
||||
* @return A new instance.
|
||||
*/
|
||||
fun from(parent: View) =
|
||||
ArtistDetailViewHolder(ItemDetailBinding.inflate(parent.context.inflater))
|
||||
|
||||
/** A comparator that can be used with DiffUtil. */
|
||||
val DIFF_CALLBACK =
|
||||
object : SimpleDiffCallback<Artist>() {
|
||||
override fun areContentsTheSame(oldItem: Artist, newItem: Artist) =
|
||||
oldItem.rawName == newItem.rawName &&
|
||||
oldItem.genres.areRawNamesTheSame(newItem.genres) &&
|
||||
oldItem.albums.size == newItem.albums.size &&
|
||||
oldItem.songs.size == newItem.songs.size
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* Copyright (c) 2022 Auxio Project
|
||||
* DetailAdapter.kt is part of Auxio.
|
||||
* DetailListAdapter.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
|
||||
|
@ -16,7 +16,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.detail.recycler
|
||||
package org.oxycblt.auxio.detail.list
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
|
@ -37,13 +37,14 @@ import org.oxycblt.auxio.util.context
|
|||
import org.oxycblt.auxio.util.inflater
|
||||
|
||||
/**
|
||||
* A [RecyclerView.Adapter] that implements behavior shared across each detail view's adapters.
|
||||
* A [RecyclerView.Adapter] that implements shared behavior between lists of child items in the
|
||||
* detail views.
|
||||
*
|
||||
* @param listener A [Listener] to bind interactions to.
|
||||
* @param diffCallback A [DiffUtil.ItemCallback] to compare list updates with.
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
abstract class DetailAdapter(
|
||||
abstract class DetailListAdapter(
|
||||
private val listener: Listener<*>,
|
||||
private val diffCallback: DiffUtil.ItemCallback<Item>
|
||||
) :
|
||||
|
@ -78,21 +79,8 @@ abstract class DetailAdapter(
|
|||
return item is BasicHeader || item is SortHeader
|
||||
}
|
||||
|
||||
/** An extended [SelectableListListener] for [DetailAdapter] implementations. */
|
||||
/** An extended [SelectableListListener] for [DetailListAdapter] implementations. */
|
||||
interface Listener<in T : Music> : SelectableListListener<T> {
|
||||
// TODO: Split off into sub-listeners if a collapsing toolbar is implemented.
|
||||
/**
|
||||
* Called when the play button in a detail header is pressed, requesting that the current
|
||||
* item should be played.
|
||||
*/
|
||||
fun onPlay()
|
||||
|
||||
/**
|
||||
* Called when the shuffle button in a detail header is pressed, requesting that the current
|
||||
* item should be shuffled
|
||||
*/
|
||||
fun onShuffle()
|
||||
|
||||
/**
|
||||
* Called when the button in a [SortHeader] item is pressed, requesting that the sort menu
|
||||
* should be opened.
|
||||
|
@ -137,9 +125,9 @@ private class SortHeaderViewHolder(private val binding: ItemSortHeaderBinding) :
|
|||
* Bind new data to this instance.
|
||||
*
|
||||
* @param sortHeader The new [SortHeader] to bind.
|
||||
* @param listener An [DetailAdapter.Listener] to bind interactions to.
|
||||
* @param listener An [DetailListAdapter.Listener] to bind interactions to.
|
||||
*/
|
||||
fun bind(sortHeader: SortHeader, listener: DetailAdapter.Listener<*>) {
|
||||
fun bind(sortHeader: SortHeader, listener: DetailListAdapter.Listener<*>) {
|
||||
binding.headerTitle.text = binding.context.getString(sortHeader.titleRes)
|
||||
binding.headerButton.apply {
|
||||
// Add a Tooltip based on the content description so that the purpose of this
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* Copyright (c) 2021 Auxio Project
|
||||
* GenreDetailListAdapter.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.list
|
||||
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.oxycblt.auxio.list.Item
|
||||
import org.oxycblt.auxio.list.adapter.SimpleDiffCallback
|
||||
import org.oxycblt.auxio.list.recycler.ArtistViewHolder
|
||||
import org.oxycblt.auxio.list.recycler.SongViewHolder
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.Genre
|
||||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.music.Song
|
||||
|
||||
/**
|
||||
* An [DetailListAdapter] implementing the header and sub-items for the [Genre] detail view.
|
||||
*
|
||||
* @param listener A [DetailListAdapter.Listener] to bind interactions to.
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
class GenreDetailListAdapter(private val listener: Listener<Music>) :
|
||||
DetailListAdapter(listener, DIFF_CALLBACK) {
|
||||
override fun getItemViewType(position: Int) =
|
||||
when (getItem(position)) {
|
||||
// Support generic Artist/Song items.
|
||||
is Artist -> ArtistViewHolder.VIEW_TYPE
|
||||
is Song -> SongViewHolder.VIEW_TYPE
|
||||
else -> super.getItemViewType(position)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
||||
when (viewType) {
|
||||
ArtistViewHolder.VIEW_TYPE -> ArtistViewHolder.from(parent)
|
||||
SongViewHolder.VIEW_TYPE -> SongViewHolder.from(parent)
|
||||
else -> super.onCreateViewHolder(parent, viewType)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
super.onBindViewHolder(holder, position)
|
||||
when (val item = getItem(position)) {
|
||||
is Artist -> (holder as ArtistViewHolder).bind(item, listener)
|
||||
is Song -> (holder as SongViewHolder).bind(item, listener)
|
||||
}
|
||||
}
|
||||
|
||||
override fun isItemFullWidth(position: Int): Boolean {
|
||||
if (super.isItemFullWidth(position)) {
|
||||
return true
|
||||
}
|
||||
// Genre headers should be full-width in all configurations
|
||||
return getItem(position) is Genre
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val DIFF_CALLBACK =
|
||||
object : SimpleDiffCallback<Item>() {
|
||||
override fun areContentsTheSame(oldItem: Item, newItem: Item) =
|
||||
when {
|
||||
oldItem is Artist && newItem is Artist ->
|
||||
ArtistViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem)
|
||||
oldItem is Song && newItem is Song ->
|
||||
SongViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem)
|
||||
else -> DetailListAdapter.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,7 +16,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.detail.recycler
|
||||
package org.oxycblt.auxio.detail.list
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
|
@ -1,154 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2021 Auxio Project
|
||||
* GenreDetailAdapter.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.recycler
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.oxycblt.auxio.IntegerTable
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.ItemDetailBinding
|
||||
import org.oxycblt.auxio.list.Item
|
||||
import org.oxycblt.auxio.list.adapter.SimpleDiffCallback
|
||||
import org.oxycblt.auxio.list.recycler.ArtistViewHolder
|
||||
import org.oxycblt.auxio.list.recycler.SongViewHolder
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.Genre
|
||||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.util.context
|
||||
import org.oxycblt.auxio.util.getPlural
|
||||
import org.oxycblt.auxio.util.inflater
|
||||
|
||||
/**
|
||||
* An [DetailAdapter] implementing the header and sub-items for the [Genre] detail view.
|
||||
*
|
||||
* @param listener A [DetailAdapter.Listener] to bind interactions to.
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
class GenreDetailAdapter(private val listener: Listener<Music>) :
|
||||
DetailAdapter(listener, DIFF_CALLBACK) {
|
||||
override fun getItemViewType(position: Int) =
|
||||
when (getItem(position)) {
|
||||
// Support the Genre header and generic Artist/Song items. There's nothing about
|
||||
// a genre that will make the artists/songs specially formatted, so it doesn't matter
|
||||
// what we use for their ViewHolders.
|
||||
is Genre -> GenreDetailViewHolder.VIEW_TYPE
|
||||
is Artist -> ArtistViewHolder.VIEW_TYPE
|
||||
is Song -> SongViewHolder.VIEW_TYPE
|
||||
else -> super.getItemViewType(position)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
||||
when (viewType) {
|
||||
GenreDetailViewHolder.VIEW_TYPE -> GenreDetailViewHolder.from(parent)
|
||||
ArtistViewHolder.VIEW_TYPE -> ArtistViewHolder.from(parent)
|
||||
SongViewHolder.VIEW_TYPE -> SongViewHolder.from(parent)
|
||||
else -> super.onCreateViewHolder(parent, viewType)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
super.onBindViewHolder(holder, position)
|
||||
when (val item = getItem(position)) {
|
||||
is Genre -> (holder as GenreDetailViewHolder).bind(item, listener)
|
||||
is Artist -> (holder as ArtistViewHolder).bind(item, listener)
|
||||
is Song -> (holder as SongViewHolder).bind(item, listener)
|
||||
}
|
||||
}
|
||||
|
||||
override fun isItemFullWidth(position: Int): Boolean {
|
||||
if (super.isItemFullWidth(position)) {
|
||||
return true
|
||||
}
|
||||
// Genre headers should be full-width in all configurations
|
||||
return getItem(position) is Genre
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val DIFF_CALLBACK =
|
||||
object : SimpleDiffCallback<Item>() {
|
||||
override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
|
||||
return when {
|
||||
oldItem is Genre && newItem is Genre ->
|
||||
GenreDetailViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem)
|
||||
oldItem is Artist && newItem is Artist ->
|
||||
ArtistViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem)
|
||||
oldItem is Song && newItem is Song ->
|
||||
SongViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem)
|
||||
else -> DetailAdapter.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A [RecyclerView.ViewHolder] that displays the [Genre] header in the detail view. Use [from] to
|
||||
* create an instance.
|
||||
*
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
private class GenreDetailViewHolder private constructor(private val binding: ItemDetailBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
/**
|
||||
* Bind new data to this instance.
|
||||
*
|
||||
* @param genre The new [Song] to bind.
|
||||
* @param listener A [DetailAdapter.Listener] to bind interactions to.
|
||||
*/
|
||||
fun bind(genre: Genre, listener: DetailAdapter.Listener<*>) {
|
||||
binding.detailCover.bind(genre)
|
||||
binding.detailType.text = binding.context.getString(R.string.lbl_genre)
|
||||
binding.detailName.text = genre.resolveName(binding.context)
|
||||
// Nothing about a genre is applicable to the sub-head text.
|
||||
binding.detailSubhead.isVisible = false
|
||||
// The song 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 {
|
||||
/** A unique ID for this [RecyclerView.ViewHolder] type. */
|
||||
const val VIEW_TYPE = IntegerTable.VIEW_TYPE_GENRE_DETAIL
|
||||
|
||||
/**
|
||||
* Create a new instance.
|
||||
*
|
||||
* @param parent The parent to inflate this instance from.
|
||||
* @return A new instance.
|
||||
*/
|
||||
fun from(parent: View) =
|
||||
GenreDetailViewHolder(ItemDetailBinding.inflate(parent.context.inflater))
|
||||
|
||||
/** A comparator that can be used with DiffUtil. */
|
||||
val DIFF_CALLBACK =
|
||||
object : SimpleDiffCallback<Genre>() {
|
||||
override fun areContentsTheSame(oldItem: Genre, newItem: Genre) =
|
||||
oldItem.rawName == newItem.rawName &&
|
||||
oldItem.songs.size == newItem.songs.size &&
|
||||
oldItem.durationMs == newItem.durationMs
|
||||
}
|
||||
}
|
||||
}
|
|
@ -34,7 +34,6 @@ import org.oxycblt.auxio.image.extractor.*
|
|||
@InstallIn(SingletonComponent::class)
|
||||
interface ImageModule {
|
||||
@Binds fun settings(imageSettings: ImageSettingsImpl): ImageSettings
|
||||
@Binds fun coverExtractor(coverExtractor: CoverExtractorImpl): CoverExtractor
|
||||
}
|
||||
|
||||
@Module
|
||||
|
|
|
@ -38,33 +38,15 @@ import org.oxycblt.auxio.music.Album
|
|||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.util.logW
|
||||
|
||||
/**
|
||||
* Stateless interface for loading [Album] cover image data.
|
||||
*
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
interface CoverExtractor {
|
||||
/**
|
||||
* Fetch an album cover, respecting the current cover configuration.
|
||||
*
|
||||
* @param context [Context] required to load the image.
|
||||
* @param imageSettings [ImageSettings] required to obtain configuration information.
|
||||
* @param album [Album] to load the cover from.
|
||||
* @return An [InputStream] of image data if the cover loading was successful, null if the cover
|
||||
* loading failed or should not occur.
|
||||
*/
|
||||
suspend fun extract(album: Album): InputStream?
|
||||
}
|
||||
|
||||
class CoverExtractorImpl
|
||||
class CoverExtractor
|
||||
@Inject
|
||||
constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val imageSettings: ImageSettings,
|
||||
private val mediaSourceFactory: MediaSource.Factory
|
||||
) : CoverExtractor {
|
||||
) {
|
||||
|
||||
override suspend fun extract(album: Album): InputStream? =
|
||||
suspend fun extract(album: Album): InputStream? =
|
||||
try {
|
||||
when (imageSettings.coverMode) {
|
||||
CoverMode.OFF -> null
|
||||
|
|
|
@ -35,6 +35,7 @@ import org.oxycblt.auxio.util.logD
|
|||
abstract class FlexibleListAdapter<T, VH : RecyclerView.ViewHolder>(
|
||||
diffCallback: DiffUtil.ItemCallback<T>
|
||||
) : RecyclerView.Adapter<VH>() {
|
||||
@Suppress("LeakingThis")
|
||||
private val differ = FlexibleListDiffer(this, diffCallback)
|
||||
final override fun getItemCount() = differ.currentList.size
|
||||
/** The current list stored by the adapter's differ instance. */
|
||||
|
@ -55,9 +56,7 @@ abstract class FlexibleListAdapter<T, VH : RecyclerView.ViewHolder>(
|
|||
instructions: UpdateInstructions?,
|
||||
callback: (() -> Unit)? = null
|
||||
) =
|
||||
differ.update(newData, instructions, callback).also {
|
||||
logD("Update delivered: $instructions" + "")
|
||||
}
|
||||
differ.update(newData, instructions, callback)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -20,6 +20,7 @@ package org.oxycblt.auxio.list.recycler
|
|||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import androidx.recyclerview.widget.ConcatAdapter
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.divider.BackportMaterialDividerItemDecoration
|
||||
|
@ -41,12 +42,26 @@ constructor(
|
|||
defStyleAttr: Int = R.attr.materialDividerStyle,
|
||||
orientation: Int = LinearLayoutManager.VERTICAL
|
||||
) : BackportMaterialDividerItemDecoration(context, attributeSet, defStyleAttr, orientation) {
|
||||
override fun shouldDrawDivider(position: Int, adapter: RecyclerView.Adapter<*>?) =
|
||||
override fun shouldDrawDivider(position: Int, adapter: RecyclerView.Adapter<*>?): Boolean {
|
||||
if (adapter is ConcatAdapter) {
|
||||
val adapterAndPosition =
|
||||
try {
|
||||
adapter.getWrappedAdapterAndPosition(position + 1)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
return false
|
||||
}
|
||||
return hasHeaderAtPosition(adapterAndPosition.second, adapterAndPosition.first)
|
||||
} else {
|
||||
return hasHeaderAtPosition(position + 1, adapter)
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasHeaderAtPosition(position: Int, adapter: RecyclerView.Adapter<*>?) =
|
||||
try {
|
||||
// Add a divider if the next item is a header. This organizes the divider to separate
|
||||
// the ends of content rather than the beginning of content, alongside an added benefit
|
||||
// of preventing top headers from having a divider applied.
|
||||
(adapter as FlexibleListAdapter<*, *>).getItem(position + 1) is Header
|
||||
(adapter as FlexibleListAdapter<*, *>).getItem(position) is Header
|
||||
} catch (e: ClassCastException) {
|
||||
false
|
||||
} catch (e: IndexOutOfBoundsException) {
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:alpha="0.12" android:color="?attr/colorOnSurface" android:state_enabled="false" />
|
||||
<item android:color="?attr/colorOnPrimary" android:state_checked="true" />
|
||||
<item android:color="?attr/colorSurfaceVariant" />
|
||||
</selector>
|
|
@ -1,6 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:alpha="0.38" android:color="?attr/colorOnSurface" android:state_enabled="false" />
|
||||
<item android:color="?attr/colorPrimary" android:state_checked="true" />
|
||||
<item android:color="?attr/colorOnSurfaceVariant" />
|
||||
</selector>
|
|
@ -1,15 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:bottom="4dp"
|
||||
android:left="4dp"
|
||||
android:right="4dp"
|
||||
android:top="4dp">
|
||||
<shape android:shape="oval">
|
||||
<solid android:color="#000000" />
|
||||
<size
|
||||
android:width="20dp"
|
||||
android:height="20dp" />
|
||||
</shape>
|
||||
</item>
|
||||
</layer-list>
|
|
@ -1,12 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="#000000" />
|
||||
<corners android:radius="56dp" />
|
||||
<size
|
||||
android:width="64dp"
|
||||
android:height="28dp" />
|
||||
</shape>
|
||||
</item>
|
||||
</layer-list>
|
|
@ -35,6 +35,6 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
|
||||
tools:listitem="@layout/item_detail" />
|
||||
tools:listitem="@layout/item_detail_header" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
@ -226,7 +226,7 @@
|
|||
<string name="fmt_db_pos">+%.1f dB</string>
|
||||
<string name="fmt_bitrate">%d kbps</string>
|
||||
<string name="fmt_sample_rate">%d Hz</string>
|
||||
<string name="fmt_indexing">Cargando a túa biblioteca de música... (%1$d/%2$d)</string>
|
||||
<string name="fmt_indexing">Cargando a túa biblioteca de música… (%1$d/%2$d)</string>
|
||||
<string name="fmt_lib_song_count">Cancións cargadas: %d</string>
|
||||
<string name="fmt_lib_album_count">Álbums cargados: %d</string>
|
||||
<string name="fmt_lib_artist_count">Artistas cargados: %d</string>
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
<item name="textAppearanceHeadlineSmall">@style/TextAppearance.Auxio.HeadlineSmall</item>
|
||||
|
||||
<item name="textAppearanceTitleLarge">@style/TextAppearance.Auxio.TitleLarge</item>
|
||||
<item name="textAppearanceTitleMedium">@style/TextAppearance.Auxio.TitleMediumLowEmphasis
|
||||
<item name="textAppearanceTitleMedium">@style/TextAppearance.Auxio.TitleMedium
|
||||
</item>
|
||||
<item name="textAppearanceTitleSmall">@style/TextAppearance.Auxio.TitleSmall</item>
|
||||
|
||||
|
|
|
@ -72,13 +72,6 @@
|
|||
</style>
|
||||
|
||||
<style name="TextAppearance.Auxio.TitleMedium" parent="TextAppearance.Material3.TitleMedium">
|
||||
<item name="fontFamily">@font/inter_semibold</item>
|
||||
<item name="android:fontFamily">@font/inter_semibold</item>
|
||||
<item name="android:textStyle">bold</item>
|
||||
<item name="android:letterSpacing">-0.00825</item>
|
||||
</style>
|
||||
|
||||
<style name="TextAppearance.Auxio.TitleMediumLowEmphasis" parent="TextAppearance.Material3.TitleMedium">
|
||||
<item name="fontFamily">@font/inter_regular</item>
|
||||
<item name="android:fontFamily">@font/inter_regular</item>
|
||||
<item name="android:textStyle">normal</item>
|
||||
|
|
Loading…
Reference in a new issue