detail: split off song property extraction

Split off the song property extraction code into a music package class.

This is part of an initiative to remove non-parameter uses of contexts
in ViewModels.
This commit is contained in:
Alexander Capehart 2023-01-23 09:06:20 -07:00
parent 6c604a9aa5
commit a6f2d82107
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
12 changed files with 213 additions and 189 deletions

View file

@ -31,8 +31,8 @@ object IntegerTable {
const val VIEW_TYPE_ARTIST = 0xA002
/** GenreViewHolder */
const val VIEW_TYPE_GENRE = 0xA003
/** HeaderViewHolder */
const val VIEW_TYPE_HEADER = 0xA004
/** BasicHeaderViewHolder */
const val VIEW_TYPE_BASIC_HEADER = 0xA004
/** SortHeaderViewHolder */
const val VIEW_TYPE_SORT_HEADER = 0xA005
/** AlbumDetailViewHolder */

View file

@ -1,43 +0,0 @@
/*
* Copyright (c) 2022 Auxio Project
*
* 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 androidx.annotation.StringRes
import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.storage.MimeType
/**
* A header variation that displays a button to open a sort menu.
* @param titleRes The string resource to use as the header title
* @author Alexander Capehart (OxygenCobalt)
*/
data class SortHeader(@StringRes val titleRes: Int) : Item
/**
* The properties of a [Song]'s file.
* @param bitrateKbps The bit rate, in kilobytes-per-second. Null if it could not be parsed.
* @param sampleRateHz The sample rate, in hertz.
* @param resolvedMimeType The known mime type of the [Song] after it's file format was determined.
* @author Alexander Capehart (OxygenCobalt)
*/
data class SongProperties(
val bitrateKbps: Int?,
val sampleRateHz: Int?,
val resolvedMimeType: MimeType
)

View file

@ -18,8 +18,6 @@
package org.oxycblt.auxio.detail
import android.app.Application
import android.media.MediaExtractor
import android.media.MediaFormat
import androidx.annotation.StringRes
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
@ -30,13 +28,14 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.yield
import org.oxycblt.auxio.R
import org.oxycblt.auxio.list.Header
import org.oxycblt.auxio.detail.recycler.SortHeader
import org.oxycblt.auxio.list.BasicHeader
import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.music.*
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.extractor.AudioInfo
import org.oxycblt.auxio.music.library.Library
import org.oxycblt.auxio.music.library.Sort
import org.oxycblt.auxio.music.storage.MimeType
import org.oxycblt.auxio.music.tags.Disc
import org.oxycblt.auxio.music.tags.ReleaseType
import org.oxycblt.auxio.playback.PlaybackSettings
@ -54,6 +53,7 @@ class DetailViewModel(application: Application) :
private val musicStore = MusicStore.getInstance()
private val musicSettings = MusicSettings.from(application)
private val playbackSettings = PlaybackSettings.from(application)
private val audioInfoProvider = AudioInfo.Provider.from(application)
private var currentSongJob: Job? = null
@ -64,9 +64,9 @@ class DetailViewModel(application: Application) :
val currentSong: StateFlow<Song?>
get() = _currentSong
private val _songProperties = MutableStateFlow<SongProperties?>(null)
/** The [SongProperties] of the currently shown [Song]. Null if not loaded yet. */
val songProperties: StateFlow<SongProperties?> = _songProperties
private val _songAudioInfo = MutableStateFlow<AudioInfo?>(null)
/** The [AudioInfo] of the currently shown [Song]. Null if not loaded yet. */
val songAudioInfo: StateFlow<AudioInfo?> = _songAudioInfo
// --- ALBUM ---
@ -156,7 +156,7 @@ class DetailViewModel(application: Application) :
val song = currentSong.value
if (song != null) {
_currentSong.value = library.sanitize(song)?.also(::loadProperties)
_currentSong.value = library.sanitize(song)?.also(::refreshAudioInfo)
logD("Updated song to ${currentSong.value}")
}
@ -181,7 +181,7 @@ class DetailViewModel(application: Application) :
/**
* Set a new [currentSong] from it's [Music.UID]. If the [Music.UID] differs, [currentSong] and
* [songProperties] will be updated to align with the new [Song].
* [songAudioInfo] will be updated to align with the new [Song].
* @param uid The UID of the [Song] to load. Must be valid.
*/
fun setSongUid(uid: Music.UID) {
@ -190,7 +190,7 @@ class DetailViewModel(application: Application) :
return
}
logD("Opening Song [uid: $uid]")
_currentSong.value = requireMusic<Song>(uid)?.also(::loadProperties)
_currentSong.value = requireMusic<Song>(uid)?.also(::refreshAudioInfo)
}
/**
@ -238,83 +238,21 @@ class DetailViewModel(application: Application) :
private fun <T : Music> requireMusic(uid: Music.UID) = musicStore.library?.find<T>(uid)
/**
* Start a new job to load a given [Song]'s [SongProperties]. Result is pushed to
* [songProperties].
* Start a new job to load a given [Song]'s [AudioInfo]. Result is pushed to [songAudioInfo].
* @param song The song to load.
*/
private fun loadProperties(song: Song) {
private fun refreshAudioInfo(song: Song) {
// Clear any previous job in order to avoid stale data from appearing in the UI.
currentSongJob?.cancel()
_songProperties.value = null
_songAudioInfo.value = null
currentSongJob =
viewModelScope.launch(Dispatchers.IO) {
val properties = this@DetailViewModel.loadPropertiesImpl(song)
val info = audioInfoProvider.extract(song)
yield()
_songProperties.value = properties
_songAudioInfo.value = info
}
}
private fun loadPropertiesImpl(song: Song): SongProperties {
// While we would use ExoPlayer to extract this information, it doesn't support
// common data like bit rate in progressive data sources due to there being no
// demand. Thus, we are stuck with the inferior OS-provided MediaExtractor.
val extractor = MediaExtractor()
try {
extractor.setDataSource(context, song.uri, emptyMap())
} catch (e: Exception) {
// Can feasibly fail with invalid file formats. Note that this isn't considered
// an error condition in the UI, as there is still plenty of other song information
// that we can show.
logW("Unable to extract song attributes.")
logW(e.stackTraceToString())
return SongProperties(null, null, song.mimeType)
}
// Get the first track from the extractor (This is basically always the only
// track we need to analyze).
val format = extractor.getTrackFormat(0)
// Accessing fields can throw an exception if the fields are not present, and
// the new method for using default values is not available on lower API levels.
// So, we are forced to handle the exception and map it to a saner null value.
val bitrate =
try {
// Convert bytes-per-second to kilobytes-per-second.
format.getInteger(MediaFormat.KEY_BIT_RATE) / 1000
} catch (e: NullPointerException) {
logD("Unable to extract bit rate field")
null
}
val sampleRate =
try {
format.getInteger(MediaFormat.KEY_SAMPLE_RATE)
} catch (e: NullPointerException) {
logE("Unable to extract sample rate field")
null
}
val resolvedMimeType =
if (song.mimeType.fromFormat != null) {
// ExoPlayer was already able to populate the format.
song.mimeType
} else {
// ExoPlayer couldn't populate the format somehow, populate it here.
val formatMimeType =
try {
format.getString(MediaFormat.KEY_MIME)
} catch (e: NullPointerException) {
logE("Unable to extract mime type field")
null
}
MimeType(song.mimeType.fromExtension, formatMimeType)
}
return SongProperties(bitrate, sampleRate, resolvedMimeType)
}
private fun refreshAlbumList(album: Album) {
logD("Refreshing album data")
val data = mutableListOf<Item>(album)
@ -368,7 +306,7 @@ class DetailViewModel(application: Application) :
logD("Release groups for this artist: ${byReleaseGroup.keys}")
for (entry in byReleaseGroup.entries.sortedBy { it.key }) {
data.add(Header(entry.key.headerTitleRes))
data.add(BasicHeader(entry.key.headerTitleRes))
data.addAll(entry.value)
}
@ -386,7 +324,7 @@ class DetailViewModel(application: Application) :
logD("Refreshing genre data")
val data = mutableListOf<Item>(genre)
// Genre is guaranteed to always have artists and songs.
data.add(Header(R.string.lbl_artists))
data.add(BasicHeader(R.string.lbl_artists))
data.addAll(genre.artists)
data.add(SortHeader(R.string.lbl_songs))
data.addAll(genreSongSort.songs(genre.songs))

View file

@ -27,6 +27,7 @@ import androidx.navigation.fragment.navArgs
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogSongDetailBinding
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.extractor.AudioInfo
import org.oxycblt.auxio.playback.formatDurationMs
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
import org.oxycblt.auxio.util.androidActivityViewModels
@ -54,10 +55,10 @@ class SongDetailDialog : ViewBindingDialogFragment<DialogSongDetailBinding>() {
super.onBindingCreated(binding, savedInstanceState)
// DetailViewModel handles most initialization from the navigation argument.
detailModel.setSongUid(args.itemUid)
collectImmediately(detailModel.currentSong, detailModel.songProperties, ::updateSong)
collectImmediately(detailModel.currentSong, detailModel.songAudioInfo, ::updateSong)
}
private fun updateSong(song: Song?, properties: SongProperties?) {
private fun updateSong(song: Song?, info: AudioInfo?) {
if (song == null) {
// Song we were showing no longer exists.
findNavController().navigateUp()
@ -65,28 +66,27 @@ class SongDetailDialog : ViewBindingDialogFragment<DialogSongDetailBinding>() {
}
val binding = requireBinding()
if (properties != null) {
// Finished loading Song properties, populate and show the list of Song information.
if (info != null) {
// Finished loading song audio info, populate and show the list of Song information.
binding.detailLoading.isInvisible = true
binding.detailContainer.isInvisible = false
val context = requireContext()
binding.detailFileName.setText(song.path.name)
binding.detailRelativeDir.setText(song.path.parent.resolveName(context))
binding.detailFormat.setText(properties.resolvedMimeType.resolveName(context))
binding.detailFormat.setText(info.resolvedMimeType.resolveName(context))
binding.detailSize.setText(Formatter.formatFileSize(context, song.size))
binding.detailDuration.setText(song.durationMs.formatDurationMs(true))
if (properties.bitrateKbps != null) {
binding.detailBitrate.setText(
getString(R.string.fmt_bitrate, properties.bitrateKbps))
if (info.bitrateKbps != null) {
binding.detailBitrate.setText(getString(R.string.fmt_bitrate, info.bitrateKbps))
} else {
binding.detailBitrate.setText(R.string.def_bitrate)
}
if (properties.sampleRateHz != null) {
if (info.sampleRateHz != null) {
binding.detailSampleRate.setText(
getString(R.string.fmt_sample_rate, properties.sampleRateHz))
getString(R.string.fmt_sample_rate, info.sampleRateHz))
} else {
binding.detailSampleRate.setText(R.string.def_sample_rate)
}

View file

@ -19,12 +19,13 @@ package org.oxycblt.auxio.detail.recycler
import android.view.View
import android.view.ViewGroup
import androidx.annotation.StringRes
import androidx.appcompat.widget.TooltipCompat
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.IntegerTable
import org.oxycblt.auxio.databinding.ItemSortHeaderBinding
import org.oxycblt.auxio.detail.SortHeader
import org.oxycblt.auxio.list.BasicHeader
import org.oxycblt.auxio.list.Header
import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.list.SelectableListListener
@ -52,21 +53,21 @@ abstract class DetailAdapter(
override fun getItemViewType(position: Int) =
when (getItem(position)) {
// Implement support for headers and sort headers
is Header -> HeaderViewHolder.VIEW_TYPE
is BasicHeader -> BasicHeaderViewHolder.VIEW_TYPE
is SortHeader -> SortHeaderViewHolder.VIEW_TYPE
else -> super.getItemViewType(position)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
when (viewType) {
HeaderViewHolder.VIEW_TYPE -> HeaderViewHolder.from(parent)
BasicHeaderViewHolder.VIEW_TYPE -> BasicHeaderViewHolder.from(parent)
SortHeaderViewHolder.VIEW_TYPE -> SortHeaderViewHolder.from(parent)
else -> error("Invalid item type $viewType")
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (val item = getItem(position)) {
is Header -> (holder as HeaderViewHolder).bind(item)
is BasicHeader -> (holder as BasicHeaderViewHolder).bind(item)
is SortHeader -> (holder as SortHeaderViewHolder).bind(item, listener)
}
}
@ -74,7 +75,7 @@ abstract class DetailAdapter(
override fun isItemFullWidth(position: Int): Boolean {
// Headers should be full-width in all configurations.
val item = getItem(position)
return item is Header || item is SortHeader
return item is BasicHeader || item is SortHeader
}
/** An extended [SelectableListListener] for [DetailAdapter] implementations. */
@ -105,8 +106,8 @@ abstract class DetailAdapter(
object : SimpleDiffCallback<Item>() {
override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
return when {
oldItem is Header && newItem is Header ->
HeaderViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem)
oldItem is BasicHeader && newItem is BasicHeader ->
BasicHeaderViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem)
oldItem is SortHeader && newItem is SortHeader ->
SortHeaderViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem)
else -> false
@ -117,8 +118,15 @@ abstract class DetailAdapter(
}
/**
* A [RecyclerView.ViewHolder] that displays a [SortHeader], a variation on [Header] that adds a
* button opening a menu for sorting. Use [from] to create an instance.
* A header variation that displays a button to open a sort menu.
* @param titleRes The string resource to use as the header title
* @author Alexander Capehart (OxygenCobalt)
*/
data class SortHeader(@StringRes override val titleRes: Int) : Header
/**
* A [RecyclerView.ViewHolder] that displays a [SortHeader], a variation on [BasicHeader] that adds
* a button opening a menu for sorting. Use [from] to create an instance.
* @author Alexander Capehart (OxygenCobalt)
*/
private class SortHeaderViewHolder(private val binding: ItemSortHeaderBinding) :

View file

@ -24,6 +24,16 @@ interface Item
/**
* A "header" used for delimiting groups of data.
* @param titleRes The string resource used for the header's title.
* @author Alexander Capehart (OxygenCobalt)
*/
data class Header(@StringRes val titleRes: Int) : Item
interface Header : Item {
/** The string resource used for the header's title. */
val titleRes: Int
}
/**
* A basic header with no additional actions.
* @param titleRes The string resource used for the header's title.
* @author Alexander Capehart (OxygenCobalt)
*/
data class BasicHeader(@StringRes override val titleRes: Int) : Header

View file

@ -24,7 +24,7 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.ItemHeaderBinding
import org.oxycblt.auxio.databinding.ItemParentBinding
import org.oxycblt.auxio.databinding.ItemSongBinding
import org.oxycblt.auxio.list.Header
import org.oxycblt.auxio.list.BasicHeader
import org.oxycblt.auxio.list.SelectableListListener
import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter
import org.oxycblt.auxio.list.adapter.SimpleDiffCallback
@ -241,23 +241,23 @@ class GenreViewHolder private constructor(private val binding: ItemParentBinding
}
/**
* A [RecyclerView.ViewHolder] that displays a [Header]. Use [from] to create an instance.
* A [RecyclerView.ViewHolder] that displays a [BasicHeader]. Use [from] to create an instance.
* @author Alexander Capehart (OxygenCobalt)
*/
class HeaderViewHolder private constructor(private val binding: ItemHeaderBinding) :
class BasicHeaderViewHolder private constructor(private val binding: ItemHeaderBinding) :
RecyclerView.ViewHolder(binding.root) {
/**
* Bind new data to this instance.
* @param header The new [Header] to bind.
* @param basicHeader The new [BasicHeader] to bind.
*/
fun bind(header: Header) {
logD(binding.context.getString(header.titleRes))
binding.title.text = binding.context.getString(header.titleRes)
fun bind(basicHeader: BasicHeader) {
logD(binding.context.getString(basicHeader.titleRes))
binding.title.text = binding.context.getString(basicHeader.titleRes)
}
companion object {
/** Unique ID for this ViewHolder type. */
const val VIEW_TYPE = IntegerTable.VIEW_TYPE_HEADER
const val VIEW_TYPE = IntegerTable.VIEW_TYPE_BASIC_HEADER
/**
* Create a new instance.
@ -265,13 +265,15 @@ class HeaderViewHolder private constructor(private val binding: ItemHeaderBindin
* @return A new instance.
*/
fun from(parent: View) =
HeaderViewHolder(ItemHeaderBinding.inflate(parent.context.inflater))
BasicHeaderViewHolder(ItemHeaderBinding.inflate(parent.context.inflater))
/** A comparator that can be used with DiffUtil. */
val DIFF_CALLBACK =
object : SimpleDiffCallback<Header>() {
override fun areContentsTheSame(oldItem: Header, newItem: Header): Boolean =
oldItem.titleRes == newItem.titleRes
object : SimpleDiffCallback<BasicHeader>() {
override fun areContentsTheSame(
oldItem: BasicHeader,
newItem: BasicHeader
): Boolean = oldItem.titleRes == newItem.titleRes
}
}
}

View file

@ -0,0 +1,123 @@
/*
* Copyright (c) 2023 Auxio Project
*
* 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.music.extractor
import android.content.Context
import android.media.MediaExtractor
import android.media.MediaFormat
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.storage.MimeType
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logE
import org.oxycblt.auxio.util.logW
/**
* The properties of a [Song]'s file.
* @param bitrateKbps The bit rate, in kilobytes-per-second. Null if it could not be parsed.
* @param sampleRateHz The sample rate, in hertz.
* @param resolvedMimeType The known mime type of the [Song] after it's file format was determined.
* @author Alexander Capehart (OxygenCobalt)
*/
data class AudioInfo(
val bitrateKbps: Int?,
val sampleRateHz: Int?,
val resolvedMimeType: MimeType
) {
/** Implements the process of extracting [AudioInfo] from a given [Song]. */
interface Provider {
/**
* Extract the [AudioInfo] of a given [Song].
* @param song The [Song] to read.
* @return The [AudioInfo] of the [Song], if possible to obtain.
*/
suspend fun extract(song: Song): AudioInfo
companion object {
/**
* Get a framework-backed implementation.
* @param context [Context] required.
*/
fun from(context: Context): Provider = RealAudioInfoProvider(context)
}
}
}
private class RealAudioInfoProvider(private val context: Context) : AudioInfo.Provider {
// While we would use ExoPlayer to extract this information, it doesn't support
// common data like bit rate in progressive data sources due to there being no
// demand. Thus, we are stuck with the inferior OS-provided MediaExtractor.
private val extractor = MediaExtractor()
override suspend fun extract(song: Song): AudioInfo {
try {
extractor.setDataSource(context, song.uri, emptyMap())
} catch (e: Exception) {
// Can feasibly fail with invalid file formats. Note that this isn't considered
// an error condition in the UI, as there is still plenty of other song information
// that we can show.
logW("Unable to extract song attributes.")
logW(e.stackTraceToString())
return AudioInfo(null, null, song.mimeType)
}
// Get the first track from the extractor (This is basically always the only
// track we need to analyze).
val format = extractor.getTrackFormat(0)
// Accessing fields can throw an exception if the fields are not present, and
// the new method for using default values is not available on lower API levels.
// So, we are forced to handle the exception and map it to a saner null value.
val bitrate =
try {
// Convert bytes-per-second to kilobytes-per-second.
format.getInteger(MediaFormat.KEY_BIT_RATE) / 1000
} catch (e: NullPointerException) {
logD("Unable to extract bit rate field")
null
}
val sampleRate =
try {
format.getInteger(MediaFormat.KEY_SAMPLE_RATE)
} catch (e: NullPointerException) {
logE("Unable to extract sample rate field")
null
}
val resolvedMimeType =
if (song.mimeType.fromFormat != null) {
// ExoPlayer was already able to populate the format.
song.mimeType
} else {
// ExoPlayer couldn't populate the format somehow, populate it here.
val formatMimeType =
try {
format.getString(MediaFormat.KEY_MIME)
} catch (e: NullPointerException) {
logE("Unable to extract mime type field")
null
}
MimeType(song.mimeType.fromExtension, formatMimeType)
}
extractor.release()
return AudioInfo(bitrate, sampleRate, resolvedMimeType)
}
}

View file

@ -44,7 +44,7 @@ class SearchAdapter(private val listener: SelectableListListener<Music>) :
is Album -> AlbumViewHolder.VIEW_TYPE
is Artist -> ArtistViewHolder.VIEW_TYPE
is Genre -> GenreViewHolder.VIEW_TYPE
is Header -> HeaderViewHolder.VIEW_TYPE
is BasicHeader -> BasicHeaderViewHolder.VIEW_TYPE
else -> super.getItemViewType(position)
}
@ -54,7 +54,7 @@ class SearchAdapter(private val listener: SelectableListListener<Music>) :
AlbumViewHolder.VIEW_TYPE -> AlbumViewHolder.from(parent)
ArtistViewHolder.VIEW_TYPE -> ArtistViewHolder.from(parent)
GenreViewHolder.VIEW_TYPE -> GenreViewHolder.from(parent)
HeaderViewHolder.VIEW_TYPE -> HeaderViewHolder.from(parent)
BasicHeaderViewHolder.VIEW_TYPE -> BasicHeaderViewHolder.from(parent)
else -> error("Invalid item type $viewType")
}
@ -65,11 +65,11 @@ class SearchAdapter(private val listener: SelectableListListener<Music>) :
is Album -> (holder as AlbumViewHolder).bind(item, listener)
is Artist -> (holder as ArtistViewHolder).bind(item, listener)
is Genre -> (holder as GenreViewHolder).bind(item, listener)
is Header -> (holder as HeaderViewHolder).bind(item)
is BasicHeader -> (holder as BasicHeaderViewHolder).bind(item)
}
}
override fun isItemFullWidth(position: Int) = getItem(position) is Header
override fun isItemFullWidth(position: Int) = getItem(position) is BasicHeader
/**
* Make sure that the top header has a correctly configured divider visibility. This would
@ -94,8 +94,8 @@ class SearchAdapter(private val listener: SelectableListListener<Music>) :
ArtistViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem)
oldItem is Genre && newItem is Genre ->
GenreViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem)
oldItem is Header && newItem is Header ->
HeaderViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem)
oldItem is BasicHeader && newItem is BasicHeader ->
BasicHeaderViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem)
else -> false
}
}

View file

@ -27,7 +27,7 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.yield
import org.oxycblt.auxio.R
import org.oxycblt.auxio.list.Header
import org.oxycblt.auxio.list.BasicHeader
import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.music.*
import org.oxycblt.auxio.music.MusicStore
@ -119,19 +119,19 @@ class SearchViewModel(application: Application) :
return buildList {
results.artists?.let { artists ->
add(Header(R.string.lbl_artists))
add(BasicHeader(R.string.lbl_artists))
addAll(SORT.artists(artists))
}
results.albums?.let { albums ->
add(Header(R.string.lbl_albums))
add(BasicHeader(R.string.lbl_albums))
addAll(SORT.albums(albums))
}
results.genres?.let { genres ->
add(Header(R.string.lbl_genres))
add(BasicHeader(R.string.lbl_genres))
addAll(SORT.genres(genres))
}
results.songs?.let { songs ->
add(Header(R.string.lbl_songs))
add(BasicHeader(R.string.lbl_songs))
addAll(SORT.songs(songs))
}
}

View file

@ -1,15 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@android:id/title"
style="@style/Widget.Auxio.TextView.Header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="Songs" />
</LinearLayout>
tools:text="Songs" />

View file

@ -1,38 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:orientation="horizontal"
android:layout_height="wrap_content">
<com.google.android.material.divider.MaterialDivider
android:id="@+id/header_divider"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/header_title"
style="@style/Widget.Auxio.TextView.Header"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/spacing_mid_medium"
app:layout_constraintEnd_toStartOf="@+id/header_button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/header_divider"
tools:text="Songs" />
<Button
android:id="@+id/header_button"
style="@style/Widget.Auxio.Button.Icon.Small"
android:layout_width="0dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/spacing_mid_medium"
android:contentDescription="@string/lbl_sort"
app:icon="@drawable/ic_sort_24"
app:layout_constraintTop_toBottomOf="@id/header_divider"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>