detail: make artist view use collapsing toolbar

This commit is contained in:
Alexander Capehart 2024-07-20 12:54:04 -06:00
parent 04265d5285
commit 0eb3ede8ec
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
6 changed files with 214 additions and 256 deletions

View file

@ -23,20 +23,11 @@ import android.view.LayoutInflater
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearSmoothScroller
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.transition.MaterialSharedAxis
import dagger.hilt.android.AndroidEntryPoint
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentDetail2Binding
import org.oxycblt.auxio.detail.list.AlbumDetailListAdapter
import org.oxycblt.auxio.detail.list.DetailListAdapter
import org.oxycblt.auxio.list.Divider
import org.oxycblt.auxio.list.Header
import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.list.ListFragment
import org.oxycblt.auxio.list.ListViewModel
@ -54,12 +45,9 @@ import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.playback.formatDurationMs
import org.oxycblt.auxio.util.collect
import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.getDimenPixels
import org.oxycblt.auxio.util.getPlural
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.navigateSafe
import org.oxycblt.auxio.util.overrideOnOverflowMenuClick
import org.oxycblt.auxio.util.setFullWidthLookup
import org.oxycblt.auxio.util.showToast
import org.oxycblt.auxio.util.unlikelyToBeNull
@ -69,10 +57,7 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
* @author Alexander Capehart (OxygenCobalt)
*/
@AndroidEntryPoint
class AlbumDetailFragment :
ListFragment<Song, FragmentDetail2Binding>(),
DetailListAdapter.Listener<Song>,
AppBarLayout.OnOffsetChangedListener {
class AlbumDetailFragment : DetailFragment<Album, Song>() {
private val detailModel: DetailViewModel by activityViewModels()
override val listModel: ListViewModel by activityViewModels()
override val musicModel: MusicViewModel by activityViewModels()
@ -83,56 +68,17 @@ class AlbumDetailFragment :
private val args: AlbumDetailFragmentArgs by navArgs()
private val albumListAdapter = AlbumDetailListAdapter(this)
private var spacingSmall = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Detail transitions are always on the X axis. Shared element transitions are more
// semantically correct, but are also too buggy to be sensible.
enterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true)
returnTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false)
exitTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true)
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false)
}
override fun onCreateBinding(inflater: LayoutInflater) =
FragmentDetail2Binding.inflate(inflater)
override fun getDetailListAdapter() = albumListAdapter
override fun getSelectionToolbar(binding: FragmentDetail2Binding) =
binding.detailSelectionToolbar
override fun onBindingCreated(binding: FragmentDetail2Binding, savedInstanceState: Bundle?) {
super.onBindingCreated(binding, savedInstanceState)
// --- UI SETUP --
binding.detailAppbar.addOnOffsetChangedListener(this)
binding.detailNormalToolbar.apply {
setNavigationOnClickListener { findNavController().navigateUp() }
overrideOnOverflowMenuClick {
listModel.openMenu(
R.menu.detail_album, unlikelyToBeNull(detailModel.currentAlbum.value))
}
}
binding.detailRecycler.apply {
adapter = albumListAdapter
(layoutManager as GridLayoutManager).setFullWidthLookup {
if (it != 0) {
val item =
detailModel.genreSongList.value.getOrElse(it - 1) {
return@setFullWidthLookup false
}
item is Divider || item is Header
} else {
true
}
}
}
spacingSmall = requireContext().getDimenPixels(R.dimen.spacing_small)
// -- VIEWMODEL SETUP ---
// DetailViewModel handles most initialization from the navigation argument.
detailModel.setAlbum(args.albumUid)
@ -150,33 +96,19 @@ class AlbumDetailFragment :
override fun onDestroyBinding(binding: FragmentDetail2Binding) {
super.onDestroyBinding(binding)
binding.detailRecycler.adapter = null
// Avoid possible race conditions that could cause a bad replace instruction to be consumed
// during list initialization and crash the app. Could happen if the user is fast enough.
detailModel.albumSongInstructions.consume()
}
override fun onOffsetChanged(appBarLayout: AppBarLayout, verticalOffset: Int) {
val binding = requireBinding()
val range = appBarLayout.totalScrollRange
val ratio = abs(verticalOffset.toFloat()) / range.toFloat()
val outRatio = min(ratio * 2, 1f)
val detailHeader = binding.detailHeader
detailHeader.scaleX = 1 - 0.05f * outRatio
detailHeader.scaleY = 1 - 0.05f * outRatio
detailHeader.alpha = 1 - outRatio
val inRatio = max(ratio - 0.5f, 0f) * 2
val detailContent = binding.detailToolbarContent
detailContent.alpha = inRatio
detailContent.translationY = spacingSmall * (1 - inRatio)
}
override fun onRealClick(item: Song) {
playbackModel.play(item, detailModel.playInAlbumWith)
}
override fun onOpenParentMenu() {
listModel.openMenu(R.menu.album, unlikelyToBeNull(detailModel.currentAlbum.value))
}
override fun onOpenMenu(item: Song) {
listModel.openMenu(R.menu.album_song, item, detailModel.playInAlbumWith)
}
@ -193,12 +125,14 @@ class AlbumDetailFragment :
}
val binding = requireBinding()
val context = requireContext()
val name = album.name.resolve(context)
binding.detailToolbarTitle.text = album.name.resolve(requireContext())
binding.detailToolbarTitle.text = name
binding.detailCover.bind(album)
// The type text depends on the release type (Album, EP, Single, etc.)
binding.detailType.text = getString(album.releaseType.stringRes)
binding.detailName.text = album.name.resolve(requireContext())
binding.detailName.text = name
// Artist name maps to the subhead text
binding.detailSubhead.apply {
text = album.artists.resolveNames(context)

View file

@ -20,21 +20,15 @@ package org.oxycblt.auxio.detail
import android.os.Bundle
import android.view.LayoutInflater
import androidx.core.view.isVisible
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.GridLayoutManager
import com.google.android.material.transition.MaterialSharedAxis
import dagger.hilt.android.AndroidEntryPoint
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentDetailBinding
import org.oxycblt.auxio.detail.header.ArtistDetailHeaderAdapter
import org.oxycblt.auxio.detail.header.DetailHeaderAdapter
import org.oxycblt.auxio.databinding.FragmentDetail2Binding
import org.oxycblt.auxio.detail.list.ArtistDetailListAdapter
import org.oxycblt.auxio.detail.list.DetailListAdapter
import org.oxycblt.auxio.list.Divider
import org.oxycblt.auxio.list.Header
import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.list.ListFragment
import org.oxycblt.auxio.list.ListViewModel
@ -47,14 +41,15 @@ import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.music.PlaylistDecision
import org.oxycblt.auxio.music.PlaylistMessage
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.resolveNames
import org.oxycblt.auxio.playback.PlaybackDecision
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.util.collect
import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.getPlural
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.navigateSafe
import org.oxycblt.auxio.util.overrideOnOverflowMenuClick
import org.oxycblt.auxio.util.setFullWidthLookup
import org.oxycblt.auxio.util.showToast
import org.oxycblt.auxio.util.unlikelyToBeNull
@ -64,18 +59,15 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
* @author Alexander Capehart (OxygenCobalt)
*/
@AndroidEntryPoint
class ArtistDetailFragment :
ListFragment<Music, FragmentDetailBinding>(),
DetailHeaderAdapter.Listener,
DetailListAdapter.Listener<Music> {
class ArtistDetailFragment : DetailFragment<Artist, Music>() {
private val detailModel: DetailViewModel by activityViewModels()
override val listModel: ListViewModel by activityViewModels()
override val musicModel: MusicViewModel by activityViewModels()
override val playbackModel: PlaybackViewModel by activityViewModels()
// Information about what 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 artistHeaderAdapter = ArtistDetailHeaderAdapter(this)
private val artistListAdapter = ArtistDetailListAdapter(this)
override fun onCreate(savedInstanceState: Bundle?) {
@ -88,39 +80,17 @@ class ArtistDetailFragment :
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false)
}
override fun onCreateBinding(inflater: LayoutInflater) = FragmentDetailBinding.inflate(inflater)
override fun onCreateBinding(inflater: LayoutInflater) =
FragmentDetail2Binding.inflate(inflater)
override fun getSelectionToolbar(binding: FragmentDetailBinding) =
override fun getSelectionToolbar(binding: FragmentDetail2Binding) =
binding.detailSelectionToolbar
override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) {
override fun getDetailListAdapter() = artistListAdapter
override fun onBindingCreated(binding: FragmentDetail2Binding, savedInstanceState: Bundle?) {
super.onBindingCreated(binding, savedInstanceState)
// --- UI SETUP ---
binding.detailNormalToolbar.apply {
setNavigationOnClickListener { findNavController().navigateUp() }
setOnMenuItemClickListener(this@ArtistDetailFragment)
overrideOnOverflowMenuClick {
listModel.openMenu(
R.menu.detail_parent, unlikelyToBeNull(detailModel.currentArtist.value))
}
}
binding.detailRecycler.apply {
adapter = ConcatAdapter(artistHeaderAdapter, artistListAdapter)
(layoutManager as GridLayoutManager).setFullWidthLookup {
if (it != 0) {
val item =
detailModel.artistSongList.value.getOrElse(it - 1) {
return@setFullWidthLookup false
}
item is Divider || item is Header
} else {
true
}
}
}
// --- VIEWMODEL SETUP ---
// DetailViewModel handles most initialization from the navigation argument.
detailModel.setArtist(args.artistUid)
@ -136,10 +106,8 @@ class ArtistDetailFragment :
collect(playbackModel.playbackDecision.flow, ::handlePlaybackDecision)
}
override fun onDestroyBinding(binding: FragmentDetailBinding) {
override fun onDestroyBinding(binding: FragmentDetail2Binding) {
super.onDestroyBinding(binding)
binding.detailNormalToolbar.setOnMenuItemClickListener(null)
binding.detailRecycler.adapter = null
// Avoid possible race conditions that could cause a bad replace instruction to be consumed
// during list initialization and crash the app. Could happen if the user is fast enough.
detailModel.artistSongInstructions.consume()
@ -153,6 +121,10 @@ class ArtistDetailFragment :
}
}
override fun onOpenParentMenu() {
listModel.openMenu(R.menu.detail_parent, unlikelyToBeNull(detailModel.currentArtist.value))
}
override fun onOpenMenu(item: Music) {
when (item) {
is Song -> listModel.openMenu(R.menu.artist_song, item, detailModel.playInArtistWith)
@ -161,14 +133,6 @@ class ArtistDetailFragment :
}
}
override fun onPlay() {
playbackModel.play(unlikelyToBeNull(detailModel.currentArtist.value))
}
override fun onShuffle() {
playbackModel.shuffle(unlikelyToBeNull(detailModel.currentArtist.value))
}
override fun onOpenSortMenu() {
findNavController().navigateSafe(ArtistDetailFragmentDirections.sort())
}
@ -179,8 +143,57 @@ class ArtistDetailFragment :
findNavController().navigateUp()
return
}
requireBinding().detailNormalToolbar.title = artist.name.resolve(requireContext())
artistHeaderAdapter.setParent(artist)
val binding = requireBinding()
val context = requireContext()
val name = artist.name.resolve(context)
binding.detailToolbarTitle.text = name
binding.detailCover.bind(artist)
binding.detailType.text = context.getString(R.string.lbl_artist)
binding.detailName.text = name
// Song and album counts map to the info
binding.detailInfo.text =
context.getString(
R.string.fmt_two,
if (artist.explicitAlbums.isNotEmpty()) {
context.getPlural(R.plurals.fmt_album_count, artist.explicitAlbums.size)
} else {
context.getString(R.string.def_album_count)
},
if (artist.songs.isNotEmpty()) {
context.getPlural(R.plurals.fmt_song_count, artist.songs.size)
} else {
context.getString(R.string.def_song_count)
})
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)
}
// 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.
logD("Artist is empty, disabling genres and playback")
binding.detailSubhead.isVisible = false
binding.detailPlayButton.isEnabled = false
binding.detailShuffleButton.isEnabled = false
}
binding.detailPlayButton.setOnClickListener {
playbackModel.play(unlikelyToBeNull(detailModel.currentArtist.value))
}
binding.detailShuffleButton.setOnClickListener {
playbackModel.shuffle(unlikelyToBeNull(detailModel.currentArtist.value))
}
}
private fun updateList(list: List<Item>) {

View file

@ -0,0 +1,131 @@
/*
* Copyright (c) 2024 Auxio Project
* DetailFragment.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
import android.os.Bundle
import android.view.LayoutInflater
import androidx.fragment.app.activityViewModels
import androidx.navigation.findNavController
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.GridLayoutManager
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.transition.MaterialSharedAxis
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentDetail2Binding
import org.oxycblt.auxio.detail.list.DetailListAdapter
import org.oxycblt.auxio.list.Divider
import org.oxycblt.auxio.list.Header
import org.oxycblt.auxio.list.ListFragment
import org.oxycblt.auxio.list.ListViewModel
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.util.getDimenPixels
import org.oxycblt.auxio.util.overrideOnOverflowMenuClick
import org.oxycblt.auxio.util.setFullWidthLookup
abstract class DetailFragment<P : MusicParent, C : Music> :
ListFragment<C, FragmentDetail2Binding>(),
DetailListAdapter.Listener<C>,
AppBarLayout.OnOffsetChangedListener {
private val detailModel: DetailViewModel by activityViewModels()
override val listModel: ListViewModel by activityViewModels()
override val musicModel: MusicViewModel by activityViewModels()
override val playbackModel: PlaybackViewModel by activityViewModels()
private var spacingSmall = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Detail transitions are always on the X axis. Shared element transitions are more
// semantically correct, but are also too buggy to be sensible.
enterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true)
returnTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false)
exitTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true)
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false)
}
override fun onCreateBinding(inflater: LayoutInflater) =
FragmentDetail2Binding.inflate(inflater)
abstract fun getDetailListAdapter(): DetailListAdapter
override fun getSelectionToolbar(binding: FragmentDetail2Binding) =
binding.detailSelectionToolbar
override fun onBindingCreated(binding: FragmentDetail2Binding, savedInstanceState: Bundle?) {
super.onBindingCreated(binding, savedInstanceState)
// --- UI SETUP ---
binding.detailAppbar.addOnOffsetChangedListener(this)
binding.detailNormalToolbar.apply {
setNavigationOnClickListener { findNavController().navigateUp() }
setOnMenuItemClickListener(this@DetailFragment)
overrideOnOverflowMenuClick { onOpenParentMenu() }
}
binding.detailRecycler.apply {
adapter = getDetailListAdapter()
(layoutManager as GridLayoutManager).setFullWidthLookup {
if (it != 0) {
val item =
detailModel.artistSongList.value.getOrElse(it - 1) {
return@setFullWidthLookup false
}
item is Divider || item is Header
} else {
true
}
}
}
spacingSmall = requireContext().getDimenPixels(R.dimen.spacing_small)
}
override fun onDestroyBinding(binding: FragmentDetail2Binding) {
super.onDestroyBinding(binding)
binding.detailAppbar.removeOnOffsetChangedListener(this)
binding.detailNormalToolbar.setOnMenuItemClickListener(null)
binding.detailRecycler.adapter = null
}
override fun onOffsetChanged(appBarLayout: AppBarLayout, verticalOffset: Int) {
val binding = requireBinding()
val range = appBarLayout.totalScrollRange
val ratio = abs(verticalOffset.toFloat()) / range.toFloat()
val outRatio = min(ratio * 2, 1f)
val detailHeader = binding.detailHeader
detailHeader.scaleX = 1 - 0.05f * outRatio
detailHeader.scaleY = 1 - 0.05f * outRatio
detailHeader.alpha = 1 - outRatio
val inRatio = max(ratio - 0.5f, 0f) * 2
val detailContent = binding.detailToolbarContent
detailContent.alpha = inRatio
detailContent.translationY = spacingSmall * (1 - inRatio)
}
abstract fun onOpenParentMenu()
}

View file

@ -625,7 +625,6 @@ constructor(
for (entry in grouping.entries) {
val header = BasicHeader(entry.key.headerTitleRes)
list.add(Divider(header))
list.add(header)
list.addAll(ARTIST_ALBUM_SORT.albums(entry.value))
}

View file

@ -1,120 +0,0 @@
/*
* 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
import org.oxycblt.auxio.util.logD
/**
* A [DetailHeaderAdapter] that shows [Artist] information.
*
* @param listener [DetailHeaderAdapter.Listener] to bind interactions to.
* @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.name.resolve(binding.context)
// Song and album counts map to the info
binding.detailInfo.text =
binding.context.getString(
R.string.fmt_two,
if (artist.explicitAlbums.isNotEmpty()) {
binding.context.getPlural(R.plurals.fmt_album_count, artist.explicitAlbums.size)
} else {
binding.context.getString(R.string.def_album_count)
},
if (artist.songs.isNotEmpty()) {
binding.context.getPlural(R.plurals.fmt_song_count, artist.songs.size)
} else {
binding.context.getString(R.string.def_song_count)
})
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)
}
// 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.
logD("Artist is empty, disabling genres and playback")
binding.detailSubhead.isVisible = false
binding.detailPlayButton.isEnabled = false
binding.detailShuffleButton.isEnabled = 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

@ -285,8 +285,9 @@ private class MediaStoreExtractorImpl(
// know when it corresponds to the folder and not, say, Low Roar's breakout album "0"?
// Also, on some devices it's literally just null. To maintain behavior sanity just
// replicate the majority behavior described prior.
rawSong.albumName = cursor.getStringOrNull(albumIndex)
?: requireNotNull(rawSong.path?.name) { "Invalid raw: No path" }
rawSong.albumName =
cursor.getStringOrNull(albumIndex)
?: requireNotNull(rawSong.path?.name) { "Invalid raw: No path" }
// Android does not make a non-existent artist tag null, it instead fills it in
// as <unknown>, which makes absolutely no sense given how other columns default
// to null if they are not present. If this column is such, null it so that