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
|
const val VIEW_TYPE_BASIC_HEADER = 0xA004
|
||||||
/** SortHeaderViewHolder */
|
/** SortHeaderViewHolder */
|
||||||
const val VIEW_TYPE_SORT_HEADER = 0xA005
|
const val VIEW_TYPE_SORT_HEADER = 0xA005
|
||||||
/** AlbumDetailViewHolder */
|
|
||||||
const val VIEW_TYPE_ALBUM_DETAIL = 0xA006
|
|
||||||
/** AlbumSongViewHolder */
|
/** AlbumSongViewHolder */
|
||||||
const val VIEW_TYPE_ALBUM_SONG = 0xA007
|
const val VIEW_TYPE_ALBUM_SONG = 0xA007
|
||||||
/** ArtistDetailViewHolder */
|
|
||||||
const val VIEW_TYPE_ARTIST_DETAIL = 0xA008
|
|
||||||
/** ArtistAlbumViewHolder */
|
/** ArtistAlbumViewHolder */
|
||||||
const val VIEW_TYPE_ARTIST_ALBUM = 0xA009
|
const val VIEW_TYPE_ARTIST_ALBUM = 0xA009
|
||||||
/** ArtistSongViewHolder */
|
/** ArtistSongViewHolder */
|
||||||
const val VIEW_TYPE_ARTIST_SONG = 0xA00A
|
const val VIEW_TYPE_ARTIST_SONG = 0xA00A
|
||||||
/** GenreDetailViewHolder */
|
|
||||||
const val VIEW_TYPE_GENRE_DETAIL = 0xA00B
|
|
||||||
/** DiscHeaderViewHolder */
|
/** DiscHeaderViewHolder */
|
||||||
const val VIEW_TYPE_DISC_HEADER = 0xA00C
|
const val VIEW_TYPE_DISC_HEADER = 0xA00C
|
||||||
/** "Music playback" notification code */
|
/** "Music playback" notification code */
|
||||||
|
|
|
@ -25,12 +25,15 @@ import android.view.View
|
||||||
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.LinearSmoothScroller
|
import androidx.recyclerview.widget.LinearSmoothScroller
|
||||||
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.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.Item
|
||||||
import org.oxycblt.auxio.list.ListFragment
|
import org.oxycblt.auxio.list.ListFragment
|
||||||
import org.oxycblt.auxio.list.Sort
|
import org.oxycblt.auxio.list.Sort
|
||||||
|
@ -49,10 +52,15 @@ import org.oxycblt.auxio.util.*
|
||||||
* A [ListFragment] that shows information about an [Album].
|
* A [ListFragment] that shows information about an [Album].
|
||||||
*
|
*
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @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
|
@AndroidEntryPoint
|
||||||
class AlbumDetailFragment :
|
class AlbumDetailFragment :
|
||||||
ListFragment<Song, FragmentDetailBinding>(), AlbumDetailAdapter.Listener {
|
ListFragment<Song, FragmentDetailBinding>(),
|
||||||
|
AlbumDetailHeaderAdapter.Listener,
|
||||||
|
DetailListAdapter.Listener<Song> {
|
||||||
private val detailModel: DetailViewModel by activityViewModels()
|
private val detailModel: DetailViewModel by activityViewModels()
|
||||||
override val navModel: NavigationViewModel by activityViewModels()
|
override val navModel: NavigationViewModel by activityViewModels()
|
||||||
override val playbackModel: PlaybackViewModel 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
|
// 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.
|
// as a UID, as that is the only safe way to parcel an album.
|
||||||
private val args: AlbumDetailFragmentArgs by navArgs()
|
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?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
@ -87,7 +96,7 @@ class AlbumDetailFragment :
|
||||||
setOnMenuItemClickListener(this@AlbumDetailFragment)
|
setOnMenuItemClickListener(this@AlbumDetailFragment)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.detailRecycler.adapter = detailAdapter
|
binding.detailRecycler.adapter = ConcatAdapter(albumHeaderAdapter, albumListAdapter)
|
||||||
|
|
||||||
// -- VIEWMODEL SETUP ---
|
// -- VIEWMODEL SETUP ---
|
||||||
// DetailViewModel handles most initialization from the navigation argument.
|
// DetailViewModel handles most initialization from the navigation argument.
|
||||||
|
@ -185,14 +194,15 @@ class AlbumDetailFragment :
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
requireBinding().detailToolbar.title = album.resolveName(requireContext())
|
requireBinding().detailToolbar.title = album.resolveName(requireContext())
|
||||||
|
albumHeaderAdapter.setParent(album)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
|
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
|
||||||
if (parent is Album && parent == unlikelyToBeNull(detailModel.currentAlbum.value)) {
|
if (parent is Album && parent == unlikelyToBeNull(detailModel.currentAlbum.value)) {
|
||||||
detailAdapter.setPlaying(song, isPlaying)
|
albumListAdapter.setPlaying(song, isPlaying)
|
||||||
} else {
|
} else {
|
||||||
// Clear the ViewHolders if the mode isn't ALL_SONGS
|
// 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>) {
|
private fun updateList(list: List<Item>) {
|
||||||
detailAdapter.update(list, detailModel.albumInstructions.consume())
|
albumListAdapter.update(list, detailModel.albumInstructions.consume())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateSelection(selected: List<Music>) {
|
private fun updateSelection(selected: List<Music>) {
|
||||||
detailAdapter.setSelected(selected.toSet())
|
albumListAdapter.setSelected(selected.toSet())
|
||||||
requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size)
|
requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,12 +25,15 @@ import android.view.View
|
||||||
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 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.FragmentDetailBinding
|
||||||
import org.oxycblt.auxio.detail.recycler.ArtistDetailAdapter
|
import org.oxycblt.auxio.detail.header.ArtistDetailHeaderAdapter
|
||||||
import org.oxycblt.auxio.detail.recycler.DetailAdapter
|
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.Item
|
||||||
import org.oxycblt.auxio.list.ListFragment
|
import org.oxycblt.auxio.list.ListFragment
|
||||||
import org.oxycblt.auxio.list.Sort
|
import org.oxycblt.auxio.list.Sort
|
||||||
|
@ -51,7 +54,9 @@ import org.oxycblt.auxio.util.*
|
||||||
*/
|
*/
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class ArtistDetailFragment :
|
class ArtistDetailFragment :
|
||||||
ListFragment<Music, FragmentDetailBinding>(), DetailAdapter.Listener<Music> {
|
ListFragment<Music, FragmentDetailBinding>(),
|
||||||
|
DetailHeaderAdapter.Listener,
|
||||||
|
DetailListAdapter.Listener<Music> {
|
||||||
private val detailModel: DetailViewModel by activityViewModels()
|
private val detailModel: DetailViewModel by activityViewModels()
|
||||||
override val navModel: NavigationViewModel by activityViewModels()
|
override val navModel: NavigationViewModel by activityViewModels()
|
||||||
override val playbackModel: PlaybackViewModel 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
|
// 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.
|
// as a UID, as that is the only safe way to parcel an artist.
|
||||||
private val args: ArtistDetailFragmentArgs by navArgs()
|
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?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
@ -86,7 +92,7 @@ class ArtistDetailFragment :
|
||||||
setOnMenuItemClickListener(this@ArtistDetailFragment)
|
setOnMenuItemClickListener(this@ArtistDetailFragment)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.detailRecycler.adapter = detailAdapter
|
binding.detailRecycler.adapter = ConcatAdapter(artistHeaderAdapter, artistListAdapter)
|
||||||
|
|
||||||
// --- VIEWMODEL SETUP ---
|
// --- VIEWMODEL SETUP ---
|
||||||
// DetailViewModel handles most initialization from the navigation argument.
|
// DetailViewModel handles most initialization from the navigation argument.
|
||||||
|
@ -194,8 +200,8 @@ class ArtistDetailFragment :
|
||||||
findNavController().navigateUp()
|
findNavController().navigateUp()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
requireBinding().detailToolbar.title = artist.resolveName(requireContext())
|
requireBinding().detailToolbar.title = artist.resolveName(requireContext())
|
||||||
|
artistHeaderAdapter.setParent(artist)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
|
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
|
||||||
|
@ -210,7 +216,7 @@ class ArtistDetailFragment :
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
detailAdapter.setPlaying(playingItem, isPlaying)
|
artistListAdapter.setPlaying(playingItem, isPlaying)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleNavigation(item: Music?) {
|
private fun handleNavigation(item: Music?) {
|
||||||
|
@ -249,11 +255,11 @@ class ArtistDetailFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateList(list: List<Item>) {
|
private fun updateList(list: List<Item>) {
|
||||||
detailAdapter.update(list, detailModel.artistInstructions.consume())
|
artistListAdapter.update(list, detailModel.artistInstructions.consume())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateSelection(selected: List<Music>) {
|
private fun updateSelection(selected: List<Music>) {
|
||||||
detailAdapter.setSelected(selected.toSet())
|
artistListAdapter.setSelected(selected.toSet())
|
||||||
requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size)
|
requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,7 @@ import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.yield
|
import kotlinx.coroutines.yield
|
||||||
import org.oxycblt.auxio.R
|
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.BasicHeader
|
||||||
import org.oxycblt.auxio.list.Item
|
import org.oxycblt.auxio.list.Item
|
||||||
import org.oxycblt.auxio.list.Sort
|
import org.oxycblt.auxio.list.Sort
|
||||||
|
@ -270,7 +270,7 @@ constructor(
|
||||||
|
|
||||||
private fun refreshAlbumList(album: Album, replace: Boolean = false) {
|
private fun refreshAlbumList(album: Album, replace: Boolean = false) {
|
||||||
logD("Refreshing album data")
|
logD("Refreshing album data")
|
||||||
val list = mutableListOf<Item>(album)
|
val list = mutableListOf<Item>()
|
||||||
list.add(SortHeader(R.string.lbl_songs))
|
list.add(SortHeader(R.string.lbl_songs))
|
||||||
val instructions =
|
val instructions =
|
||||||
if (replace) {
|
if (replace) {
|
||||||
|
@ -302,7 +302,7 @@ constructor(
|
||||||
|
|
||||||
private fun refreshArtistList(artist: Artist, replace: Boolean = false) {
|
private fun refreshArtistList(artist: Artist, replace: Boolean = false) {
|
||||||
logD("Refreshing artist data")
|
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 albums = Sort(Sort.Mode.ByDate, Sort.Direction.DESCENDING).albums(artist.albums)
|
||||||
|
|
||||||
val byReleaseGroup =
|
val byReleaseGroup =
|
||||||
|
@ -351,7 +351,7 @@ constructor(
|
||||||
|
|
||||||
private fun refreshGenreList(genre: Genre, replace: Boolean = false) {
|
private fun refreshGenreList(genre: Genre, replace: Boolean = false) {
|
||||||
logD("Refreshing genre data")
|
logD("Refreshing genre data")
|
||||||
val list = mutableListOf<Item>(genre)
|
val list = mutableListOf<Item>()
|
||||||
// Genre is guaranteed to always have artists and songs.
|
// Genre is guaranteed to always have artists and songs.
|
||||||
list.add(BasicHeader(R.string.lbl_artists))
|
list.add(BasicHeader(R.string.lbl_artists))
|
||||||
list.addAll(genre.artists)
|
list.addAll(genre.artists)
|
||||||
|
|
|
@ -25,12 +25,15 @@ import android.view.View
|
||||||
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 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.FragmentDetailBinding
|
||||||
import org.oxycblt.auxio.detail.recycler.DetailAdapter
|
import org.oxycblt.auxio.detail.header.DetailHeaderAdapter
|
||||||
import org.oxycblt.auxio.detail.recycler.GenreDetailAdapter
|
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.Item
|
||||||
import org.oxycblt.auxio.list.ListFragment
|
import org.oxycblt.auxio.list.ListFragment
|
||||||
import org.oxycblt.auxio.list.Sort
|
import org.oxycblt.auxio.list.Sort
|
||||||
|
@ -52,7 +55,9 @@ import org.oxycblt.auxio.util.*
|
||||||
*/
|
*/
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class GenreDetailFragment :
|
class GenreDetailFragment :
|
||||||
ListFragment<Music, FragmentDetailBinding>(), DetailAdapter.Listener<Music> {
|
ListFragment<Music, FragmentDetailBinding>(),
|
||||||
|
DetailHeaderAdapter.Listener,
|
||||||
|
DetailListAdapter.Listener<Music> {
|
||||||
private val detailModel: DetailViewModel by activityViewModels()
|
private val detailModel: DetailViewModel by activityViewModels()
|
||||||
override val navModel: NavigationViewModel by activityViewModels()
|
override val navModel: NavigationViewModel by activityViewModels()
|
||||||
override val playbackModel: PlaybackViewModel 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
|
// 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 detailAdapter = GenreDetailAdapter(this)
|
private val genreHeaderAdapter = GenreDetailHeaderAdapter(this)
|
||||||
|
private val genreListAdapter = GenreDetailListAdapter(this)
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
@ -85,7 +91,7 @@ class GenreDetailFragment :
|
||||||
setOnMenuItemClickListener(this@GenreDetailFragment)
|
setOnMenuItemClickListener(this@GenreDetailFragment)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.detailRecycler.adapter = detailAdapter
|
binding.detailRecycler.adapter = ConcatAdapter(genreHeaderAdapter, genreListAdapter)
|
||||||
|
|
||||||
// --- VIEWMODEL SETUP ---
|
// --- VIEWMODEL SETUP ---
|
||||||
// DetailViewModel handles most initialization from the navigation argument.
|
// DetailViewModel handles most initialization from the navigation argument.
|
||||||
|
@ -191,8 +197,8 @@ class GenreDetailFragment :
|
||||||
findNavController().navigateUp()
|
findNavController().navigateUp()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
requireBinding().detailToolbar.title = genre.resolveName(requireContext())
|
requireBinding().detailToolbar.title = genre.resolveName(requireContext())
|
||||||
|
genreHeaderAdapter.setParent(genre)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
|
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) {
|
if (parent is Genre && parent.uid == unlikelyToBeNull(detailModel.currentGenre.value).uid) {
|
||||||
playingMusic = song
|
playingMusic = song
|
||||||
}
|
}
|
||||||
detailAdapter.setPlaying(playingMusic, isPlaying)
|
genreListAdapter.setPlaying(playingMusic, isPlaying)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleNavigation(item: Music?) {
|
private fun handleNavigation(item: Music?) {
|
||||||
|
@ -232,11 +238,11 @@ class GenreDetailFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateList(list: List<Item>) {
|
private fun updateList(list: List<Item>) {
|
||||||
detailAdapter.update(list, detailModel.genreInstructions.consume())
|
genreListAdapter.update(list, detailModel.genreInstructions.consume())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateSelection(selected: List<Music>) {
|
private fun updateSelection(selected: List<Music>) {
|
||||||
detailAdapter.setSelected(selected.toSet())
|
genreListAdapter.setSelected(selected.toSet())
|
||||||
requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size)
|
requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,8 +29,8 @@ import androidx.navigation.fragment.navArgs
|
||||||
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.DialogSongDetailBinding
|
import org.oxycblt.auxio.databinding.DialogSongDetailBinding
|
||||||
import org.oxycblt.auxio.detail.recycler.SongProperty
|
import org.oxycblt.auxio.detail.list.SongProperty
|
||||||
import org.oxycblt.auxio.detail.recycler.SongPropertyAdapter
|
import org.oxycblt.auxio.detail.list.SongPropertyAdapter
|
||||||
import org.oxycblt.auxio.list.adapter.UpdateInstructions
|
import org.oxycblt.auxio.list.adapter.UpdateInstructions
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.Song
|
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
|
* 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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -16,7 +16,7 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.detail.recycler
|
package org.oxycblt.auxio.detail.list
|
||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
@ -26,7 +26,6 @@ import androidx.recyclerview.widget.RecyclerView
|
||||||
import org.oxycblt.auxio.IntegerTable
|
import org.oxycblt.auxio.IntegerTable
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.ItemAlbumSongBinding
|
import org.oxycblt.auxio.databinding.ItemAlbumSongBinding
|
||||||
import org.oxycblt.auxio.databinding.ItemDetailBinding
|
|
||||||
import org.oxycblt.auxio.databinding.ItemDiscHeaderBinding
|
import org.oxycblt.auxio.databinding.ItemDiscHeaderBinding
|
||||||
import org.oxycblt.auxio.list.Item
|
import org.oxycblt.auxio.list.Item
|
||||||
import org.oxycblt.auxio.list.SelectableListListener
|
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.list.adapter.SimpleDiffCallback
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.music.areRawNamesTheSame
|
|
||||||
import org.oxycblt.auxio.music.metadata.Disc
|
import org.oxycblt.auxio.music.metadata.Disc
|
||||||
import org.oxycblt.auxio.music.resolveNames
|
|
||||||
import org.oxycblt.auxio.playback.formatDurationMs
|
import org.oxycblt.auxio.playback.formatDurationMs
|
||||||
import org.oxycblt.auxio.util.context
|
import org.oxycblt.auxio.util.context
|
||||||
import org.oxycblt.auxio.util.getPlural
|
|
||||||
import org.oxycblt.auxio.util.inflater
|
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)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
class AlbumDetailAdapter(private val listener: Listener) : DetailAdapter(listener, DIFF_CALLBACK) {
|
class AlbumDetailListAdapter(private val listener: Listener<Song>) :
|
||||||
/**
|
DetailListAdapter(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()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemViewType(position: Int) =
|
override fun getItemViewType(position: Int) =
|
||||||
when (getItem(position)) {
|
when (getItem(position)) {
|
||||||
// Support the Album header, sub-headers for each disc, and special album songs.
|
// Support sub-headers for each disc, and special album songs.
|
||||||
is Album -> AlbumDetailViewHolder.VIEW_TYPE
|
|
||||||
is Disc -> DiscViewHolder.VIEW_TYPE
|
is Disc -> DiscViewHolder.VIEW_TYPE
|
||||||
is Song -> AlbumSongViewHolder.VIEW_TYPE
|
is Song -> AlbumSongViewHolder.VIEW_TYPE
|
||||||
else -> super.getItemViewType(position)
|
else -> super.getItemViewType(position)
|
||||||
|
@ -72,7 +56,6 @@ class AlbumDetailAdapter(private val listener: Listener) : DetailAdapter(listene
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
||||||
when (viewType) {
|
when (viewType) {
|
||||||
AlbumDetailViewHolder.VIEW_TYPE -> AlbumDetailViewHolder.from(parent)
|
|
||||||
DiscViewHolder.VIEW_TYPE -> DiscViewHolder.from(parent)
|
DiscViewHolder.VIEW_TYPE -> DiscViewHolder.from(parent)
|
||||||
AlbumSongViewHolder.VIEW_TYPE -> AlbumSongViewHolder.from(parent)
|
AlbumSongViewHolder.VIEW_TYPE -> AlbumSongViewHolder.from(parent)
|
||||||
else -> super.onCreateViewHolder(parent, viewType)
|
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) {
|
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||||
super.onBindViewHolder(holder, position)
|
super.onBindViewHolder(holder, position)
|
||||||
when (val item = getItem(position)) {
|
when (val item = getItem(position)) {
|
||||||
is Album -> (holder as AlbumDetailViewHolder).bind(item, listener)
|
|
||||||
is Disc -> (holder as DiscViewHolder).bind(item)
|
is Disc -> (holder as DiscViewHolder).bind(item)
|
||||||
is Song -> (holder as AlbumSongViewHolder).bind(item, listener)
|
is Song -> (holder as AlbumSongViewHolder).bind(item, listener)
|
||||||
}
|
}
|
||||||
|
@ -100,93 +82,18 @@ class AlbumDetailAdapter(private val listener: Listener) : DetailAdapter(listene
|
||||||
/** A comparator that can be used with DiffUtil. */
|
/** A comparator that can be used with DiffUtil. */
|
||||||
val DIFF_CALLBACK =
|
val DIFF_CALLBACK =
|
||||||
object : SimpleDiffCallback<Item>() {
|
object : SimpleDiffCallback<Item>() {
|
||||||
override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
|
override fun areContentsTheSame(oldItem: Item, newItem: Item) =
|
||||||
return when {
|
when {
|
||||||
oldItem is Album && newItem is Album ->
|
|
||||||
AlbumDetailViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem)
|
|
||||||
oldItem is Disc && newItem is Disc ->
|
oldItem is Disc && newItem is Disc ->
|
||||||
DiscViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem)
|
DiscViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem)
|
||||||
oldItem is Song && newItem is Song ->
|
oldItem is Song && newItem is Song ->
|
||||||
AlbumSongViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem)
|
AlbumSongViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem)
|
||||||
|
|
||||||
// Fall back to DetailAdapter's differ to handle other headers.
|
// 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
|
* 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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -16,15 +16,13 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.detail.recycler
|
package org.oxycblt.auxio.detail.list
|
||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import org.oxycblt.auxio.IntegerTable
|
import org.oxycblt.auxio.IntegerTable
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.ItemDetailBinding
|
|
||||||
import org.oxycblt.auxio.databinding.ItemParentBinding
|
import org.oxycblt.auxio.databinding.ItemParentBinding
|
||||||
import org.oxycblt.auxio.databinding.ItemSongBinding
|
import org.oxycblt.auxio.databinding.ItemSongBinding
|
||||||
import org.oxycblt.auxio.list.Item
|
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.list.adapter.SimpleDiffCallback
|
||||||
import org.oxycblt.auxio.music.*
|
import org.oxycblt.auxio.music.*
|
||||||
import org.oxycblt.auxio.util.context
|
import org.oxycblt.auxio.util.context
|
||||||
import org.oxycblt.auxio.util.getPlural
|
|
||||||
import org.oxycblt.auxio.util.inflater
|
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)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
class ArtistDetailAdapter(private val listener: Listener<Music>) :
|
class ArtistDetailListAdapter(private val listener: Listener<Music>) :
|
||||||
DetailAdapter(listener, DIFF_CALLBACK) {
|
DetailListAdapter(listener, DIFF_CALLBACK) {
|
||||||
override fun getItemViewType(position: Int) =
|
override fun getItemViewType(position: Int) =
|
||||||
when (getItem(position)) {
|
when (getItem(position)) {
|
||||||
// Support an artist header, and special artist albums/songs.
|
// Support a special artist albums/songs.
|
||||||
is Artist -> ArtistDetailViewHolder.VIEW_TYPE
|
|
||||||
is Album -> ArtistAlbumViewHolder.VIEW_TYPE
|
is Album -> ArtistAlbumViewHolder.VIEW_TYPE
|
||||||
is Song -> ArtistSongViewHolder.VIEW_TYPE
|
is Song -> ArtistSongViewHolder.VIEW_TYPE
|
||||||
else -> super.getItemViewType(position)
|
else -> super.getItemViewType(position)
|
||||||
|
@ -55,7 +51,6 @@ class ArtistDetailAdapter(private val listener: Listener<Music>) :
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
||||||
when (viewType) {
|
when (viewType) {
|
||||||
ArtistDetailViewHolder.VIEW_TYPE -> ArtistDetailViewHolder.from(parent)
|
|
||||||
ArtistAlbumViewHolder.VIEW_TYPE -> ArtistAlbumViewHolder.from(parent)
|
ArtistAlbumViewHolder.VIEW_TYPE -> ArtistAlbumViewHolder.from(parent)
|
||||||
ArtistSongViewHolder.VIEW_TYPE -> ArtistSongViewHolder.from(parent)
|
ArtistSongViewHolder.VIEW_TYPE -> ArtistSongViewHolder.from(parent)
|
||||||
else -> super.onCreateViewHolder(parent, viewType)
|
else -> super.onCreateViewHolder(parent, viewType)
|
||||||
|
@ -65,7 +60,6 @@ class ArtistDetailAdapter(private val listener: Listener<Music>) :
|
||||||
super.onBindViewHolder(holder, position)
|
super.onBindViewHolder(holder, position)
|
||||||
// Re-binding an item with new data and not just a changed selection/playing state.
|
// Re-binding an item with new data and not just a changed selection/playing state.
|
||||||
when (val item = getItem(position)) {
|
when (val item = getItem(position)) {
|
||||||
is Artist -> (holder as ArtistDetailViewHolder).bind(item, listener)
|
|
||||||
is Album -> (holder as ArtistAlbumViewHolder).bind(item, listener)
|
is Album -> (holder as ArtistAlbumViewHolder).bind(item, listener)
|
||||||
is Song -> (holder as ArtistSongViewHolder).bind(item, listener)
|
is Song -> (holder as ArtistSongViewHolder).bind(item, listener)
|
||||||
}
|
}
|
||||||
|
@ -83,98 +77,16 @@ class ArtistDetailAdapter(private val listener: Listener<Music>) :
|
||||||
/** A comparator that can be used with DiffUtil. */
|
/** A comparator that can be used with DiffUtil. */
|
||||||
val DIFF_CALLBACK =
|
val DIFF_CALLBACK =
|
||||||
object : SimpleDiffCallback<Item>() {
|
object : SimpleDiffCallback<Item>() {
|
||||||
override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
|
override fun areContentsTheSame(oldItem: Item, newItem: Item) =
|
||||||
return when {
|
when {
|
||||||
oldItem is Artist && newItem is Artist ->
|
|
||||||
ArtistDetailViewHolder.DIFF_CALLBACK.areContentsTheSame(
|
|
||||||
oldItem, newItem)
|
|
||||||
oldItem is Album && newItem is Album ->
|
oldItem is Album && newItem is Album ->
|
||||||
ArtistAlbumViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem)
|
ArtistAlbumViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem)
|
||||||
oldItem is Song && newItem is Song ->
|
oldItem is Song && newItem is Song ->
|
||||||
ArtistSongViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem)
|
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
|
* 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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -16,7 +16,7 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.detail.recycler
|
package org.oxycblt.auxio.detail.list
|
||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
@ -37,13 +37,14 @@ import org.oxycblt.auxio.util.context
|
||||||
import org.oxycblt.auxio.util.inflater
|
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 listener A [Listener] to bind interactions to.
|
||||||
* @param diffCallback A [DiffUtil.ItemCallback] to compare list updates with.
|
* @param diffCallback A [DiffUtil.ItemCallback] to compare list updates with.
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
abstract class DetailAdapter(
|
abstract class DetailListAdapter(
|
||||||
private val listener: Listener<*>,
|
private val listener: Listener<*>,
|
||||||
private val diffCallback: DiffUtil.ItemCallback<Item>
|
private val diffCallback: DiffUtil.ItemCallback<Item>
|
||||||
) :
|
) :
|
||||||
|
@ -78,21 +79,8 @@ abstract class DetailAdapter(
|
||||||
return item is BasicHeader || item is SortHeader
|
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> {
|
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
|
* Called when the button in a [SortHeader] item is pressed, requesting that the sort menu
|
||||||
* should be opened.
|
* should be opened.
|
||||||
|
@ -137,9 +125,9 @@ private class SortHeaderViewHolder(private val binding: ItemSortHeaderBinding) :
|
||||||
* Bind new data to this instance.
|
* Bind new data to this instance.
|
||||||
*
|
*
|
||||||
* @param sortHeader The new [SortHeader] to bind.
|
* @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.headerTitle.text = binding.context.getString(sortHeader.titleRes)
|
||||||
binding.headerButton.apply {
|
binding.headerButton.apply {
|
||||||
// Add a Tooltip based on the content description so that the purpose of this
|
// 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/>.
|
* 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.View
|
||||||
import android.view.ViewGroup
|
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)
|
@InstallIn(SingletonComponent::class)
|
||||||
interface ImageModule {
|
interface ImageModule {
|
||||||
@Binds fun settings(imageSettings: ImageSettingsImpl): ImageSettings
|
@Binds fun settings(imageSettings: ImageSettingsImpl): ImageSettings
|
||||||
@Binds fun coverExtractor(coverExtractor: CoverExtractorImpl): CoverExtractor
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
|
|
|
@ -38,33 +38,15 @@ import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.logW
|
import org.oxycblt.auxio.util.logW
|
||||||
|
|
||||||
/**
|
class CoverExtractor
|
||||||
* 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
|
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
@ApplicationContext private val context: Context,
|
@ApplicationContext private val context: Context,
|
||||||
private val imageSettings: ImageSettings,
|
private val imageSettings: ImageSettings,
|
||||||
private val mediaSourceFactory: MediaSource.Factory
|
private val mediaSourceFactory: MediaSource.Factory
|
||||||
) : CoverExtractor {
|
) {
|
||||||
|
|
||||||
override suspend fun extract(album: Album): InputStream? =
|
suspend fun extract(album: Album): InputStream? =
|
||||||
try {
|
try {
|
||||||
when (imageSettings.coverMode) {
|
when (imageSettings.coverMode) {
|
||||||
CoverMode.OFF -> null
|
CoverMode.OFF -> null
|
||||||
|
|
|
@ -35,6 +35,7 @@ import org.oxycblt.auxio.util.logD
|
||||||
abstract class FlexibleListAdapter<T, VH : RecyclerView.ViewHolder>(
|
abstract class FlexibleListAdapter<T, VH : RecyclerView.ViewHolder>(
|
||||||
diffCallback: DiffUtil.ItemCallback<T>
|
diffCallback: DiffUtil.ItemCallback<T>
|
||||||
) : RecyclerView.Adapter<VH>() {
|
) : RecyclerView.Adapter<VH>() {
|
||||||
|
@Suppress("LeakingThis")
|
||||||
private val differ = FlexibleListDiffer(this, diffCallback)
|
private val differ = FlexibleListDiffer(this, diffCallback)
|
||||||
final override fun getItemCount() = differ.currentList.size
|
final override fun getItemCount() = differ.currentList.size
|
||||||
/** The current list stored by the adapter's differ instance. */
|
/** The current list stored by the adapter's differ instance. */
|
||||||
|
@ -55,9 +56,7 @@ abstract class FlexibleListAdapter<T, VH : RecyclerView.ViewHolder>(
|
||||||
instructions: UpdateInstructions?,
|
instructions: UpdateInstructions?,
|
||||||
callback: (() -> Unit)? = null
|
callback: (() -> Unit)? = null
|
||||||
) =
|
) =
|
||||||
differ.update(newData, instructions, callback).also {
|
differ.update(newData, instructions, callback)
|
||||||
logD("Update delivered: $instructions" + "")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -20,6 +20,7 @@ package org.oxycblt.auxio.list.recycler
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
|
import androidx.recyclerview.widget.ConcatAdapter
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.google.android.material.divider.BackportMaterialDividerItemDecoration
|
import com.google.android.material.divider.BackportMaterialDividerItemDecoration
|
||||||
|
@ -41,12 +42,26 @@ constructor(
|
||||||
defStyleAttr: Int = R.attr.materialDividerStyle,
|
defStyleAttr: Int = R.attr.materialDividerStyle,
|
||||||
orientation: Int = LinearLayoutManager.VERTICAL
|
orientation: Int = LinearLayoutManager.VERTICAL
|
||||||
) : BackportMaterialDividerItemDecoration(context, attributeSet, defStyleAttr, orientation) {
|
) : 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 {
|
try {
|
||||||
// Add a divider if the next item is a header. This organizes the divider to separate
|
// 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
|
// the ends of content rather than the beginning of content, alongside an added benefit
|
||||||
// of preventing top headers from having a divider applied.
|
// 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) {
|
} catch (e: ClassCastException) {
|
||||||
false
|
false
|
||||||
} catch (e: IndexOutOfBoundsException) {
|
} 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_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
|
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>
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
@ -226,7 +226,7 @@
|
||||||
<string name="fmt_db_pos">+%.1f dB</string>
|
<string name="fmt_db_pos">+%.1f dB</string>
|
||||||
<string name="fmt_bitrate">%d kbps</string>
|
<string name="fmt_bitrate">%d kbps</string>
|
||||||
<string name="fmt_sample_rate">%d Hz</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_song_count">Cancións cargadas: %d</string>
|
||||||
<string name="fmt_lib_album_count">Álbums cargados: %d</string>
|
<string name="fmt_lib_album_count">Álbums cargados: %d</string>
|
||||||
<string name="fmt_lib_artist_count">Artistas 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="textAppearanceHeadlineSmall">@style/TextAppearance.Auxio.HeadlineSmall</item>
|
||||||
|
|
||||||
<item name="textAppearanceTitleLarge">@style/TextAppearance.Auxio.TitleLarge</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>
|
||||||
<item name="textAppearanceTitleSmall">@style/TextAppearance.Auxio.TitleSmall</item>
|
<item name="textAppearanceTitleSmall">@style/TextAppearance.Auxio.TitleSmall</item>
|
||||||
|
|
||||||
|
|
|
@ -72,13 +72,6 @@
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="TextAppearance.Auxio.TitleMedium" parent="TextAppearance.Material3.TitleMedium">
|
<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="fontFamily">@font/inter_regular</item>
|
||||||
<item name="android:fontFamily">@font/inter_regular</item>
|
<item name="android:fontFamily">@font/inter_regular</item>
|
||||||
<item name="android:textStyle">normal</item>
|
<item name="android:textStyle">normal</item>
|
||||||
|
|
Loading…
Reference in a new issue