From 9cc75a0e11f4fb98a33b335c72ce6bd03d61990c Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sun, 19 Mar 2023 12:02:29 -0600 Subject: [PATCH] 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. --- .../java/org/oxycblt/auxio/IntegerTable.kt | 6 - .../auxio/detail/AlbumDetailFragment.kt | 26 ++- .../auxio/detail/ArtistDetailFragment.kt | 24 ++- .../oxycblt/auxio/detail/DetailViewModel.kt | 8 +- .../auxio/detail/GenreDetailFragment.kt | 24 ++- .../oxycblt/auxio/detail/SongDetailDialog.kt | 4 +- .../detail/header/AlbumDetailHeaderAdapter.kt | 112 +++++++++++++ .../header/ArtistDetailHeaderAdapter.kt | 109 +++++++++++++ .../detail/header/DetailHeaderAdapter.kt | 70 ++++++++ .../detail/header/GenreDetailHeaderAdapter.kt | 87 ++++++++++ .../AlbumDetailListAdapter.kt} | 113 ++----------- .../ArtistDetailListAdapter.kt} | 108 ++---------- .../DetailListAdapter.kt} | 28 +--- .../detail/list/GenreDetailListAdapter.kt | 84 ++++++++++ .../{recycler => list}/SongPropertyAdapter.kt | 2 +- .../detail/recycler/GenreDetailAdapter.kt | 154 ------------------ .../org/oxycblt/auxio/image/ImageModule.kt | 1 - .../auxio/image/extractor/CoverExtractor.kt | 24 +-- .../auxio/list/adapter/FlexibleListAdapter.kt | 5 +- .../list/recycler/HeaderItemDecoration.kt | 19 ++- .../main/res/color/sel_m3_switch_thumb.xml | 6 - .../main/res/color/sel_m3_switch_track.xml | 6 - .../main/res/drawable/ui_m3_switch_thumb.xml | 15 -- .../main/res/drawable/ui_m3_switch_track.xml | 12 -- ...item_detail.xml => item_detail_header.xml} | 0 ...item_detail.xml => item_detail_header.xml} | 0 ...item_detail.xml => item_detail_header.xml} | 0 ...item_detail.xml => item_detail_header.xml} | 0 app/src/main/res/layout/fragment_detail.xml | 2 +- ...item_detail.xml => item_detail_header.xml} | 0 app/src/main/res/values-gl/strings.xml | 2 +- app/src/main/res/values/styles_core.xml | 2 +- app/src/main/res/values/typography.xml | 7 - 33 files changed, 570 insertions(+), 490 deletions(-) create mode 100644 app/src/main/java/org/oxycblt/auxio/detail/header/AlbumDetailHeaderAdapter.kt create mode 100644 app/src/main/java/org/oxycblt/auxio/detail/header/ArtistDetailHeaderAdapter.kt create mode 100644 app/src/main/java/org/oxycblt/auxio/detail/header/DetailHeaderAdapter.kt create mode 100644 app/src/main/java/org/oxycblt/auxio/detail/header/GenreDetailHeaderAdapter.kt rename app/src/main/java/org/oxycblt/auxio/detail/{recycler/AlbumDetailAdapter.kt => list/AlbumDetailListAdapter.kt} (62%) rename app/src/main/java/org/oxycblt/auxio/detail/{recycler/ArtistDetailAdapter.kt => list/ArtistDetailListAdapter.kt} (61%) rename app/src/main/java/org/oxycblt/auxio/detail/{recycler/DetailAdapter.kt => list/DetailListAdapter.kt} (86%) create mode 100644 app/src/main/java/org/oxycblt/auxio/detail/list/GenreDetailListAdapter.kt rename app/src/main/java/org/oxycblt/auxio/detail/{recycler => list}/SongPropertyAdapter.kt (98%) delete mode 100644 app/src/main/java/org/oxycblt/auxio/detail/recycler/GenreDetailAdapter.kt delete mode 100644 app/src/main/res/color/sel_m3_switch_thumb.xml delete mode 100644 app/src/main/res/color/sel_m3_switch_track.xml delete mode 100644 app/src/main/res/drawable/ui_m3_switch_thumb.xml delete mode 100644 app/src/main/res/drawable/ui_m3_switch_track.xml rename app/src/main/res/layout-h600dp/{item_detail.xml => item_detail_header.xml} (100%) rename app/src/main/res/layout-land/{item_detail.xml => item_detail_header.xml} (100%) rename app/src/main/res/layout-sw600dp/{item_detail.xml => item_detail_header.xml} (100%) rename app/src/main/res/layout-sw840dp/{item_detail.xml => item_detail_header.xml} (100%) rename app/src/main/res/layout/{item_detail.xml => item_detail_header.xml} (100%) diff --git a/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt b/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt index 3a85161eb..d2d1e933f 100644 --- a/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt +++ b/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt @@ -37,18 +37,12 @@ object IntegerTable { const val VIEW_TYPE_BASIC_HEADER = 0xA004 /** SortHeaderViewHolder */ const val VIEW_TYPE_SORT_HEADER = 0xA005 - /** AlbumDetailViewHolder */ - const val VIEW_TYPE_ALBUM_DETAIL = 0xA006 /** AlbumSongViewHolder */ const val VIEW_TYPE_ALBUM_SONG = 0xA007 - /** ArtistDetailViewHolder */ - const val VIEW_TYPE_ARTIST_DETAIL = 0xA008 /** ArtistAlbumViewHolder */ const val VIEW_TYPE_ARTIST_ALBUM = 0xA009 /** ArtistSongViewHolder */ const val VIEW_TYPE_ARTIST_SONG = 0xA00A - /** GenreDetailViewHolder */ - const val VIEW_TYPE_GENRE_DETAIL = 0xA00B /** DiscHeaderViewHolder */ const val VIEW_TYPE_DISC_HEADER = 0xA00C /** "Music playback" notification code */ diff --git a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt index 17a59ae2a..05b8c534a 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt @@ -25,12 +25,15 @@ import android.view.View import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs +import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.LinearSmoothScroller import com.google.android.material.transition.MaterialSharedAxis import dagger.hilt.android.AndroidEntryPoint import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentDetailBinding -import org.oxycblt.auxio.detail.recycler.AlbumDetailAdapter +import org.oxycblt.auxio.detail.header.AlbumDetailHeaderAdapter +import org.oxycblt.auxio.detail.list.AlbumDetailListAdapter +import org.oxycblt.auxio.detail.list.DetailListAdapter import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.ListFragment import org.oxycblt.auxio.list.Sort @@ -49,10 +52,15 @@ import org.oxycblt.auxio.util.* * A [ListFragment] that shows information about an [Album]. * * @author Alexander Capehart (OxygenCobalt) + * + * TODO: Split up list and header adapters, and then work from there. Header item works fine. Make + * sure that other pos-dependent code functions */ @AndroidEntryPoint class AlbumDetailFragment : - ListFragment(), AlbumDetailAdapter.Listener { + ListFragment(), + AlbumDetailHeaderAdapter.Listener, + DetailListAdapter.Listener { private val detailModel: DetailViewModel by activityViewModels() override val navModel: NavigationViewModel by activityViewModels() override val playbackModel: PlaybackViewModel by activityViewModels() @@ -60,7 +68,8 @@ class AlbumDetailFragment : // Information about what album to display is initially within the navigation arguments // as a UID, as that is the only safe way to parcel an album. private val args: AlbumDetailFragmentArgs by navArgs() - private val detailAdapter = AlbumDetailAdapter(this) + private val albumHeaderAdapter = AlbumDetailHeaderAdapter(this) + private val albumListAdapter = AlbumDetailListAdapter(this) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -87,7 +96,7 @@ class AlbumDetailFragment : setOnMenuItemClickListener(this@AlbumDetailFragment) } - binding.detailRecycler.adapter = detailAdapter + binding.detailRecycler.adapter = ConcatAdapter(albumHeaderAdapter, albumListAdapter) // -- VIEWMODEL SETUP --- // DetailViewModel handles most initialization from the navigation argument. @@ -185,14 +194,15 @@ class AlbumDetailFragment : return } requireBinding().detailToolbar.title = album.resolveName(requireContext()) + albumHeaderAdapter.setParent(album) } private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) { if (parent is Album && parent == unlikelyToBeNull(detailModel.currentAlbum.value)) { - detailAdapter.setPlaying(song, isPlaying) + albumListAdapter.setPlaying(song, isPlaying) } else { // Clear the ViewHolders if the mode isn't ALL_SONGS - detailAdapter.setPlaying(null, isPlaying) + albumListAdapter.setPlaying(null, isPlaying) } } @@ -277,11 +287,11 @@ class AlbumDetailFragment : } private fun updateList(list: List) { - detailAdapter.update(list, detailModel.albumInstructions.consume()) + albumListAdapter.update(list, detailModel.albumInstructions.consume()) } private fun updateSelection(selected: List) { - detailAdapter.setSelected(selected.toSet()) + albumListAdapter.setSelected(selected.toSet()) requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size) } } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt index 140ced37a..eecd5b5bc 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt @@ -25,12 +25,15 @@ import android.view.View import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs +import androidx.recyclerview.widget.ConcatAdapter import com.google.android.material.transition.MaterialSharedAxis import dagger.hilt.android.AndroidEntryPoint import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentDetailBinding -import org.oxycblt.auxio.detail.recycler.ArtistDetailAdapter -import org.oxycblt.auxio.detail.recycler.DetailAdapter +import org.oxycblt.auxio.detail.header.ArtistDetailHeaderAdapter +import org.oxycblt.auxio.detail.header.DetailHeaderAdapter +import org.oxycblt.auxio.detail.list.ArtistDetailListAdapter +import org.oxycblt.auxio.detail.list.DetailListAdapter import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.ListFragment import org.oxycblt.auxio.list.Sort @@ -51,7 +54,9 @@ import org.oxycblt.auxio.util.* */ @AndroidEntryPoint class ArtistDetailFragment : - ListFragment(), DetailAdapter.Listener { + ListFragment(), + DetailHeaderAdapter.Listener, + DetailListAdapter.Listener { private val detailModel: DetailViewModel by activityViewModels() override val navModel: NavigationViewModel by activityViewModels() override val playbackModel: PlaybackViewModel by activityViewModels() @@ -59,7 +64,8 @@ class ArtistDetailFragment : // Information about what artist to display is initially within the navigation arguments // as a UID, as that is the only safe way to parcel an artist. private val args: ArtistDetailFragmentArgs by navArgs() - private val detailAdapter = ArtistDetailAdapter(this) + private val artistHeaderAdapter = ArtistDetailHeaderAdapter(this) + private val artistListAdapter = ArtistDetailListAdapter(this) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -86,7 +92,7 @@ class ArtistDetailFragment : setOnMenuItemClickListener(this@ArtistDetailFragment) } - binding.detailRecycler.adapter = detailAdapter + binding.detailRecycler.adapter = ConcatAdapter(artistHeaderAdapter, artistListAdapter) // --- VIEWMODEL SETUP --- // DetailViewModel handles most initialization from the navigation argument. @@ -194,8 +200,8 @@ class ArtistDetailFragment : findNavController().navigateUp() return } - requireBinding().detailToolbar.title = artist.resolveName(requireContext()) + artistHeaderAdapter.setParent(artist) } private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) { @@ -210,7 +216,7 @@ class ArtistDetailFragment : else -> null } - detailAdapter.setPlaying(playingItem, isPlaying) + artistListAdapter.setPlaying(playingItem, isPlaying) } private fun handleNavigation(item: Music?) { @@ -249,11 +255,11 @@ class ArtistDetailFragment : } private fun updateList(list: List) { - detailAdapter.update(list, detailModel.artistInstructions.consume()) + artistListAdapter.update(list, detailModel.artistInstructions.consume()) } private fun updateSelection(selected: List) { - detailAdapter.setSelected(selected.toSet()) + artistListAdapter.setSelected(selected.toSet()) requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size) } } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt index eca205ecc..20923abac 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt @@ -30,7 +30,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.yield import org.oxycblt.auxio.R -import org.oxycblt.auxio.detail.recycler.SortHeader +import org.oxycblt.auxio.detail.list.SortHeader import org.oxycblt.auxio.list.BasicHeader import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.Sort @@ -270,7 +270,7 @@ constructor( private fun refreshAlbumList(album: Album, replace: Boolean = false) { logD("Refreshing album data") - val list = mutableListOf(album) + val list = mutableListOf() list.add(SortHeader(R.string.lbl_songs)) val instructions = if (replace) { @@ -302,7 +302,7 @@ constructor( private fun refreshArtistList(artist: Artist, replace: Boolean = false) { logD("Refreshing artist data") - val list = mutableListOf(artist) + val list = mutableListOf() val albums = Sort(Sort.Mode.ByDate, Sort.Direction.DESCENDING).albums(artist.albums) val byReleaseGroup = @@ -351,7 +351,7 @@ constructor( private fun refreshGenreList(genre: Genre, replace: Boolean = false) { logD("Refreshing genre data") - val list = mutableListOf(genre) + val list = mutableListOf() // Genre is guaranteed to always have artists and songs. list.add(BasicHeader(R.string.lbl_artists)) list.addAll(genre.artists) diff --git a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt index 62ff03034..555d8549a 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt @@ -25,12 +25,15 @@ import android.view.View import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs +import androidx.recyclerview.widget.ConcatAdapter import com.google.android.material.transition.MaterialSharedAxis import dagger.hilt.android.AndroidEntryPoint import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentDetailBinding -import org.oxycblt.auxio.detail.recycler.DetailAdapter -import org.oxycblt.auxio.detail.recycler.GenreDetailAdapter +import org.oxycblt.auxio.detail.header.DetailHeaderAdapter +import org.oxycblt.auxio.detail.header.GenreDetailHeaderAdapter +import org.oxycblt.auxio.detail.list.DetailListAdapter +import org.oxycblt.auxio.detail.list.GenreDetailListAdapter import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.ListFragment import org.oxycblt.auxio.list.Sort @@ -52,7 +55,9 @@ import org.oxycblt.auxio.util.* */ @AndroidEntryPoint class GenreDetailFragment : - ListFragment(), DetailAdapter.Listener { + ListFragment(), + DetailHeaderAdapter.Listener, + DetailListAdapter.Listener { private val detailModel: DetailViewModel by activityViewModels() override val navModel: NavigationViewModel by activityViewModels() override val playbackModel: PlaybackViewModel by activityViewModels() @@ -60,7 +65,8 @@ class GenreDetailFragment : // Information about what genre to display is initially within the navigation arguments // as a UID, as that is the only safe way to parcel an genre. private val args: GenreDetailFragmentArgs by navArgs() - private val detailAdapter = GenreDetailAdapter(this) + private val genreHeaderAdapter = GenreDetailHeaderAdapter(this) + private val genreListAdapter = GenreDetailListAdapter(this) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -85,7 +91,7 @@ class GenreDetailFragment : setOnMenuItemClickListener(this@GenreDetailFragment) } - binding.detailRecycler.adapter = detailAdapter + binding.detailRecycler.adapter = ConcatAdapter(genreHeaderAdapter, genreListAdapter) // --- VIEWMODEL SETUP --- // DetailViewModel handles most initialization from the navigation argument. @@ -191,8 +197,8 @@ class GenreDetailFragment : findNavController().navigateUp() return } - requireBinding().detailToolbar.title = genre.resolveName(requireContext()) + genreHeaderAdapter.setParent(genre) } private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) { @@ -204,7 +210,7 @@ class GenreDetailFragment : if (parent is Genre && parent.uid == unlikelyToBeNull(detailModel.currentGenre.value).uid) { playingMusic = song } - detailAdapter.setPlaying(playingMusic, isPlaying) + genreListAdapter.setPlaying(playingMusic, isPlaying) } private fun handleNavigation(item: Music?) { @@ -232,11 +238,11 @@ class GenreDetailFragment : } private fun updateList(list: List) { - detailAdapter.update(list, detailModel.genreInstructions.consume()) + genreListAdapter.update(list, detailModel.genreInstructions.consume()) } private fun updateSelection(selected: List) { - detailAdapter.setSelected(selected.toSet()) + genreListAdapter.setSelected(selected.toSet()) requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size) } } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt b/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt index a1990ac3e..a766b6afd 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt @@ -29,8 +29,8 @@ import androidx.navigation.fragment.navArgs import dagger.hilt.android.AndroidEntryPoint import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.DialogSongDetailBinding -import org.oxycblt.auxio.detail.recycler.SongProperty -import org.oxycblt.auxio.detail.recycler.SongPropertyAdapter +import org.oxycblt.auxio.detail.list.SongProperty +import org.oxycblt.auxio.detail.list.SongPropertyAdapter import org.oxycblt.auxio.list.adapter.UpdateInstructions import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Song diff --git a/app/src/main/java/org/oxycblt/auxio/detail/header/AlbumDetailHeaderAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/header/AlbumDetailHeaderAdapter.kt new file mode 100644 index 000000000..423eeffc4 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/detail/header/AlbumDetailHeaderAdapter.kt @@ -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 . + */ + +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() { + 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)) + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/detail/header/ArtistDetailHeaderAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/header/ArtistDetailHeaderAdapter.kt new file mode 100644 index 000000000..6b6f3cc2d --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/detail/header/ArtistDetailHeaderAdapter.kt @@ -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 . + */ + +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() { + 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)) + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/detail/header/DetailHeaderAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/header/DetailHeaderAdapter.kt new file mode 100644 index 000000000..5789b66cd --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/detail/header/DetailHeaderAdapter.kt @@ -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 . + */ + +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 : + RecyclerView.Adapter() { + 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() + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/detail/header/GenreDetailHeaderAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/header/GenreDetailHeaderAdapter.kt new file mode 100644 index 000000000..3340a13b1 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/detail/header/GenreDetailHeaderAdapter.kt @@ -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 . + */ + +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() { + 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)) + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/detail/recycler/AlbumDetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/list/AlbumDetailListAdapter.kt similarity index 62% rename from app/src/main/java/org/oxycblt/auxio/detail/recycler/AlbumDetailAdapter.kt rename to app/src/main/java/org/oxycblt/auxio/detail/list/AlbumDetailListAdapter.kt index 671a4eafe..84c8683ad 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/recycler/AlbumDetailAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/list/AlbumDetailListAdapter.kt @@ -1,6 +1,6 @@ /* * Copyright (c) 2021 Auxio Project - * AlbumDetailAdapter.kt is part of Auxio. + * AlbumDetailListAdapter.kt is part of Auxio. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.detail.recycler +package org.oxycblt.auxio.detail.list import android.view.View import android.view.ViewGroup @@ -26,7 +26,6 @@ import androidx.recyclerview.widget.RecyclerView import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.ItemAlbumSongBinding -import org.oxycblt.auxio.databinding.ItemDetailBinding import org.oxycblt.auxio.databinding.ItemDiscHeaderBinding import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.SelectableListListener @@ -34,37 +33,22 @@ import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter import org.oxycblt.auxio.list.adapter.SimpleDiffCallback import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.music.areRawNamesTheSame import org.oxycblt.auxio.music.metadata.Disc -import org.oxycblt.auxio.music.resolveNames import org.oxycblt.auxio.playback.formatDurationMs import org.oxycblt.auxio.util.context -import org.oxycblt.auxio.util.getPlural import org.oxycblt.auxio.util.inflater /** - * An [DetailAdapter] implementing the header and sub-items for the [Album] detail view. + * An [DetailListAdapter] implementing the header and sub-items for the [Album] detail view. * - * @param listener A [Listener] to bind interactions to. + * @param listener A [DetailListAdapter.Listener] to bind interactions to. * @author Alexander Capehart (OxygenCobalt) */ -class AlbumDetailAdapter(private val listener: Listener) : DetailAdapter(listener, DIFF_CALLBACK) { - /** - * An extension to [DetailAdapter.Listener] that enables interactions specific to the album - * detail view. - */ - interface Listener : DetailAdapter.Listener { - /** - * Called when the artist name in the [Album] header was clicked, requesting navigation to - * it's parent artist. - */ - fun onNavigateToParentArtist() - } - +class AlbumDetailListAdapter(private val listener: Listener) : + DetailListAdapter(listener, DIFF_CALLBACK) { override fun getItemViewType(position: Int) = when (getItem(position)) { - // Support the Album header, sub-headers for each disc, and special album songs. - is Album -> AlbumDetailViewHolder.VIEW_TYPE + // Support sub-headers for each disc, and special album songs. is Disc -> DiscViewHolder.VIEW_TYPE is Song -> AlbumSongViewHolder.VIEW_TYPE else -> super.getItemViewType(position) @@ -72,7 +56,6 @@ class AlbumDetailAdapter(private val listener: Listener) : DetailAdapter(listene override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = when (viewType) { - AlbumDetailViewHolder.VIEW_TYPE -> AlbumDetailViewHolder.from(parent) DiscViewHolder.VIEW_TYPE -> DiscViewHolder.from(parent) AlbumSongViewHolder.VIEW_TYPE -> AlbumSongViewHolder.from(parent) else -> super.onCreateViewHolder(parent, viewType) @@ -81,7 +64,6 @@ class AlbumDetailAdapter(private val listener: Listener) : DetailAdapter(listene override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { super.onBindViewHolder(holder, position) when (val item = getItem(position)) { - is Album -> (holder as AlbumDetailViewHolder).bind(item, listener) is Disc -> (holder as DiscViewHolder).bind(item) is Song -> (holder as AlbumSongViewHolder).bind(item, listener) } @@ -100,91 +82,16 @@ class AlbumDetailAdapter(private val listener: Listener) : DetailAdapter(listene /** A comparator that can be used with DiffUtil. */ val DIFF_CALLBACK = object : SimpleDiffCallback() { - override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean { - return when { - oldItem is Album && newItem is Album -> - AlbumDetailViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem) + override fun areContentsTheSame(oldItem: Item, newItem: Item) = + when { oldItem is Disc && newItem is Disc -> DiscViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem) oldItem is Song && newItem is Song -> AlbumSongViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem) // Fall back to DetailAdapter's differ to handle other headers. - else -> DetailAdapter.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem) + else -> DetailListAdapter.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem) } - } - } - } -} - -/** - * A [RecyclerView.ViewHolder] that displays the [Album] header in the detail view. Use [from] to - * create an instance. - * - * @author Alexander Capehart (OxygenCobalt) - */ -private class AlbumDetailViewHolder private constructor(private val binding: ItemDetailBinding) : - RecyclerView.ViewHolder(binding.root) { - - /** - * Bind new data to this instance. - * - * @param album The new [Album] to bind. - * @param listener A [AlbumDetailAdapter.Listener] to bind interactions to. - */ - fun bind(album: Album, listener: AlbumDetailAdapter.Listener) { - binding.detailCover.bind(album) - - // The type text depends on the release type (Album, EP, Single, etc.) - binding.detailType.text = binding.context.getString(album.releaseType.stringRes) - - binding.detailName.text = album.resolveName(binding.context) - - // Artist name maps to the subhead text - binding.detailSubhead.apply { - text = album.artists.resolveNames(context) - - // Add a QoL behavior where navigation to the artist will occur if the artist - // name is pressed. - setOnClickListener { listener.onNavigateToParentArtist() } - } - - // Date, song count, and duration map to the info text - binding.detailInfo.apply { - // Fall back to a friendlier "No date" text if the album doesn't have date information - val date = album.dates?.resolveDate(context) ?: context.getString(R.string.def_date) - val songCount = context.getPlural(R.plurals.fmt_song_count, album.songs.size) - val duration = album.durationMs.formatDurationMs(true) - text = context.getString(R.string.fmt_three, date, songCount, duration) - } - - binding.detailPlayButton.setOnClickListener { listener.onPlay() } - binding.detailShuffleButton.setOnClickListener { listener.onShuffle() } - } - - companion object { - /** A unique ID for this [RecyclerView.ViewHolder] type. */ - const val VIEW_TYPE = IntegerTable.VIEW_TYPE_ALBUM_DETAIL - - /** - * Create a new instance. - * - * @param parent The parent to inflate this instance from. - * @return A new instance. - */ - fun from(parent: View) = - AlbumDetailViewHolder(ItemDetailBinding.inflate(parent.context.inflater)) - - /** A comparator that can be used with DiffUtil. */ - val DIFF_CALLBACK = - object : SimpleDiffCallback() { - 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 } } } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/recycler/ArtistDetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/list/ArtistDetailListAdapter.kt similarity index 61% rename from app/src/main/java/org/oxycblt/auxio/detail/recycler/ArtistDetailAdapter.kt rename to app/src/main/java/org/oxycblt/auxio/detail/list/ArtistDetailListAdapter.kt index 80857fa32..c23c7c20c 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/recycler/ArtistDetailAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/list/ArtistDetailListAdapter.kt @@ -1,6 +1,6 @@ /* * Copyright (c) 2021 Auxio Project - * ArtistDetailAdapter.kt is part of Auxio. + * ArtistDetailListAdapter.kt is part of Auxio. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -16,15 +16,13 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.detail.recycler +package org.oxycblt.auxio.detail.list import android.view.View import android.view.ViewGroup -import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.R -import org.oxycblt.auxio.databinding.ItemDetailBinding import org.oxycblt.auxio.databinding.ItemParentBinding import org.oxycblt.auxio.databinding.ItemSongBinding import org.oxycblt.auxio.list.Item @@ -33,21 +31,19 @@ import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter import org.oxycblt.auxio.list.adapter.SimpleDiffCallback import org.oxycblt.auxio.music.* import org.oxycblt.auxio.util.context -import org.oxycblt.auxio.util.getPlural import org.oxycblt.auxio.util.inflater /** - * A [DetailAdapter] implementing the header and sub-items for the [Artist] detail view. + * A [DetailListAdapter] implementing the header and sub-items for the [Artist] detail view. * - * @param listener A [DetailAdapter.Listener] to bind interactions to. + * @param listener A [DetailListAdapter.Listener] to bind interactions to. * @author Alexander Capehart (OxygenCobalt) */ -class ArtistDetailAdapter(private val listener: Listener) : - DetailAdapter(listener, DIFF_CALLBACK) { +class ArtistDetailListAdapter(private val listener: Listener) : + DetailListAdapter(listener, DIFF_CALLBACK) { override fun getItemViewType(position: Int) = when (getItem(position)) { - // Support an artist header, and special artist albums/songs. - is Artist -> ArtistDetailViewHolder.VIEW_TYPE + // Support a special artist albums/songs. is Album -> ArtistAlbumViewHolder.VIEW_TYPE is Song -> ArtistSongViewHolder.VIEW_TYPE else -> super.getItemViewType(position) @@ -55,7 +51,6 @@ class ArtistDetailAdapter(private val listener: Listener) : override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = when (viewType) { - ArtistDetailViewHolder.VIEW_TYPE -> ArtistDetailViewHolder.from(parent) ArtistAlbumViewHolder.VIEW_TYPE -> ArtistAlbumViewHolder.from(parent) ArtistSongViewHolder.VIEW_TYPE -> ArtistSongViewHolder.from(parent) else -> super.onCreateViewHolder(parent, viewType) @@ -65,7 +60,6 @@ class ArtistDetailAdapter(private val listener: Listener) : super.onBindViewHolder(holder, position) // Re-binding an item with new data and not just a changed selection/playing state. when (val item = getItem(position)) { - is Artist -> (holder as ArtistDetailViewHolder).bind(item, listener) is Album -> (holder as ArtistAlbumViewHolder).bind(item, listener) is Song -> (holder as ArtistSongViewHolder).bind(item, listener) } @@ -83,96 +77,14 @@ class ArtistDetailAdapter(private val listener: Listener) : /** A comparator that can be used with DiffUtil. */ val DIFF_CALLBACK = object : SimpleDiffCallback() { - override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean { - return when { - oldItem is Artist && newItem is Artist -> - ArtistDetailViewHolder.DIFF_CALLBACK.areContentsTheSame( - oldItem, newItem) + override fun areContentsTheSame(oldItem: Item, newItem: Item) = + when { oldItem is Album && newItem is Album -> ArtistAlbumViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem) oldItem is Song && newItem is Song -> ArtistSongViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem) - else -> DetailAdapter.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem) + else -> DetailListAdapter.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem) } - } - } - } -} - -/** - * A [RecyclerView.ViewHolder] that displays the [Artist] header in the detail view. Use [from] to - * create an instance. - * - * @author Alexander Capehart (OxygenCobalt) - */ -private class ArtistDetailViewHolder private constructor(private val binding: ItemDetailBinding) : - RecyclerView.ViewHolder(binding.root) { - - /** - * Bind new data to this instance. - * - * @param artist The new [Artist] to bind. - * @param listener A [DetailAdapter.Listener] to bind interactions to. - */ - fun bind(artist: Artist, listener: DetailAdapter.Listener<*>) { - binding.detailCover.bind(artist) - binding.detailType.text = binding.context.getString(R.string.lbl_artist) - binding.detailName.text = artist.resolveName(binding.context) - - if (artist.songs.isNotEmpty()) { - // Information about the artist's genre(s) map to the sub-head text - binding.detailSubhead.apply { - isVisible = true - text = artist.genres.resolveNames(context) - } - - // Song and album counts map to the info - binding.detailInfo.text = - binding.context.getString( - R.string.fmt_two, - binding.context.getPlural(R.plurals.fmt_album_count, artist.albums.size), - binding.context.getPlural(R.plurals.fmt_song_count, artist.songs.size)) - - // In the case that this header used to he configured to have no songs, - // we want to reset the visibility of all information that was hidden. - binding.detailPlayButton.isVisible = true - binding.detailShuffleButton.isVisible = true - } else { - // The artist does not have any songs, so hide functionality that makes no sense. - // ex. Play and Shuffle, Song Counts, and Genre Information. - // Artists are always guaranteed to have albums however, so continue to show those. - binding.detailSubhead.isVisible = false - binding.detailInfo.text = - binding.context.getPlural(R.plurals.fmt_album_count, artist.albums.size) - binding.detailPlayButton.isVisible = false - binding.detailShuffleButton.isVisible = false - } - - binding.detailPlayButton.setOnClickListener { listener.onPlay() } - binding.detailShuffleButton.setOnClickListener { listener.onShuffle() } - } - - companion object { - /** A unique ID for this [RecyclerView.ViewHolder] type. */ - const val VIEW_TYPE = IntegerTable.VIEW_TYPE_ARTIST_DETAIL - - /** - * Create a new instance. - * - * @param parent The parent to inflate this instance from. - * @return A new instance. - */ - fun from(parent: View) = - ArtistDetailViewHolder(ItemDetailBinding.inflate(parent.context.inflater)) - - /** A comparator that can be used with DiffUtil. */ - val DIFF_CALLBACK = - object : SimpleDiffCallback() { - 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 } } } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/recycler/DetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/list/DetailListAdapter.kt similarity index 86% rename from app/src/main/java/org/oxycblt/auxio/detail/recycler/DetailAdapter.kt rename to app/src/main/java/org/oxycblt/auxio/detail/list/DetailListAdapter.kt index b6c49c0a3..7959ec47d 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/recycler/DetailAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/list/DetailListAdapter.kt @@ -1,6 +1,6 @@ /* * Copyright (c) 2022 Auxio Project - * DetailAdapter.kt is part of Auxio. + * DetailListAdapter.kt is part of Auxio. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.detail.recycler +package org.oxycblt.auxio.detail.list import android.view.View import android.view.ViewGroup @@ -37,13 +37,14 @@ import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.inflater /** - * A [RecyclerView.Adapter] that implements behavior shared across each detail view's adapters. + * A [RecyclerView.Adapter] that implements shared behavior between lists of child items in the + * detail views. * * @param listener A [Listener] to bind interactions to. * @param diffCallback A [DiffUtil.ItemCallback] to compare list updates with. * @author Alexander Capehart (OxygenCobalt) */ -abstract class DetailAdapter( +abstract class DetailListAdapter( private val listener: Listener<*>, private val diffCallback: DiffUtil.ItemCallback ) : @@ -78,21 +79,8 @@ abstract class DetailAdapter( return item is BasicHeader || item is SortHeader } - /** An extended [SelectableListListener] for [DetailAdapter] implementations. */ + /** An extended [SelectableListListener] for [DetailListAdapter] implementations. */ interface Listener : SelectableListListener { - // TODO: Split off into sub-listeners if a collapsing toolbar is implemented. - /** - * Called when the play button in a detail header is pressed, requesting that the current - * item should be played. - */ - fun onPlay() - - /** - * Called when the shuffle button in a detail header is pressed, requesting that the current - * item should be shuffled - */ - fun onShuffle() - /** * Called when the button in a [SortHeader] item is pressed, requesting that the sort menu * should be opened. @@ -137,9 +125,9 @@ private class SortHeaderViewHolder(private val binding: ItemSortHeaderBinding) : * Bind new data to this instance. * * @param sortHeader The new [SortHeader] to bind. - * @param listener An [DetailAdapter.Listener] to bind interactions to. + * @param listener An [DetailListAdapter.Listener] to bind interactions to. */ - fun bind(sortHeader: SortHeader, listener: DetailAdapter.Listener<*>) { + fun bind(sortHeader: SortHeader, listener: DetailListAdapter.Listener<*>) { binding.headerTitle.text = binding.context.getString(sortHeader.titleRes) binding.headerButton.apply { // Add a Tooltip based on the content description so that the purpose of this diff --git a/app/src/main/java/org/oxycblt/auxio/detail/list/GenreDetailListAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/list/GenreDetailListAdapter.kt new file mode 100644 index 000000000..67ebe3781 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/detail/list/GenreDetailListAdapter.kt @@ -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 . + */ + +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) : + 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() { + 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) + } + } + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/detail/recycler/SongPropertyAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/list/SongPropertyAdapter.kt similarity index 98% rename from app/src/main/java/org/oxycblt/auxio/detail/recycler/SongPropertyAdapter.kt rename to app/src/main/java/org/oxycblt/auxio/detail/list/SongPropertyAdapter.kt index c9f3742e6..690f3a792 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/recycler/SongPropertyAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/list/SongPropertyAdapter.kt @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.detail.recycler +package org.oxycblt.auxio.detail.list import android.view.View import android.view.ViewGroup diff --git a/app/src/main/java/org/oxycblt/auxio/detail/recycler/GenreDetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/recycler/GenreDetailAdapter.kt deleted file mode 100644 index 1ff15c857..000000000 --- a/app/src/main/java/org/oxycblt/auxio/detail/recycler/GenreDetailAdapter.kt +++ /dev/null @@ -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 . - */ - -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) : - 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() { - 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() { - override fun areContentsTheSame(oldItem: Genre, newItem: Genre) = - oldItem.rawName == newItem.rawName && - oldItem.songs.size == newItem.songs.size && - oldItem.durationMs == newItem.durationMs - } - } -} diff --git a/app/src/main/java/org/oxycblt/auxio/image/ImageModule.kt b/app/src/main/java/org/oxycblt/auxio/image/ImageModule.kt index fa9b4a900..ac9dd75c9 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/ImageModule.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/ImageModule.kt @@ -34,7 +34,6 @@ import org.oxycblt.auxio.image.extractor.* @InstallIn(SingletonComponent::class) interface ImageModule { @Binds fun settings(imageSettings: ImageSettingsImpl): ImageSettings - @Binds fun coverExtractor(coverExtractor: CoverExtractorImpl): CoverExtractor } @Module diff --git a/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverExtractor.kt b/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverExtractor.kt index b87680017..78a405501 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverExtractor.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverExtractor.kt @@ -38,33 +38,15 @@ import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logW -/** - * Stateless interface for loading [Album] cover image data. - * - * @author Alexander Capehart (OxygenCobalt) - */ -interface CoverExtractor { - /** - * Fetch an album cover, respecting the current cover configuration. - * - * @param context [Context] required to load the image. - * @param imageSettings [ImageSettings] required to obtain configuration information. - * @param album [Album] to load the cover from. - * @return An [InputStream] of image data if the cover loading was successful, null if the cover - * loading failed or should not occur. - */ - suspend fun extract(album: Album): InputStream? -} - -class CoverExtractorImpl +class CoverExtractor @Inject constructor( @ApplicationContext private val context: Context, private val imageSettings: ImageSettings, private val mediaSourceFactory: MediaSource.Factory -) : CoverExtractor { +) { - override suspend fun extract(album: Album): InputStream? = + suspend fun extract(album: Album): InputStream? = try { when (imageSettings.coverMode) { CoverMode.OFF -> null diff --git a/app/src/main/java/org/oxycblt/auxio/list/adapter/FlexibleListAdapter.kt b/app/src/main/java/org/oxycblt/auxio/list/adapter/FlexibleListAdapter.kt index aa0941211..ed31739d7 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/adapter/FlexibleListAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/adapter/FlexibleListAdapter.kt @@ -35,6 +35,7 @@ import org.oxycblt.auxio.util.logD abstract class FlexibleListAdapter( diffCallback: DiffUtil.ItemCallback ) : RecyclerView.Adapter() { + @Suppress("LeakingThis") private val differ = FlexibleListDiffer(this, diffCallback) final override fun getItemCount() = differ.currentList.size /** The current list stored by the adapter's differ instance. */ @@ -55,9 +56,7 @@ abstract class FlexibleListAdapter( instructions: UpdateInstructions?, callback: (() -> Unit)? = null ) = - differ.update(newData, instructions, callback).also { - logD("Update delivered: $instructions" + "") - } + differ.update(newData, instructions, callback) } /** diff --git a/app/src/main/java/org/oxycblt/auxio/list/recycler/HeaderItemDecoration.kt b/app/src/main/java/org/oxycblt/auxio/list/recycler/HeaderItemDecoration.kt index f14773d13..1ffc6c1fc 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/recycler/HeaderItemDecoration.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/recycler/HeaderItemDecoration.kt @@ -20,6 +20,7 @@ package org.oxycblt.auxio.list.recycler import android.content.Context import android.util.AttributeSet +import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.google.android.material.divider.BackportMaterialDividerItemDecoration @@ -41,12 +42,26 @@ constructor( defStyleAttr: Int = R.attr.materialDividerStyle, orientation: Int = LinearLayoutManager.VERTICAL ) : BackportMaterialDividerItemDecoration(context, attributeSet, defStyleAttr, orientation) { - override fun shouldDrawDivider(position: Int, adapter: RecyclerView.Adapter<*>?) = + override fun shouldDrawDivider(position: Int, adapter: RecyclerView.Adapter<*>?): Boolean { + if (adapter is ConcatAdapter) { + val adapterAndPosition = + try { + adapter.getWrappedAdapterAndPosition(position + 1) + } catch (e: IllegalArgumentException) { + return false + } + return hasHeaderAtPosition(adapterAndPosition.second, adapterAndPosition.first) + } else { + return hasHeaderAtPosition(position + 1, adapter) + } + } + + private fun hasHeaderAtPosition(position: Int, adapter: RecyclerView.Adapter<*>?) = try { // Add a divider if the next item is a header. This organizes the divider to separate // the ends of content rather than the beginning of content, alongside an added benefit // of preventing top headers from having a divider applied. - (adapter as FlexibleListAdapter<*, *>).getItem(position + 1) is Header + (adapter as FlexibleListAdapter<*, *>).getItem(position) is Header } catch (e: ClassCastException) { false } catch (e: IndexOutOfBoundsException) { diff --git a/app/src/main/res/color/sel_m3_switch_thumb.xml b/app/src/main/res/color/sel_m3_switch_thumb.xml deleted file mode 100644 index a2d394958..000000000 --- a/app/src/main/res/color/sel_m3_switch_thumb.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/color/sel_m3_switch_track.xml b/app/src/main/res/color/sel_m3_switch_track.xml deleted file mode 100644 index 19d48ce76..000000000 --- a/app/src/main/res/color/sel_m3_switch_track.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ui_m3_switch_thumb.xml b/app/src/main/res/drawable/ui_m3_switch_thumb.xml deleted file mode 100644 index 04e5221cf..000000000 --- a/app/src/main/res/drawable/ui_m3_switch_thumb.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ui_m3_switch_track.xml b/app/src/main/res/drawable/ui_m3_switch_track.xml deleted file mode 100644 index f89e5de3a..000000000 --- a/app/src/main/res/drawable/ui_m3_switch_track.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout-h600dp/item_detail.xml b/app/src/main/res/layout-h600dp/item_detail_header.xml similarity index 100% rename from app/src/main/res/layout-h600dp/item_detail.xml rename to app/src/main/res/layout-h600dp/item_detail_header.xml diff --git a/app/src/main/res/layout-land/item_detail.xml b/app/src/main/res/layout-land/item_detail_header.xml similarity index 100% rename from app/src/main/res/layout-land/item_detail.xml rename to app/src/main/res/layout-land/item_detail_header.xml diff --git a/app/src/main/res/layout-sw600dp/item_detail.xml b/app/src/main/res/layout-sw600dp/item_detail_header.xml similarity index 100% rename from app/src/main/res/layout-sw600dp/item_detail.xml rename to app/src/main/res/layout-sw600dp/item_detail_header.xml diff --git a/app/src/main/res/layout-sw840dp/item_detail.xml b/app/src/main/res/layout-sw840dp/item_detail_header.xml similarity index 100% rename from app/src/main/res/layout-sw840dp/item_detail.xml rename to app/src/main/res/layout-sw840dp/item_detail_header.xml diff --git a/app/src/main/res/layout/fragment_detail.xml b/app/src/main/res/layout/fragment_detail.xml index d62acc2e2..82a5fc5fa 100644 --- a/app/src/main/res/layout/fragment_detail.xml +++ b/app/src/main/res/layout/fragment_detail.xml @@ -35,6 +35,6 @@ android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" - tools:listitem="@layout/item_detail" /> + tools:listitem="@layout/item_detail_header" /> \ No newline at end of file diff --git a/app/src/main/res/layout/item_detail.xml b/app/src/main/res/layout/item_detail_header.xml similarity index 100% rename from app/src/main/res/layout/item_detail.xml rename to app/src/main/res/layout/item_detail_header.xml diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 3e1256dc9..9d62c8e9e 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -226,7 +226,7 @@ +%.1f dB %d kbps %d Hz - Cargando a túa biblioteca de música... (%1$d/%2$d) + Cargando a túa biblioteca de música… (%1$d/%2$d) Cancións cargadas: %d Álbums cargados: %d Artistas cargados: %d diff --git a/app/src/main/res/values/styles_core.xml b/app/src/main/res/values/styles_core.xml index 094cdb665..bbe18c90c 100644 --- a/app/src/main/res/values/styles_core.xml +++ b/app/src/main/res/values/styles_core.xml @@ -33,7 +33,7 @@ @style/TextAppearance.Auxio.HeadlineSmall @style/TextAppearance.Auxio.TitleLarge - @style/TextAppearance.Auxio.TitleMediumLowEmphasis + @style/TextAppearance.Auxio.TitleMedium @style/TextAppearance.Auxio.TitleSmall diff --git a/app/src/main/res/values/typography.xml b/app/src/main/res/values/typography.xml index db22ed074..af958330b 100644 --- a/app/src/main/res/values/typography.xml +++ b/app/src/main/res/values/typography.xml @@ -72,13 +72,6 @@ - -