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:
Alexander Capehart 2023-03-19 12:02:29 -06:00
parent f57b5dfc81
commit 9cc75a0e11
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
33 changed files with 570 additions and 490 deletions

View file

@ -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 */

View file

@ -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)
} }
} }

View file

@ -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)
} }
} }

View file

@ -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)

View file

@ -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)
} }
} }

View file

@ -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

View file

@ -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))
}
}

View file

@ -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))
}
}

View file

@ -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()
}
}

View file

@ -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))
}
}

View file

@ -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,91 +82,16 @@ 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
} }
} }
} }

View file

@ -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,96 +77,14 @@ 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
} }
} }
} }

View file

@ -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

View file

@ -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)
}
}
}
}

View file

@ -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

View file

@ -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
}
}
}

View file

@ -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

View file

@ -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

View file

@ -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" + "")
}
} }
/** /**

View file

@ -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) {

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>