list: rework diffing abstraction

Make all adapters relying on diffing unified into a DiffAdapter
superclass that can then accurately respond to the new
UpdateInstructions data.

UpdateInstructions is still not fully used everywhere, but will be
soon.
This commit is contained in:
Alexander Capehart 2023-01-15 20:30:45 -07:00
parent b524beb0ac
commit df98bb535f
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
32 changed files with 468 additions and 230 deletions

View file

@ -9,6 +9,7 @@
- Added ability to edit previously played or currently playing items in the queue
- Added support for date values formatted as "YYYYMMDD"
- Pressing the button will now clear the current selection before navigating back
- Added support for non-standard `ARTISTS` tags
#### What's Fixed
- Fixed unreliable ReplayGain adjustment application in certain situations

View file

@ -21,7 +21,7 @@
<queries />
<application
android:name=".AuxioApp"
android:name=".Auxio"
android:allowBackup="true"
android:fullBackupContent="@xml/backup_descriptor"
android:icon="@mipmap/ic_launcher"

View file

@ -38,7 +38,7 @@ import org.oxycblt.auxio.ui.UISettings
* Auxio: A simple, rational music player for android.
* @author Alexander Capehart (OxygenCobalt)
*/
class AuxioApp : Application(), ImageLoaderFactory {
class Auxio : Application(), ImageLoaderFactory {
override fun onCreate() {
super.onCreate()
// Migrate any settings that may have changed in an app update.

View file

@ -131,7 +131,7 @@ class MainActivity : AppCompatActivity() {
val action =
when (intent.action) {
Intent.ACTION_VIEW -> InternalPlayer.Action.Open(intent.data ?: return false)
AuxioApp.INTENT_KEY_SHORTCUT_SHUFFLE -> InternalPlayer.Action.ShuffleAll
Auxio.INTENT_KEY_SHORTCUT_SHUFFLE -> InternalPlayer.Action.ShuffleAll
else -> return false
}
playbackModel.startAction(action)

View file

@ -235,8 +235,8 @@ class MainFragment :
tryHideAllSheets()
}
// Since the listener is also reliant on the bottom sheets, we must also update it
// every frame.
// Since the navigation listener is also reliant on the bottom sheets, we must also update
// it every frame.
callback.invalidateEnabled()
return true

View file

@ -82,7 +82,7 @@ class AlbumDetailFragment :
// DetailViewModel handles most initialization from the navigation argument.
detailModel.setAlbumUid(args.albumUid)
collectImmediately(detailModel.currentAlbum, ::updateAlbum)
collectImmediately(detailModel.albumList, detailAdapter::submitList)
collectImmediately(detailModel.albumList, detailAdapter::diffList)
collectImmediately(
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
collect(navModel.exploreNavigationItem, ::handleNavigation)
@ -170,10 +170,10 @@ class AlbumDetailFragment :
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
if (parent is Album && parent == unlikelyToBeNull(detailModel.currentAlbum.value)) {
detailAdapter.setPlayingItem(song, isPlaying)
detailAdapter.setPlaying(song, isPlaying)
} else {
// Clear the ViewHolders if the mode isn't ALL_SONGS
detailAdapter.setPlayingItem(null, isPlaying)
detailAdapter.setPlaying(null, isPlaying)
}
}
@ -258,7 +258,7 @@ class AlbumDetailFragment :
}
private fun updateSelection(selected: List<Music>) {
detailAdapter.setSelectedItems(selected)
detailAdapter.setSelected(selected.toSet())
requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size)
}
}

View file

@ -85,7 +85,7 @@ class ArtistDetailFragment :
// DetailViewModel handles most initialization from the navigation argument.
detailModel.setArtistUid(args.artistUid)
collectImmediately(detailModel.currentArtist, ::updateItem)
collectImmediately(detailModel.artistList, detailAdapter::submitList)
collectImmediately(detailModel.artistList, detailAdapter::diffList)
collectImmediately(
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
collect(navModel.exploreNavigationItem, ::handleNavigation)
@ -195,7 +195,7 @@ class ArtistDetailFragment :
else -> null
}
detailAdapter.setPlayingItem(playingItem, isPlaying)
detailAdapter.setPlaying(playingItem, isPlaying)
}
private fun handleNavigation(item: Music?) {
@ -234,7 +234,7 @@ class ArtistDetailFragment :
}
private fun updateSelection(selected: List<Music>) {
detailAdapter.setSelectedItems(selected)
detailAdapter.setSelected(selected.toSet())
requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size)
}
}

View file

@ -84,7 +84,7 @@ class GenreDetailFragment :
// DetailViewModel handles most initialization from the navigation argument.
detailModel.setGenreUid(args.genreUid)
collectImmediately(detailModel.currentGenre, ::updateItem)
collectImmediately(detailModel.genreList, detailAdapter::submitList)
collectImmediately(detailModel.genreList, detailAdapter::diffList)
collectImmediately(
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
collect(navModel.exploreNavigationItem, ::handleNavigation)
@ -189,7 +189,7 @@ class GenreDetailFragment :
if (parent is Genre && parent.uid == unlikelyToBeNull(detailModel.currentGenre.value).uid) {
playingMusic = song
}
detailAdapter.setPlayingItem(playingMusic, isPlaying)
detailAdapter.setPlaying(playingMusic, isPlaying)
}
private fun handleNavigation(item: Music?) {
@ -217,7 +217,7 @@ class GenreDetailFragment :
}
private fun updateSelection(selected: List<Music>) {
detailAdapter.setSelectedItems(selected)
detailAdapter.setSelected(selected.toSet())
requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size)
}
}

View file

@ -57,7 +57,7 @@ class AlbumDetailAdapter(private val listener: Listener) : DetailAdapter(listene
}
override fun getItemViewType(position: Int) =
when (differ.currentList[position]) {
when (getItem(position)) {
// Support the Album header, sub-headers for each disc, and special album songs.
is Album -> AlbumDetailViewHolder.VIEW_TYPE
is DiscHeader -> DiscHeaderViewHolder.VIEW_TYPE
@ -75,7 +75,7 @@ class AlbumDetailAdapter(private val listener: Listener) : DetailAdapter(listene
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
super.onBindViewHolder(holder, position)
when (val item = differ.currentList[position]) {
when (val item = getItem(position)) {
is Album -> (holder as AlbumDetailViewHolder).bind(item, listener)
is DiscHeader -> (holder as DiscHeaderViewHolder).bind(item)
is Song -> (holder as AlbumSongViewHolder).bind(item, listener)
@ -83,9 +83,12 @@ class AlbumDetailAdapter(private val listener: Listener) : DetailAdapter(listene
}
override fun isItemFullWidth(position: Int): Boolean {
if (super.isItemFullWidth(position)) {
return true
}
// The album and disc headers should be full-width in all configurations.
val item = differ.currentList[position]
return super.isItemFullWidth(position) || item is Album || item is DiscHeader
val item = getItem(position)
return item is Album || item is DiscHeader
}
private companion object {

View file

@ -46,7 +46,7 @@ import org.oxycblt.auxio.util.inflater
class ArtistDetailAdapter(private val listener: Listener<Music>) :
DetailAdapter(listener, DIFF_CALLBACK) {
override fun getItemViewType(position: Int) =
when (differ.currentList[position]) {
when (getItem(position)) {
// Support an artist header, and special artist albums/songs.
is Artist -> ArtistDetailViewHolder.VIEW_TYPE
is Album -> ArtistAlbumViewHolder.VIEW_TYPE
@ -65,7 +65,7 @@ class ArtistDetailAdapter(private val listener: Listener<Music>) :
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
super.onBindViewHolder(holder, position)
// Re-binding an item with new data and not just a changed selection/playing state.
when (val item = differ.currentList[position]) {
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)
@ -73,9 +73,11 @@ class ArtistDetailAdapter(private val listener: Listener<Music>) :
}
override fun isItemFullWidth(position: Int): Boolean {
if (super.isItemFullWidth(position)) {
return true
}
// Artist headers should be full-width in all configurations.
val item = differ.currentList[position]
return super.isItemFullWidth(position) || item is Artist
return getItem(position) is Artist
}
private companion object {

View file

@ -20,7 +20,6 @@ package org.oxycblt.auxio.detail.recycler
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.TooltipCompat
import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.IntegerTable
@ -37,19 +36,19 @@ import org.oxycblt.auxio.util.inflater
/**
* A [RecyclerView.Adapter] that implements behavior shared across each detail view's adapters.
* @param listener A [Listener] to bind interactions to.
* @param itemCallback A [DiffUtil.ItemCallback] to use with [AsyncListDiffer] when updating the
* @param diffCallback A [DiffUtil.ItemCallback] to use for item comparison when diffing the
* internal list.
* @author Alexander Capehart (OxygenCobalt)
*/
abstract class DetailAdapter(
private val listener: Listener<*>,
itemCallback: DiffUtil.ItemCallback<Item>
) : SelectionIndicatorAdapter<RecyclerView.ViewHolder>(), AuxioRecyclerView.SpanSizeLookup {
// Safe to leak this since the listener will not fire during initialization
@Suppress("LeakingThis") protected val differ = AsyncListDiffer(this, itemCallback)
diffCallback: DiffUtil.ItemCallback<Item>
) :
SelectionIndicatorAdapter<Item, RecyclerView.ViewHolder>(ListDiffer.Async(diffCallback)),
AuxioRecyclerView.SpanSizeLookup {
override fun getItemViewType(position: Int) =
when (differ.currentList[position]) {
when (getItem(position)) {
// Implement support for headers and sort headers
is Header -> HeaderViewHolder.VIEW_TYPE
is SortHeader -> SortHeaderViewHolder.VIEW_TYPE
@ -64,7 +63,7 @@ abstract class DetailAdapter(
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (val item = differ.currentList[position]) {
when (val item = getItem(position)) {
is Header -> (holder as HeaderViewHolder).bind(item)
is SortHeader -> (holder as SortHeaderViewHolder).bind(item, listener)
}
@ -72,22 +71,10 @@ abstract class DetailAdapter(
override fun isItemFullWidth(position: Int): Boolean {
// Headers should be full-width in all configurations.
val item = differ.currentList[position]
val item = getItem(position)
return item is Header || item is SortHeader
}
override val currentList: List<Item>
get() = differ.currentList
/**
* Asynchronously update the list with new items. Assumes that the list only contains data
* supported by the concrete [DetailAdapter] implementation.
* @param newList The new [Item]s for the adapter to display.
*/
fun submitList(newList: List<Item>) {
differ.submitList(newList)
}
/** An extended [SelectableListListener] for [DetailAdapter] implementations. */
interface Listener<in T : Music> : SelectableListListener<T> {
// TODO: Split off into sub-listeners if a collapsing toolbar is implemented.

View file

@ -44,7 +44,7 @@ import org.oxycblt.auxio.util.inflater
class GenreDetailAdapter(private val listener: Listener<Music>) :
DetailAdapter(listener, DIFF_CALLBACK) {
override fun getItemViewType(position: Int) =
when (differ.currentList[position]) {
when (getItem(position)) {
// Support the Genre header and generic Artist/Song items. There's nothing about
// a genre that will make the artists/songs homogeneous, so it doesn't matter what we
// use for their ViewHolders.
@ -64,7 +64,7 @@ class GenreDetailAdapter(private val listener: Listener<Music>) :
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
super.onBindViewHolder(holder, position)
when (val item = differ.currentList[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)
@ -72,9 +72,11 @@ class GenreDetailAdapter(private val listener: Listener<Music>) :
}
override fun isItemFullWidth(position: Int): Boolean {
if (super.isItemFullWidth(position)) {
return true
}
// Genre headers should be full-width in all configurations
val item = differ.currentList[position]
return super.isItemFullWidth(position) || item is Genre
return getItem(position) is Genre
}
private companion object {

View file

@ -31,8 +31,8 @@ import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView
import org.oxycblt.auxio.list.*
import org.oxycblt.auxio.list.ListFragment
import org.oxycblt.auxio.list.recycler.AlbumViewHolder
import org.oxycblt.auxio.list.recycler.ListDiffer
import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter
import org.oxycblt.auxio.list.recycler.SyncListDiffer
import org.oxycblt.auxio.music.*
import org.oxycblt.auxio.music.library.Sort
import org.oxycblt.auxio.playback.formatDurationMs
@ -67,7 +67,7 @@ class AlbumListFragment :
}
collectImmediately(homeModel.albumsList, albumAdapter::replaceList)
collectImmediately(selectionModel.selected, albumAdapter::setSelectedItems)
collectImmediately(selectionModel.selected, ::updateSelection)
collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
}
@ -130,9 +130,13 @@ class AlbumListFragment :
openMusicMenu(anchor, R.menu.menu_album_actions, item)
}
private fun updateSelection(selection: List<Music>) {
albumAdapter.setSelected(selection.filterIsInstanceTo(mutableSetOf()))
}
private fun updatePlayback(parent: MusicParent?, isPlaying: Boolean) {
// If an album is playing, highlight it within this adapter.
albumAdapter.setPlayingItem(parent as? Album, isPlaying)
albumAdapter.setPlaying(parent as? Album, isPlaying)
}
/**
@ -140,25 +144,14 @@ class AlbumListFragment :
* @param listener An [SelectableListListener] to bind interactions to.
*/
private class AlbumAdapter(private val listener: SelectableListListener<Album>) :
SelectionIndicatorAdapter<AlbumViewHolder>() {
private val differ = SyncListDiffer(this, AlbumViewHolder.DIFF_CALLBACK)
override val currentList: List<Item>
get() = differ.currentList
SelectionIndicatorAdapter<Album, AlbumViewHolder>(
ListDiffer.Async(AlbumViewHolder.DIFF_CALLBACK)) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
AlbumViewHolder.from(parent)
override fun onBindViewHolder(holder: AlbumViewHolder, position: Int) {
holder.bind(differ.currentList[position], listener)
}
/**
* Asynchronously update the list with new [Album]s.
* @param newList The new [Album]s for the adapter to display.
*/
fun replaceList(newList: List<Album>) {
differ.replaceList(newList)
holder.bind(getItem(position), listener)
}
}
}

View file

@ -29,9 +29,10 @@ import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView
import org.oxycblt.auxio.list.*
import org.oxycblt.auxio.list.ListFragment
import org.oxycblt.auxio.list.recycler.ArtistViewHolder
import org.oxycblt.auxio.list.recycler.ListDiffer
import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter
import org.oxycblt.auxio.list.recycler.SyncListDiffer
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.library.Sort
@ -48,7 +49,7 @@ class ArtistListFragment :
FastScrollRecyclerView.PopupProvider,
FastScrollRecyclerView.Listener {
private val homeModel: HomeViewModel by activityViewModels()
private val homeAdapter = ArtistAdapter(this)
private val artistAdapter = ArtistAdapter(this)
override fun onCreateBinding(inflater: LayoutInflater) =
FragmentHomeListBinding.inflate(inflater)
@ -58,13 +59,13 @@ class ArtistListFragment :
binding.homeRecycler.apply {
id = R.id.home_artist_recycler
adapter = homeAdapter
adapter = artistAdapter
popupProvider = this@ArtistListFragment
listener = this@ArtistListFragment
}
collectImmediately(homeModel.artistsList, homeAdapter::replaceList)
collectImmediately(selectionModel.selected, homeAdapter::setSelectedItems)
collectImmediately(homeModel.artistsList, artistAdapter::replaceList)
collectImmediately(selectionModel.selected, ::updateSelection)
collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
}
@ -107,9 +108,13 @@ class ArtistListFragment :
openMusicMenu(anchor, R.menu.menu_artist_actions, item)
}
private fun updateSelection(selection: List<Music>) {
artistAdapter.setSelected(selection.filterIsInstanceTo(mutableSetOf()))
}
private fun updatePlayback(parent: MusicParent?, isPlaying: Boolean) {
// If an artist is playing, highlight it within this adapter.
homeAdapter.setPlayingItem(parent as? Artist, isPlaying)
artistAdapter.setPlaying(parent as? Artist, isPlaying)
}
/**
@ -117,25 +122,14 @@ class ArtistListFragment :
* @param listener An [SelectableListListener] to bind interactions to.
*/
private class ArtistAdapter(private val listener: SelectableListListener<Artist>) :
SelectionIndicatorAdapter<ArtistViewHolder>() {
private val differ = SyncListDiffer(this, ArtistViewHolder.DIFF_CALLBACK)
override val currentList: List<Item>
get() = differ.currentList
SelectionIndicatorAdapter<Artist, ArtistViewHolder>(
ListDiffer.Async(ArtistViewHolder.DIFF_CALLBACK)) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
ArtistViewHolder.from(parent)
override fun onBindViewHolder(holder: ArtistViewHolder, position: Int) {
holder.bind(differ.currentList[position], listener)
}
/**
* Asynchronously update the list with new [Artist]s.
* @param newList The new [Artist]s for the adapter to display.
*/
fun replaceList(newList: List<Artist>) {
differ.replaceList(newList)
holder.bind(getItem(position), listener)
}
}
}

View file

@ -29,9 +29,10 @@ import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView
import org.oxycblt.auxio.list.*
import org.oxycblt.auxio.list.ListFragment
import org.oxycblt.auxio.list.recycler.GenreViewHolder
import org.oxycblt.auxio.list.recycler.ListDiffer
import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter
import org.oxycblt.auxio.list.recycler.SyncListDiffer
import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.library.Sort
@ -47,7 +48,7 @@ class GenreListFragment :
FastScrollRecyclerView.PopupProvider,
FastScrollRecyclerView.Listener {
private val homeModel: HomeViewModel by activityViewModels()
private val homeAdapter = GenreAdapter(this)
private val genreAdapter = GenreAdapter(this)
override fun onCreateBinding(inflater: LayoutInflater) =
FragmentHomeListBinding.inflate(inflater)
@ -57,13 +58,13 @@ class GenreListFragment :
binding.homeRecycler.apply {
id = R.id.home_genre_recycler
adapter = homeAdapter
adapter = genreAdapter
popupProvider = this@GenreListFragment
listener = this@GenreListFragment
}
collectImmediately(homeModel.genresList, homeAdapter::replaceList)
collectImmediately(selectionModel.selected, homeAdapter::setSelectedItems)
collectImmediately(homeModel.genresList, genreAdapter::replaceList)
collectImmediately(selectionModel.selected, ::updateSelection)
collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
}
@ -106,9 +107,13 @@ class GenreListFragment :
openMusicMenu(anchor, R.menu.menu_artist_actions, item)
}
private fun updateSelection(selection: List<Music>) {
genreAdapter.setSelected(selection.filterIsInstanceTo(mutableSetOf()))
}
private fun updatePlayback(parent: MusicParent?, isPlaying: Boolean) {
// If a genre is playing, highlight it within this adapter.
homeAdapter.setPlayingItem(parent as? Genre, isPlaying)
genreAdapter.setPlaying(parent as? Genre, isPlaying)
}
/**
@ -116,25 +121,13 @@ class GenreListFragment :
* @param listener An [SelectableListListener] to bind interactions to.
*/
private class GenreAdapter(private val listener: SelectableListListener<Genre>) :
SelectionIndicatorAdapter<GenreViewHolder>() {
private val differ = SyncListDiffer(this, GenreViewHolder.DIFF_CALLBACK)
override val currentList: List<Item>
get() = differ.currentList
SelectionIndicatorAdapter<Genre, GenreViewHolder>(
ListDiffer.Async(GenreViewHolder.DIFF_CALLBACK)) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
GenreViewHolder.from(parent)
override fun onBindViewHolder(holder: GenreViewHolder, position: Int) {
holder.bind(differ.currentList[position], listener)
}
/**
* Asynchronously update the list with new [Genre]s.
* @param newList The new [Genre]s for the adapter to display.
*/
fun replaceList(newList: List<Genre>) {
differ.replaceList(newList)
holder.bind(getItem(position), listener)
}
}
}

View file

@ -30,9 +30,10 @@ import org.oxycblt.auxio.home.HomeViewModel
import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView
import org.oxycblt.auxio.list.*
import org.oxycblt.auxio.list.ListFragment
import org.oxycblt.auxio.list.recycler.ListDiffer
import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter
import org.oxycblt.auxio.list.recycler.SongViewHolder
import org.oxycblt.auxio.list.recycler.SyncListDiffer
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song
@ -50,7 +51,7 @@ class SongListFragment :
FastScrollRecyclerView.PopupProvider,
FastScrollRecyclerView.Listener {
private val homeModel: HomeViewModel by activityViewModels()
private val homeAdapter = SongAdapter(this)
private val songAdapter = SongAdapter(this)
// Save memory by re-using the same formatter and string builder when creating popup text
private val formatterSb = StringBuilder(64)
private val formatter = Formatter(formatterSb)
@ -63,13 +64,13 @@ class SongListFragment :
binding.homeRecycler.apply {
id = R.id.home_song_recycler
adapter = homeAdapter
adapter = songAdapter
popupProvider = this@SongListFragment
listener = this@SongListFragment
}
collectImmediately(homeModel.songLists, homeAdapter::replaceList)
collectImmediately(selectionModel.selected, homeAdapter::setSelectedItems)
collectImmediately(homeModel.songLists, songAdapter::replaceList)
collectImmediately(selectionModel.selected, ::updateSelection)
collectImmediately(
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
}
@ -136,12 +137,16 @@ class SongListFragment :
openMusicMenu(anchor, R.menu.menu_song_actions, item)
}
private fun updateSelection(selection: List<Music>) {
songAdapter.setSelected(selection.filterIsInstanceTo(mutableSetOf()))
}
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
if (parent == null) {
homeAdapter.setPlayingItem(song, isPlaying)
songAdapter.setPlaying(song, isPlaying)
} else {
// Ignore playback that is not from all songs
homeAdapter.setPlayingItem(null, isPlaying)
songAdapter.setPlaying(null, isPlaying)
}
}
@ -150,25 +155,14 @@ class SongListFragment :
* @param listener An [SelectableListListener] to bind interactions to.
*/
private class SongAdapter(private val listener: SelectableListListener<Song>) :
SelectionIndicatorAdapter<SongViewHolder>() {
private val differ = SyncListDiffer(this, SongViewHolder.DIFF_CALLBACK)
override val currentList: List<Item>
get() = differ.currentList
SelectionIndicatorAdapter<Song, SongViewHolder>(
ListDiffer.Async(SongViewHolder.DIFF_CALLBACK)) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
SongViewHolder.from(parent)
override fun onBindViewHolder(holder: SongViewHolder, position: Int) {
holder.bind(differ.currentList[position], listener)
}
/**
* Asynchronously update the list with new [Song]s.
* @param newList The new [Song]s for the adapter to display.
*/
fun replaceList(newList: List<Song>) {
differ.replaceList(newList)
holder.bind(getItem(position), listener)
}
}
}

View file

@ -1,3 +1,20 @@
/*
* 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.list
/**
@ -12,8 +29,8 @@ enum class UpdateInstructions {
DIFF,
/**
* Synchronously remove the current list and replace it with a new one. This should be used
* for large diffs with that would cause erratic scroll behavior or in-efficiency.
* Synchronously remove the current list and replace it with a new one. This should be used for
* large diffs with that would cause erratic scroll behavior or in-efficiency.
*/
REPLACE
}
}

View file

@ -0,0 +1,71 @@
/*
* 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.list.recycler
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.list.UpdateInstructions
/**
* A [RecyclerView.Adapter] with [ListDiffer] integration.
* @param differFactory The [ListDiffer.Factory] that defines the type of [ListDiffer] to use.
*/
abstract class DiffAdapter<T, VH : RecyclerView.ViewHolder>(differFactory: ListDiffer.Factory<T>) :
RecyclerView.Adapter<VH>() {
private val differ = differFactory.new(@Suppress("LeakingThis") this)
final override fun getItemCount() = differ.currentList.size
/** The current list of [T] items. */
val currentList: List<T>
get() = differ.currentList
/**
* Get a [T] item at the given position.
* @param at The position to get the item at.
* @throws IndexOutOfBoundsException If the index is not in the list bounds/
*/
fun getItem(at: Int) = differ.currentList[at]
/**
* Dynamically determine how to update the list based on the given [UpdateInstructions].
* @param newList The new list of [T] items to show.
* @param instructions The [UpdateInstructions] specifying how to update the list.
*/
fun submitList(newList: List<T>, instructions: UpdateInstructions) {
when (instructions) {
UpdateInstructions.DIFF -> diffList(newList)
UpdateInstructions.REPLACE -> replaceList(newList)
}
}
/**
* Update this list using [DiffUtil]. This can simplify the work of updating the list, but can
* also cause erratic behavior.
* @param newList The new list of [T] items to show.
* @param onDone Callback that will be invoked when the update is completed, allowing means to
* reset the state.
*/
fun diffList(newList: List<T>, onDone: () -> Unit = {}) = differ.diffList(newList, onDone)
/**
* Visually replace the previous list with a new list. This is useful for large diffs that are
* too erratic for [diffList].
* @param newList The new list of [T] items to show.
*/
fun replaceList(newList: List<T>) = differ.replaceList(newList)
}

View file

@ -0,0 +1,224 @@
/*
* 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.list.recycler
import androidx.recyclerview.widget.AdapterListUpdateCallback
import androidx.recyclerview.widget.AsyncDifferConfig
import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListUpdateCallback
import androidx.recyclerview.widget.RecyclerView
import java.lang.reflect.Field
import org.oxycblt.auxio.util.lazyReflectedField
import org.oxycblt.auxio.util.requireIs
/**
* List differ wrapper that provides more flexibility regarding the way lists are updated.
* @author Alexander Capehart (OxygenCobalt)
*/
interface ListDiffer<T> {
/** The current list of [T] items. */
val currentList: List<T>
/**
* Update this list using [DiffUtil]. This can simplify the work of updating the list, but can
* also cause erratic behavior.
* @param newList The new list of [T] items to show.
* @param onDone Callback that will be invoked when the update is completed, allowing means to
* reset the state.
*/
fun diffList(newList: List<T>, onDone: () -> Unit = {})
/**
* Visually replace the previous list with a new list. This is useful for large diffs that are
* too erratic for [diffList].
* @param newList The new list of [T] items to show.
*/
fun replaceList(newList: List<T>)
/**
* Defines the creation of new [ListDiffer] instances. Allows such [ListDiffer]s to be passed as
* arguments without reliance on a `this` [RecyclerView.Adapter].
*/
abstract class Factory<T> {
/**
* Create a new [ListDiffer] bound to the given [RecyclerView.Adapter].
* @param adapter The [RecyclerView.Adapter] to bind to.
*/
abstract fun new(adapter: RecyclerView.Adapter<*>): ListDiffer<T>
}
/**
* Update lists on another thread. This is useful when large diffs are likely to occur in this
* list that would be exceedingly slow with [Blocking].
* @param diffCallback A [DiffUtil.ItemCallback] to use for item comparison when diffing the
* internal list.
*/
class Async<T>(private val diffCallback: DiffUtil.ItemCallback<T>) : Factory<T>() {
override fun new(adapter: RecyclerView.Adapter<*>): ListDiffer<T> =
RealAsyncListDiffer(AdapterListUpdateCallback(adapter), diffCallback)
}
/**
* Update lists on the main thread. This is useful when many small, discrete list diffs are
* likely to occur that would cause [Async] to get race conditions.
* @param diffCallback A [DiffUtil.ItemCallback] to use for item comparison when diffing the
* internal list.
*/
class Blocking<T>(private val diffCallback: DiffUtil.ItemCallback<T>) : Factory<T>() {
override fun new(adapter: RecyclerView.Adapter<*>): ListDiffer<T> =
RealBlockingListDiffer(AdapterListUpdateCallback(adapter), diffCallback)
}
}
private class RealAsyncListDiffer<T>(
private val updateCallback: ListUpdateCallback,
diffCallback: DiffUtil.ItemCallback<T>
) : ListDiffer<T> {
private val inner =
AsyncListDiffer(updateCallback, AsyncDifferConfig.Builder(diffCallback).build())
override val currentList: List<T>
get() = inner.currentList
override fun diffList(newList: List<T>, onDone: () -> Unit) {
inner.submitList(newList, onDone)
}
override fun replaceList(newList: List<T>) {
if (inner.currentList == newList) {
// Nothing to do.
return
}
// Do possibly the most idiotic thing possible and mutate the internal differ state
// so we don't have to deal with any disjoint list garbage. This should cancel any prior
// updates and correctly set up the list values while still allowing for the same
// visual animation as the blocking replaceList.
val oldListSize = inner.currentList.size
ASD_MAX_GENERATION_FIELD.set(inner, requireIs<Int>(ASD_MAX_GENERATION_FIELD.get(inner)) + 1)
ASD_MUTABLE_LIST_FIELD.set(inner, newList.ifEmpty { null })
ASD_READ_ONLY_LIST_FIELD.set(inner, newList)
updateCallback.onRemoved(0, oldListSize)
updateCallback.onInserted(0, newList.size)
}
private companion object {
val ASD_MAX_GENERATION_FIELD: Field by
lazyReflectedField(AsyncListDiffer::class, "mMaxScheduledGeneration")
val ASD_MUTABLE_LIST_FIELD: Field by lazyReflectedField(AsyncListDiffer::class, "mList")
val ASD_READ_ONLY_LIST_FIELD: Field by
lazyReflectedField(AsyncListDiffer::class, "mReadOnlyList")
}
}
private class RealBlockingListDiffer<T>(
private val updateCallback: ListUpdateCallback,
private val diffCallback: DiffUtil.ItemCallback<T>
) : ListDiffer<T> {
override var currentList = listOf<T>()
override fun diffList(newList: List<T>, onDone: () -> Unit) {
if (newList === currentList || newList.isEmpty() && currentList.isEmpty()) {
onDone()
return
}
if (newList.isEmpty()) {
val oldListSize = currentList.size
currentList = listOf()
updateCallback.onRemoved(0, oldListSize)
onDone()
return
}
if (currentList.isEmpty()) {
currentList = newList
updateCallback.onInserted(0, newList.size)
onDone()
return
}
val oldList = currentList
val result =
DiffUtil.calculateDiff(
object : DiffUtil.Callback() {
override fun getOldListSize(): Int {
return oldList.size
}
override fun getNewListSize(): Int {
return newList.size
}
override fun areItemsTheSame(
oldItemPosition: Int,
newItemPosition: Int
): Boolean {
val oldItem: T? = oldList[oldItemPosition]
val newItem: T? = newList[newItemPosition]
return if (oldItem != null && newItem != null) {
diffCallback.areItemsTheSame(oldItem, newItem)
} else {
oldItem == null && newItem == null
}
}
override fun areContentsTheSame(
oldItemPosition: Int,
newItemPosition: Int
): Boolean {
val oldItem: T? = oldList[oldItemPosition]
val newItem: T? = newList[newItemPosition]
return if (oldItem != null && newItem != null) {
diffCallback.areContentsTheSame(oldItem, newItem)
} else if (oldItem == null && newItem == null) {
true
} else {
throw AssertionError()
}
}
override fun getChangePayload(
oldItemPosition: Int,
newItemPosition: Int
): Any? {
val oldItem: T? = oldList[oldItemPosition]
val newItem: T? = newList[newItemPosition]
return if (oldItem != null && newItem != null) {
diffCallback.getChangePayload(oldItem, newItem)
} else {
throw AssertionError()
}
}
})
currentList = newList
result.dispatchUpdatesTo(updateCallback)
onDone()
}
override fun replaceList(newList: List<T>) {
if (currentList == newList) {
// Nothing to do.
return
}
diffList(listOf())
diffList(newList)
}
}

View file

@ -19,33 +19,27 @@ package org.oxycblt.auxio.list.recycler
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.util.logD
/**
* A [RecyclerView.Adapter] that supports indicating the playback status of a particular item.
* @param differFactory The [ListDiffer.Factory] that defines the type of [ListDiffer] to use.
* @author Alexander Capehart (OxygenCobalt)
*/
abstract class PlayingIndicatorAdapter<VH : RecyclerView.ViewHolder> : RecyclerView.Adapter<VH>() {
abstract class PlayingIndicatorAdapter<T, VH : RecyclerView.ViewHolder>(
differFactory: ListDiffer.Factory<T>
) : DiffAdapter<T, VH>(differFactory) {
// There are actually two states for this adapter:
// - The currently playing item, which is usually marked as "selected" and becomes accented.
// - Whether playback is ongoing, which corresponds to whether the item's ImageGroup is
// marked as "playing" or not.
private var currentMusic: Music? = null
private var currentItem: T? = null
private var isPlaying = false
/**
* The current list of the adapter. This is used to update items if the indicator state changes.
*/
abstract val currentList: List<Item>
override fun getItemCount() = currentList.size
override fun onBindViewHolder(holder: VH, position: Int, payloads: List<Any>) {
// Only try to update the playing indicator if the ViewHolder supports it
if (holder is ViewHolder) {
holder.updatePlayingIndicator(currentList[position] == currentMusic, isPlaying)
holder.updatePlayingIndicator(currentList[position] == currentItem, isPlaying)
}
if (payloads.isEmpty()) {
@ -56,14 +50,14 @@ abstract class PlayingIndicatorAdapter<VH : RecyclerView.ViewHolder> : RecyclerV
}
/**
* Update the currently playing item in the list.
* @param music The [Music] currently being played, or null if it is not being played.
* @param item The [T] currently being played, or null if it is not being played.
* @param isPlaying Whether playback is ongoing or paused.
*/
fun setPlayingItem(music: Music?, isPlaying: Boolean) {
fun setPlaying(item: T?, isPlaying: Boolean) {
var updatedItem = false
if (currentMusic != music) {
val oldItem = currentMusic
currentMusic = music
if (currentItem != item) {
val oldItem = currentItem
currentItem = item
// Remove the playing indicator from the old item
if (oldItem != null) {
@ -76,8 +70,8 @@ abstract class PlayingIndicatorAdapter<VH : RecyclerView.ViewHolder> : RecyclerV
}
// Enable the playing indicator on the new item
if (music != null) {
val pos = currentList.indexOfFirst { it == music }
if (item != null) {
val pos = currentList.indexOfFirst { it == item }
if (pos > -1) {
notifyItemChanged(pos, PAYLOAD_PLAYING_INDICATOR_CHANGED)
} else {
@ -94,8 +88,8 @@ abstract class PlayingIndicatorAdapter<VH : RecyclerView.ViewHolder> : RecyclerV
// We may have already called notifyItemChanged before when checking
// if the item was being played, so in that case we don't need to
// update again here.
if (!updatedItem && music != null) {
val pos = currentList.indexOfFirst { it == music }
if (!updatedItem && item != null) {
val pos = currentList.indexOfFirst { it == item }
if (pos > -1) {
notifyItemChanged(pos, PAYLOAD_PLAYING_INDICATOR_CHANGED)
} else {

View file

@ -24,11 +24,13 @@ import org.oxycblt.auxio.music.Music
/**
* A [PlayingIndicatorAdapter] that also supports indicating the selection status of a group of
* items.
* @param differFactory The [ListDiffer.Factory] that defines the type of [ListDiffer] to use.
* @author Alexander Capehart (OxygenCobalt)
*/
abstract class SelectionIndicatorAdapter<VH : RecyclerView.ViewHolder> :
PlayingIndicatorAdapter<VH>() {
private var selectedItems = setOf<Music>()
abstract class SelectionIndicatorAdapter<T, VH : RecyclerView.ViewHolder>(
differFactory: ListDiffer.Factory<T>
) : PlayingIndicatorAdapter<T, VH>(differFactory) {
private var selectedItems = setOf<T>()
override fun onBindViewHolder(holder: VH, position: Int, payloads: List<Any>) {
super.onBindViewHolder(holder, position, payloads)
@ -39,9 +41,9 @@ abstract class SelectionIndicatorAdapter<VH : RecyclerView.ViewHolder> :
/**
* Update the list of selected items.
* @param items A list of selected [Music].
* @param items A set of selected [T] items.
*/
fun setSelectedItems(items: List<Music>) {
fun setSelected(items: Set<T>) {
val oldSelectedItems = selectedItems
val newSelectedItems = items.toSet()
if (newSelectedItems == oldSelectedItems) {

View file

@ -64,8 +64,7 @@ class MetadataExtractor(
/**
* Returns a flow that parses all [Song.Raw] instances queued by the sub-extractors. This will
* first delegate to the sub-extractors before parsing the metadata itself.
* @param emit A listener that will be invoked with every new [Song.Raw] instance when they are
* successfully loaded.
* @return A flow of [Song.Raw] instances.
*/
fun extract() = flow {
while (true) {
@ -310,8 +309,9 @@ class Task(context: Context, private val raw: Song.Raw) {
// Album artist
comments["musicbrainz_albumartistid"]?.let { raw.albumArtistMusicBrainzIds = it }
(comments["albumartists"] ?: comments["albumartist"])?.let { raw.albumArtistNames = it }
(comments["albumartists_sort"] ?: comments["albumartistsort"])
?.let { raw.albumArtistSortNames = it }
(comments["albumartists_sort"] ?: comments["albumartistsort"])?.let {
raw.albumArtistSortNames = it
}
// Genre
comments["genre"]?.let { raw.genreNames = it }

View file

@ -199,7 +199,7 @@ interface PlaybackSettings : Settings<PlaybackSettings.Listener> {
}
}
companion object {
private companion object {
const val OLD_KEY_ALT_NOTIF_ACTION = "KEY_ALT_NOTIF_ACTION"
const val OLD_KEY_LIB_PLAYBACK_MODE = "KEY_SONG_PLAY_MODE2"
const val OLD_KEY_DETAIL_PLAYBACK_MODE = "auxio_detail_song_play_mode"

View file

@ -27,9 +27,10 @@ import com.google.android.material.shape.MaterialShapeDrawable
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.ItemQueueSongBinding
import org.oxycblt.auxio.list.EditableListListener
import org.oxycblt.auxio.list.recycler.DiffAdapter
import org.oxycblt.auxio.list.recycler.ListDiffer
import org.oxycblt.auxio.list.recycler.PlayingIndicatorAdapter
import org.oxycblt.auxio.list.recycler.SongViewHolder
import org.oxycblt.auxio.list.recycler.SyncListDiffer
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.util.*
@ -39,16 +40,13 @@ import org.oxycblt.auxio.util.*
* @author Alexander Capehart (OxygenCobalt)
*/
class QueueAdapter(private val listener: EditableListListener<Song>) :
RecyclerView.Adapter<QueueSongViewHolder>() {
private var differ = SyncListDiffer(this, QueueSongViewHolder.DIFF_CALLBACK)
DiffAdapter<Song, QueueSongViewHolder>(ListDiffer.Blocking(QueueSongViewHolder.DIFF_CALLBACK)) {
// Since PlayingIndicator adapter relies on an item value, we cannot use it for this
// adapter, as one item can appear at several points in the UI. Use a similar implementation
// with an index value instead.
private var currentIndex = 0
private var isPlaying = false
override fun getItemCount() = differ.currentList.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
QueueSongViewHolder.from(parent)
@ -61,31 +59,13 @@ class QueueAdapter(private val listener: EditableListListener<Song>) :
payload: List<Any>
) {
if (payload.isEmpty()) {
viewHolder.bind(differ.currentList[position], listener)
viewHolder.bind(getItem(position), listener)
}
viewHolder.isFuture = position > currentIndex
viewHolder.updatePlayingIndicator(position == currentIndex, isPlaying)
}
/**
* Synchronously update the list with new items. This is exceedingly slow for large diffs, so
* only use it for trivial updates.
* @param newList The new [Song]s for the adapter to display.
*/
fun submitList(newList: List<Song>) {
differ.submitList(newList)
}
/**
* Replace the list with a new list. This is exceedingly slow for large diffs, so only use it
* for trivial updates.
* @param newList The new [Song]s for the adapter to display.
*/
fun replaceList(newList: List<Song>) {
differ.replaceList(newList)
}
/**
* Set the position of the currently playing item in the queue. This will mark the item as
* playing and any previous items as played.

View file

@ -33,7 +33,6 @@ import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.ViewBindingFragment
import org.oxycblt.auxio.util.androidActivityViewModels
import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.logD
/**
* A [ViewBindingFragment] that displays an editable queue.
@ -102,13 +101,7 @@ class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), EditableListL
// Replace or diff the queue depending on the type of change it is.
val instructions = queueModel.instructions
if (instructions?.update == UpdateInstructions.REPLACE) {
logD("Replacing queue")
queueAdapter.replaceList(queue)
} else {
logD("Diffing queue")
queueAdapter.submitList(queue)
}
queueAdapter.submitList(queue, instructions?.update ?: UpdateInstructions.DIFF)
// Update position in list (and thus past/future items)
queueAdapter.setPosition(index, isPlaying)

View file

@ -165,7 +165,7 @@ class PlaybackStateDatabase private constructor(context: Context) :
fun write(state: SavedState?) {
requireBackgroundThread()
// Only bother saving a state if a song is actively playing from one.
// This is not the case with a null state or a state with an out-of-bounds index.
// This is not the case with a null state.
if (state != null) {
// Transform saved state into raw state, which can then be written to the database.
val rawPlaybackState =

View file

@ -18,7 +18,6 @@
package org.oxycblt.auxio.search
import android.view.ViewGroup
import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.list.*
import org.oxycblt.auxio.list.recycler.*
@ -30,14 +29,11 @@ import org.oxycblt.auxio.music.*
* @author Alexander Capehart (OxygenCobalt)
*/
class SearchAdapter(private val listener: SelectableListListener<Music>) :
SelectionIndicatorAdapter<RecyclerView.ViewHolder>(), AuxioRecyclerView.SpanSizeLookup {
private val differ = AsyncListDiffer(this, DIFF_CALLBACK)
override val currentList: List<Item>
get() = differ.currentList
SelectionIndicatorAdapter<Item, RecyclerView.ViewHolder>(ListDiffer.Async(DIFF_CALLBACK)),
AuxioRecyclerView.SpanSizeLookup {
override fun getItemViewType(position: Int) =
when (differ.currentList[position]) {
when (getItem(position)) {
is Song -> SongViewHolder.VIEW_TYPE
is Album -> AlbumViewHolder.VIEW_TYPE
is Artist -> ArtistViewHolder.VIEW_TYPE
@ -57,7 +53,7 @@ class SearchAdapter(private val listener: SelectableListListener<Music>) :
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (val item = differ.currentList[position]) {
when (val item = getItem(position)) {
is Song -> (holder as SongViewHolder).bind(item, listener)
is Album -> (holder as AlbumViewHolder).bind(item, listener)
is Artist -> (holder as ArtistViewHolder).bind(item, listener)
@ -66,17 +62,7 @@ class SearchAdapter(private val listener: SelectableListListener<Music>) :
}
}
override fun isItemFullWidth(position: Int) = differ.currentList[position] is Header
/**
* Asynchronously update the list with new items. Assumes that the list only contains supported
* data..
* @param newList The new [Item]s for the adapter to display.
* @param callback A block called when the asynchronous update is completed.
*/
fun submitList(newList: List<Item>, callback: () -> Unit) {
differ.submitList(newList, callback)
}
override fun isItemFullWidth(position: Int) = getItem(position) is Header
private companion object {
/** A comparator that can be used with DiffUtil. */

View file

@ -153,7 +153,7 @@ class SearchFragment : ListFragment<Music, FragmentSearchBinding>() {
// Don't show the RecyclerView (and it's stray overscroll effects) when there
// are no results.
binding.searchRecycler.isInvisible = results.isEmpty()
searchAdapter.submitList(results.toMutableList()) {
searchAdapter.diffList(results.toMutableList()) {
// I would make it so that the position is only scrolled back to the top when
// the query actually changes instead of once every re-creation event, but sadly
// that doesn't seem possible.
@ -162,7 +162,7 @@ class SearchFragment : ListFragment<Music, FragmentSearchBinding>() {
}
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
searchAdapter.setPlayingItem(parent ?: song, isPlaying)
searchAdapter.setPlaying(parent ?: song, isPlaying)
}
private fun handleNavigation(item: Music?) {
@ -180,7 +180,7 @@ class SearchFragment : ListFragment<Music, FragmentSearchBinding>() {
}
private fun updateSelection(selected: List<Music>) {
searchAdapter.setSelectedItems(selected)
searchAdapter.setSelected(selected.toSet())
if (requireBinding().searchSelectionToolbar.updateSelectionAmount(selected.size) &&
selected.isNotEmpty()) {
// Make selection of obscured items easier by hiding the keyboard.

View file

@ -123,14 +123,15 @@ class AboutFragment : ViewBindingFragment<FragmentAboutBinding>() {
if (pkgName == "android") {
// No default browser [Must open app chooser, may not be supported]
openAppChooser(browserIntent)
} else try {
browserIntent.setPackage(pkgName)
startActivity(browserIntent)
} catch (e: ActivityNotFoundException) {
// Not a browser but an app chooser
browserIntent.setPackage(null)
openAppChooser(browserIntent)
}
} else
try {
browserIntent.setPackage(pkgName)
startActivity(browserIntent)
} catch (e: ActivityNotFoundException) {
// Not a browser but an app chooser
browserIntent.setPackage(null)
openAppChooser(browserIntent)
}
} else {
// No app installed to open the link
context.showToast(R.string.err_no_app)

View file

@ -75,10 +75,8 @@ interface UISettings : Settings<UISettings.Listener> {
var accent = sharedPreferences.getInt(OLD_KEY_ACCENT3, 5)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// Accents were previously frozen as soon as the OS was updated to android
// twelve,
// as dynamic colors were enabled by default. This is no longer the case, so we
// need
// to re-update the setting to dynamic colors here.
// twelve, as dynamic colors were enabled by default. This is no longer the
// case, so we need to re-update the setting to dynamic colors here.
accent = 16
}
@ -96,7 +94,7 @@ interface UISettings : Settings<UISettings.Listener> {
}
}
companion object {
private companion object {
const val OLD_KEY_ACCENT3 = "auxio_accent"
}
}

View file

@ -39,7 +39,8 @@ fun <T> unlikelyToBeNull(value: T?) =
* @return A data casted to [T].
* @throws IllegalStateException If the data cannot be casted to [T].
*/
inline fun <reified T> requireIs(data: Any): T {
inline fun <reified T> requireIs(data: Any?): T {
requireNotNull(data) { "Unexpected datatype: null" }
check(data is T) { "Unexpected datatype: ${data::class.simpleName}" }
return data
}

View file

@ -8,6 +8,8 @@
android:background="?attr/colorSurface"
android:transitionGroup="true">
<!-- TODO: Try to align the queue bar with the playback bar. -->
<androidx.fragment.app.FragmentContainerView
android:id="@+id/explore_nav_host"
android:name="androidx.navigation.fragment.NavHostFragment"