detail: diff instead of replace when resorting

Completely rework the detail list implementations so that resorting the
song list causes a replace operation instead of a diff operation.

This finally makes the list experience consistent across the app.
This commit is contained in:
Alexander Capehart 2023-01-18 16:45:13 -07:00
parent f7bf12c4a5
commit 0c69a35e80
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
31 changed files with 315 additions and 148 deletions

View file

@ -4,7 +4,7 @@
#### What's New #### What's New
- Added ability to play/shuffle selections - Added ability to play/shuffle selections
- Visually refreshed header components - Resigned header components
- Resigned settings view - Resigned settings view
#### What's Improved #### What's Improved
@ -13,6 +13,7 @@
- Pressing the button will now clear the current selection before navigating back - Pressing the button will now clear the current selection before navigating back
- Added support for non-standard `ARTISTS` tags - Added support for non-standard `ARTISTS` tags
- Play Next and Add To Queue now start playback if there is no queue to add - Play Next and Add To Queue now start playback if there is no queue to add
- Made resorting list animations consistent across app
#### What's Fixed #### What's Fixed
- Fixed unreliable ReplayGain adjustment application in certain situations - Fixed unreliable ReplayGain adjustment application in certain situations
@ -20,6 +21,8 @@
file manager file manager
- Fixed notification not updating due to settings changes - Fixed notification not updating due to settings changes
- Fixed genre picker from repeatedly showing up when device rotates - Fixed genre picker from repeatedly showing up when device rotates
- Fixed multi-value genres not being recognized on vorbis files
- Fixed sharp-cornered widget bar appearing even when round mode was enabled
#### What's Changed #### What's Changed
- Implemented new queue system - Implemented new queue system

View file

@ -83,7 +83,7 @@ import java.util.Map;
* window-like. For BottomSheetDialog use {@link BottomSheetDialog#setTitle(int)}, and for * window-like. For BottomSheetDialog use {@link BottomSheetDialog#setTitle(int)}, and for
* BottomSheetDialogFragment use {@link ViewCompat#setAccessibilityPaneTitle(View, CharSequence)}. * BottomSheetDialogFragment use {@link ViewCompat#setAccessibilityPaneTitle(View, CharSequence)}.
* *
* Modified at several points by Alexander Capehart backport miscellaneous fixes not currently * Modified at several points by Alexander Capehart to backport miscellaneous fixes not currently
* obtainable in the currently used MDC library. * obtainable in the currently used MDC library.
*/ */
public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayout.Behavior<V> { public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayout.Behavior<V> {

View file

@ -53,6 +53,9 @@ import com.google.android.material.resources.MaterialResources;
* layoutManager.getOrientation()); * layoutManager.getOrientation());
* recyclerView.addItemDecoration(dividerItemDecoration); * recyclerView.addItemDecoration(dividerItemDecoration);
* </pre> * </pre>
*
* Modified at several points by Alexander Capehart to backport miscellaneous fixes not currently
* obtainable in the currently used MDC library.
*/ */
public class BackportMaterialDividerItemDecoration extends ItemDecoration { public class BackportMaterialDividerItemDecoration extends ItemDecoration {
public static final int HORIZONTAL = LinearLayout.HORIZONTAL; public static final int HORIZONTAL = LinearLayout.HORIZONTAL;

View file

@ -31,7 +31,6 @@ import org.oxycblt.auxio.databinding.FragmentDetailBinding
import org.oxycblt.auxio.detail.recycler.AlbumDetailAdapter import org.oxycblt.auxio.detail.recycler.AlbumDetailAdapter
import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.list.ListFragment import org.oxycblt.auxio.list.ListFragment
import org.oxycblt.auxio.list.recycler.BasicInstructions
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
@ -141,12 +140,12 @@ class AlbumDetailFragment :
override fun onOpenSortMenu(anchor: View) { override fun onOpenSortMenu(anchor: View) {
openMenu(anchor, R.menu.menu_album_sort) { openMenu(anchor, R.menu.menu_album_sort) {
val sort = detailModel.albumSortSort val sort = detailModel.albumSongSort
unlikelyToBeNull(menu.findItem(sort.mode.itemId)).isChecked = true unlikelyToBeNull(menu.findItem(sort.mode.itemId)).isChecked = true
unlikelyToBeNull(menu.findItem(R.id.option_sort_asc)).isChecked = sort.isAscending unlikelyToBeNull(menu.findItem(R.id.option_sort_asc)).isChecked = sort.isAscending
setOnMenuItemClickListener { item -> setOnMenuItemClickListener { item ->
item.isChecked = !item.isChecked item.isChecked = !item.isChecked
detailModel.albumSortSort = detailModel.albumSongSort =
if (item.itemId == R.id.option_sort_asc) { if (item.itemId == R.id.option_sort_asc) {
sort.withAscending(item.isChecked) sort.withAscending(item.isChecked)
} else { } else {
@ -260,7 +259,9 @@ class AlbumDetailFragment :
} }
private fun updateList(items: List<Item>) { private fun updateList(items: List<Item>) {
detailAdapter.submitList(items, BasicInstructions.DIFF) detailAdapter.submitList(
items, detailModel.albumListInstructions ?: DetailListInstructions.Diff)
detailModel.finishAlbumListInstructions()
} }
private fun updateSelection(selected: List<Music>) { private fun updateSelection(selected: List<Music>) {

View file

@ -31,7 +31,6 @@ import org.oxycblt.auxio.detail.recycler.ArtistDetailAdapter
import org.oxycblt.auxio.detail.recycler.DetailAdapter import org.oxycblt.auxio.detail.recycler.DetailAdapter
import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.list.ListFragment import org.oxycblt.auxio.list.ListFragment
import org.oxycblt.auxio.list.recycler.BasicInstructions
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
@ -236,7 +235,9 @@ class ArtistDetailFragment :
} }
private fun updateList(items: List<Item>) { private fun updateList(items: List<Item>) {
detailAdapter.submitList(items, BasicInstructions.DIFF) detailAdapter.submitList(
items, detailModel.artistListInstructions ?: DetailListInstructions.Diff)
detailModel.finishArtistListInstructions()
} }
private fun updateSelection(selected: List<Music>) { private fun updateSelection(selected: List<Music>) {

View file

@ -25,12 +25,14 @@ import org.oxycblt.auxio.music.storage.MimeType
/** /**
* A header variation that displays a button to open a sort menu. * A header variation that displays a button to open a sort menu.
* @param titleRes The string resource to use as the header title * @param titleRes The string resource to use as the header title
* @author Alexander Capehart (OxygenCobalt)
*/ */
data class SortHeader(@StringRes val titleRes: Int) : Item data class SortHeader(@StringRes val titleRes: Int) : Item
/** /**
* A header variation that delimits between disc groups. * A header variation that delimits between disc groups.
* @param disc The disc number to be displayed on the header. * @param disc The disc number to be displayed on the header.
* @author Alexander Capehart (OxygenCobalt)
*/ */
data class DiscHeader(val disc: Int) : Item data class DiscHeader(val disc: Int) : Item
@ -39,9 +41,25 @@ data class DiscHeader(val disc: Int) : Item
* @param bitrateKbps The bit rate, in kilobytes-per-second. Null if it could not be parsed. * @param bitrateKbps The bit rate, in kilobytes-per-second. Null if it could not be parsed.
* @param sampleRateHz The sample rate, in hertz. * @param sampleRateHz The sample rate, in hertz.
* @param resolvedMimeType The known mime type of the [Song] after it's file format was determined. * @param resolvedMimeType The known mime type of the [Song] after it's file format was determined.
* @author Alexander Capehart (OxygenCobalt)
*/ */
data class SongProperties( data class SongProperties(
val bitrateKbps: Int?, val bitrateKbps: Int?,
val sampleRateHz: Int?, val sampleRateHz: Int?,
val resolvedMimeType: MimeType val resolvedMimeType: MimeType
) )
/**
* Represents the specific way to update a list of items in the detail lists.
* @author Alexander Capehart (OxygenCobalt)
*/
sealed class DetailListInstructions {
/** Do a plain asynchronous diff. */
object Diff : DetailListInstructions()
/**
* Replace all the items starting at the given index.
* @param at The index to start replacing at.
*/
data class ReplaceRest(val at: Int) : DetailListInstructions()
}

View file

@ -78,14 +78,18 @@ class DetailViewModel(application: Application) :
/** The current list data derived from [currentAlbum]. */ /** The current list data derived from [currentAlbum]. */
val albumList: StateFlow<List<Item>> val albumList: StateFlow<List<Item>>
get() = _albumList get() = _albumList
/** Specifies how to update [albumList] when it changes. */
var albumListInstructions: DetailListInstructions? = null
private set
/** The current [Sort] used for [Song]s in [albumList]. */ /** The current [Sort] used for [Song]s in [albumList]. */
var albumSortSort: Sort var albumSongSort: Sort
get() = musicSettings.albumSongSort get() = musicSettings.albumSongSort
set(value) { set(value) {
musicSettings.albumSongSort = value musicSettings.albumSongSort = value
// Refresh the album list to reflect the new sort. // Refresh the album list to reflect the new sort. Make sure we only visually replace
currentAlbum.value?.let(::refreshAlbumList) // the song information, however.
currentAlbum.value?.let { refreshAlbumList(it, true) }
} }
// --- ARTIST --- // --- ARTIST ---
@ -98,14 +102,18 @@ class DetailViewModel(application: Application) :
private val _artistList = MutableStateFlow(listOf<Item>()) private val _artistList = MutableStateFlow(listOf<Item>())
/** The current list derived from [currentArtist]. */ /** The current list derived from [currentArtist]. */
val artistList: StateFlow<List<Item>> = _artistList val artistList: StateFlow<List<Item>> = _artistList
/** Specifies how to update [artistList] when it changes. */
var artistListInstructions: DetailListInstructions? = null
private set
/** The current [Sort] used for [Song]s in [artistList]. */ /** The current [Sort] used for [Song]s in [artistList]. */
var artistSongSort: Sort var artistSongSort: Sort
get() = musicSettings.artistSongSort get() = musicSettings.artistSongSort
set(value) { set(value) {
musicSettings.artistSongSort = value musicSettings.artistSongSort = value
// Refresh the artist list to reflect the new sort. // Refresh the artist list to reflect the new sort. Make sure we only visually replace
currentArtist.value?.let(::refreshArtistList) // the song information, however.
currentArtist.value?.let { refreshArtistList(it, true) }
} }
// --- GENRE --- // --- GENRE ---
@ -118,14 +126,18 @@ class DetailViewModel(application: Application) :
private val _genreList = MutableStateFlow(listOf<Item>()) private val _genreList = MutableStateFlow(listOf<Item>())
/** The current list data derived from [currentGenre]. */ /** The current list data derived from [currentGenre]. */
val genreList: StateFlow<List<Item>> = _genreList val genreList: StateFlow<List<Item>> = _genreList
/** Specifies how to update [genreList] when it changes. */
var genreListInstructions: DetailListInstructions? = null
private set
/** The current [Sort] used for [Song]s in [genreList]. */ /** The current [Sort] used for [Song]s in [genreList]. */
var genreSongSort: Sort var genreSongSort: Sort
get() = musicSettings.genreSongSort get() = musicSettings.genreSongSort
set(value) { set(value) {
musicSettings.genreSongSort = value musicSettings.genreSongSort = value
// Refresh the genre list to reflect the new sort. // Refresh the genre list to reflect the new sort. Make sure we only visually replace
currentGenre.value?.let(::refreshGenreList) // the song information, however.
currentGenre.value?.let { refreshGenreList(it, true) }
} }
/** /**
@ -161,19 +173,19 @@ class DetailViewModel(application: Application) :
val album = currentAlbum.value val album = currentAlbum.value
if (album != null) { if (album != null) {
_currentAlbum.value = library.sanitize(album)?.also(::refreshAlbumList) _currentAlbum.value = library.sanitize(album)?.also { refreshAlbumList(it, false) }
logD("Updated genre to ${currentAlbum.value}") logD("Updated genre to ${currentAlbum.value}")
} }
val artist = currentArtist.value val artist = currentArtist.value
if (artist != null) { if (artist != null) {
_currentArtist.value = library.sanitize(artist)?.also(::refreshArtistList) _currentArtist.value = library.sanitize(artist)?.also { refreshArtistList(it, false) }
logD("Updated genre to ${currentArtist.value}") logD("Updated genre to ${currentArtist.value}")
} }
val genre = currentGenre.value val genre = currentGenre.value
if (genre != null) { if (genre != null) {
_currentGenre.value = library.sanitize(genre)?.also(::refreshGenreList) _currentGenre.value = library.sanitize(genre)?.also { refreshGenreList(it, false) }
logD("Updated genre to ${currentGenre.value}") logD("Updated genre to ${currentGenre.value}")
} }
} }
@ -203,7 +215,7 @@ class DetailViewModel(application: Application) :
return return
} }
logD("Opening Album [uid: $uid]") logD("Opening Album [uid: $uid]")
_currentAlbum.value = requireMusic<Album>(uid)?.also(::refreshAlbumList) _currentAlbum.value = requireMusic<Album>(uid)?.also { refreshAlbumList(it, false) }
} }
/** /**
@ -217,7 +229,7 @@ class DetailViewModel(application: Application) :
return return
} }
logD("Opening Artist [uid: $uid]") logD("Opening Artist [uid: $uid]")
_currentArtist.value = requireMusic<Artist>(uid)?.also(::refreshArtistList) _currentArtist.value = requireMusic<Artist>(uid)?.also { refreshArtistList(it, false) }
} }
/** /**
@ -231,7 +243,29 @@ class DetailViewModel(application: Application) :
return return
} }
logD("Opening Genre [uid: $uid]") logD("Opening Genre [uid: $uid]")
_currentGenre.value = requireMusic<Genre>(uid)?.also(::refreshGenreList) _currentGenre.value = requireMusic<Genre>(uid)?.also { refreshGenreList(it, false) }
}
/**
* Signal that the specified [DetailListInstructions] in [albumListInstructions] were performed.
*/
fun finishAlbumListInstructions() {
albumListInstructions = null
}
/**
* Signal that the specified [DetailListInstructions] in [artistListInstructions] were
* performed.
*/
fun finishArtistListInstructions() {
artistListInstructions = null
}
/**
* Signal that the specified [DetailListInstructions] in [genreListInstructions] were performed.
*/
fun finishGenreListInstructions() {
genreListInstructions = null
} }
private fun <T : Music> requireMusic(uid: Music.UID) = musicStore.library?.find<T>(uid) private fun <T : Music> requireMusic(uid: Music.UID) = musicStore.library?.find<T>(uid)
@ -314,14 +348,14 @@ class DetailViewModel(application: Application) :
return SongProperties(bitrate, sampleRate, resolvedMimeType) return SongProperties(bitrate, sampleRate, resolvedMimeType)
} }
private fun refreshAlbumList(album: Album) { private fun refreshAlbumList(album: Album, replace: Boolean): Int {
logD("Refreshing album data") logD("Refreshing album data")
val data = mutableListOf<Item>(album) val data = mutableListOf<Item>(album)
data.add(SortHeader(R.string.lbl_songs)) data.add(SortHeader(R.string.lbl_songs))
val songsStartIndex = data.size
// To create a good user experience regarding disc numbers, we group the album's // To create a good user experience regarding disc numbers, we group the album's
// songs up by disc and then delimit the groups by a disc header. // songs up by disc and then delimit the groups by a disc header.
val songs = albumSortSort.songs(album.songs) val songs = albumSongSort.songs(album.songs)
// Songs without disc tags become part of Disc 1. // Songs without disc tags become part of Disc 1.
val byDisc = songs.groupBy { it.disc ?: 1 } val byDisc = songs.groupBy { it.disc ?: 1 }
if (byDisc.size > 1) { if (byDisc.size > 1) {
@ -334,11 +368,17 @@ class DetailViewModel(application: Application) :
// Album only has one disc, don't add any redundant headers // Album only has one disc, don't add any redundant headers
data.addAll(songs) data.addAll(songs)
} }
albumListInstructions =
if (replace) {
DetailListInstructions.ReplaceRest(songsStartIndex)
} else {
DetailListInstructions.Diff
}
_albumList.value = data _albumList.value = data
return songsStartIndex
} }
private fun refreshArtistList(artist: Artist) { private fun refreshArtistList(artist: Artist, replace: Boolean) {
logD("Refreshing artist data") logD("Refreshing artist data")
val data = mutableListOf<Item>(artist) val data = mutableListOf<Item>(artist)
val albums = Sort(Sort.Mode.ByDate, false).albums(artist.albums) val albums = Sort(Sort.Mode.ByDate, false).albums(artist.albums)
@ -371,24 +411,40 @@ class DetailViewModel(application: Application) :
data.addAll(entry.value) data.addAll(entry.value)
} }
var songsStartIndex: Int? = null
// Artists may not be linked to any songs, only include a header entry if we have any. // Artists may not be linked to any songs, only include a header entry if we have any.
if (artist.songs.isNotEmpty()) { if (artist.songs.isNotEmpty()) {
logD("Songs present in this artist, adding header") logD("Songs present in this artist, adding header")
data.add(SortHeader(R.string.lbl_songs)) data.add(SortHeader(R.string.lbl_songs))
songsStartIndex = data.size
data.addAll(artistSongSort.songs(artist.songs)) data.addAll(artistSongSort.songs(artist.songs))
} }
artistListInstructions =
if (replace) {
DetailListInstructions.ReplaceRest(
requireNotNull(songsStartIndex) { "Cannot replace empty artist song list" })
} else {
DetailListInstructions.Diff
}
_artistList.value = data.toList() _artistList.value = data.toList()
} }
private fun refreshGenreList(genre: Genre) { private fun refreshGenreList(genre: Genre, replace: Boolean) {
logD("Refreshing genre data") logD("Refreshing genre data")
val data = mutableListOf<Item>(genre) val data = mutableListOf<Item>(genre)
// Genre is guaranteed to always have artists and songs. // Genre is guaranteed to always have artists and songs.
data.add(Header(R.string.lbl_artists)) data.add(Header(R.string.lbl_artists))
data.addAll(genre.artists) data.addAll(genre.artists)
data.add(SortHeader(R.string.lbl_songs)) data.add(SortHeader(R.string.lbl_songs))
val songsStartIndex = data.size
data.addAll(genreSongSort.songs(genre.songs)) data.addAll(genreSongSort.songs(genre.songs))
genreListInstructions =
if (replace) {
DetailListInstructions.ReplaceRest(songsStartIndex)
} else {
DetailListInstructions.Diff
}
_genreList.value = data _genreList.value = data
} }

View file

@ -31,7 +31,6 @@ import org.oxycblt.auxio.detail.recycler.DetailAdapter
import org.oxycblt.auxio.detail.recycler.GenreDetailAdapter import org.oxycblt.auxio.detail.recycler.GenreDetailAdapter
import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.list.ListFragment import org.oxycblt.auxio.list.ListFragment
import org.oxycblt.auxio.list.recycler.BasicInstructions
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre
@ -219,7 +218,9 @@ class GenreDetailFragment :
} }
private fun updateList(items: List<Item>) { private fun updateList(items: List<Item>) {
detailAdapter.submitList(items, BasicInstructions.DIFF) detailAdapter.submitList(
items, detailModel.genreListInstructions ?: DetailListInstructions.Diff)
detailModel.finishGenreListInstructions()
} }
private fun updateSelection(selected: List<Music>) { private fun updateSelection(selected: List<Music>) {

View file

@ -29,8 +29,8 @@ import org.oxycblt.auxio.databinding.ItemDiscHeaderBinding
import org.oxycblt.auxio.detail.DiscHeader import org.oxycblt.auxio.detail.DiscHeader
import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.list.SelectableListListener import org.oxycblt.auxio.list.SelectableListListener
import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter
import org.oxycblt.auxio.list.recycler.SimpleItemCallback import org.oxycblt.auxio.list.adapter.SimpleDiffCallback
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.formatDurationMs import org.oxycblt.auxio.playback.formatDurationMs
@ -94,7 +94,7 @@ class AlbumDetailAdapter(private val listener: Listener) : DetailAdapter(listene
private companion object { private companion object {
/** A comparator that can be used with DiffUtil. */ /** A comparator that can be used with DiffUtil. */
val DIFF_CALLBACK = val DIFF_CALLBACK =
object : SimpleItemCallback<Item>() { object : SimpleDiffCallback<Item>() {
override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean { override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
return when { return when {
oldItem is Album && newItem is Album -> oldItem is Album && newItem is Album ->
@ -169,7 +169,7 @@ private class AlbumDetailViewHolder private constructor(private val binding: Ite
/** A comparator that can be used with DiffUtil. */ /** A comparator that can be used with DiffUtil. */
val DIFF_CALLBACK = val DIFF_CALLBACK =
object : SimpleItemCallback<Album>() { object : SimpleDiffCallback<Album>() {
override fun areContentsTheSame(oldItem: Album, newItem: Album) = override fun areContentsTheSame(oldItem: Album, newItem: Album) =
oldItem.rawName == newItem.rawName && oldItem.rawName == newItem.rawName &&
oldItem.areArtistContentsTheSame(newItem) && oldItem.areArtistContentsTheSame(newItem) &&
@ -210,7 +210,7 @@ private class DiscHeaderViewHolder(private val binding: ItemDiscHeaderBinding) :
/** A comparator that can be used with DiffUtil. */ /** A comparator that can be used with DiffUtil. */
val DIFF_CALLBACK = val DIFF_CALLBACK =
object : SimpleItemCallback<DiscHeader>() { object : SimpleDiffCallback<DiscHeader>() {
override fun areContentsTheSame(oldItem: DiscHeader, newItem: DiscHeader) = override fun areContentsTheSame(oldItem: DiscHeader, newItem: DiscHeader) =
oldItem.disc == newItem.disc oldItem.disc == newItem.disc
} }
@ -277,7 +277,7 @@ private class AlbumSongViewHolder private constructor(private val binding: ItemA
/** A comparator that can be used with DiffUtil. */ /** A comparator that can be used with DiffUtil. */
val DIFF_CALLBACK = val DIFF_CALLBACK =
object : SimpleItemCallback<Song>() { object : SimpleDiffCallback<Song>() {
override fun areContentsTheSame(oldItem: Song, newItem: Song) = override fun areContentsTheSame(oldItem: Song, newItem: Song) =
oldItem.rawName == newItem.rawName && oldItem.durationMs == newItem.durationMs oldItem.rawName == newItem.rawName && oldItem.durationMs == newItem.durationMs
} }

View file

@ -28,8 +28,8 @@ import org.oxycblt.auxio.databinding.ItemParentBinding
import org.oxycblt.auxio.databinding.ItemSongBinding import org.oxycblt.auxio.databinding.ItemSongBinding
import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.list.SelectableListListener import org.oxycblt.auxio.list.SelectableListListener
import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter
import org.oxycblt.auxio.list.recycler.SimpleItemCallback import org.oxycblt.auxio.list.adapter.SimpleDiffCallback
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
@ -83,7 +83,7 @@ class ArtistDetailAdapter(private val listener: Listener<Music>) :
private companion object { private companion object {
/** A comparator that can be used with DiffUtil. */ /** A comparator that can be used with DiffUtil. */
val DIFF_CALLBACK = val DIFF_CALLBACK =
object : SimpleItemCallback<Item>() { object : SimpleDiffCallback<Item>() {
override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean { override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
return when { return when {
oldItem is Artist && newItem is Artist -> oldItem is Artist && newItem is Artist ->
@ -165,7 +165,7 @@ private class ArtistDetailViewHolder private constructor(private val binding: It
/** A comparator that can be used with DiffUtil. */ /** A comparator that can be used with DiffUtil. */
val DIFF_CALLBACK = val DIFF_CALLBACK =
object : SimpleItemCallback<Artist>() { object : SimpleDiffCallback<Artist>() {
override fun areContentsTheSame(oldItem: Artist, newItem: Artist) = override fun areContentsTheSame(oldItem: Artist, newItem: Artist) =
oldItem.rawName == newItem.rawName && oldItem.rawName == newItem.rawName &&
oldItem.areGenreContentsTheSame(newItem) && oldItem.areGenreContentsTheSame(newItem) &&
@ -220,7 +220,7 @@ private class ArtistAlbumViewHolder private constructor(private val binding: Ite
/** A comparator that can be used with DiffUtil. */ /** A comparator that can be used with DiffUtil. */
val DIFF_CALLBACK = val DIFF_CALLBACK =
object : SimpleItemCallback<Album>() { object : SimpleDiffCallback<Album>() {
override fun areContentsTheSame(oldItem: Album, newItem: Album) = override fun areContentsTheSame(oldItem: Album, newItem: Album) =
oldItem.rawName == newItem.rawName && oldItem.dates == newItem.dates oldItem.rawName == newItem.rawName && oldItem.dates == newItem.dates
} }
@ -269,7 +269,7 @@ private class ArtistSongViewHolder private constructor(private val binding: Item
/** A comparator that can be used with DiffUtil. */ /** A comparator that can be used with DiffUtil. */
val DIFF_CALLBACK = val DIFF_CALLBACK =
object : SimpleItemCallback<Song>() { object : SimpleDiffCallback<Song>() {
override fun areContentsTheSame(oldItem: Song, newItem: Song) = override fun areContentsTheSame(oldItem: Song, newItem: Song) =
oldItem.rawName == newItem.rawName && oldItem.rawName == newItem.rawName &&
oldItem.album.rawName == newItem.album.rawName oldItem.album.rawName == newItem.album.rawName

View file

@ -20,16 +20,24 @@ package org.oxycblt.auxio.detail.recycler
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.widget.TooltipCompat import androidx.appcompat.widget.TooltipCompat
import androidx.recyclerview.widget.AdapterListUpdateCallback
import androidx.recyclerview.widget.AsyncDifferConfig
import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListUpdateCallback
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.IntegerTable
import org.oxycblt.auxio.databinding.ItemSortHeaderBinding import org.oxycblt.auxio.databinding.ItemSortHeaderBinding
import org.oxycblt.auxio.detail.DetailListInstructions
import org.oxycblt.auxio.detail.SortHeader import org.oxycblt.auxio.detail.SortHeader
import org.oxycblt.auxio.list.Header import org.oxycblt.auxio.list.Header
import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.list.SelectableListListener import org.oxycblt.auxio.list.SelectableListListener
import org.oxycblt.auxio.list.adapter.ListDiffer
import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter
import org.oxycblt.auxio.list.adapter.SimpleDiffCallback
import org.oxycblt.auxio.list.adapter.overwriteList
import org.oxycblt.auxio.list.recycler.* import org.oxycblt.auxio.list.recycler.*
import org.oxycblt.auxio.list.recycler.BasicInstructions
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.inflater import org.oxycblt.auxio.util.inflater
@ -45,8 +53,8 @@ abstract class DetailAdapter(
private val listener: Listener<*>, private val listener: Listener<*>,
diffCallback: DiffUtil.ItemCallback<Item> diffCallback: DiffUtil.ItemCallback<Item>
) : ) :
SelectionIndicatorAdapter<Item, BasicInstructions, RecyclerView.ViewHolder>( SelectionIndicatorAdapter<Item, DetailListInstructions, RecyclerView.ViewHolder>(
ListDiffer.Async(diffCallback)), DetailListDiffer.Factory(diffCallback)),
AuxioRecyclerView.SpanSizeLookup { AuxioRecyclerView.SpanSizeLookup {
override fun getItemViewType(position: Int) = override fun getItemViewType(position: Int) =
@ -102,7 +110,7 @@ abstract class DetailAdapter(
protected companion object { protected companion object {
/** A comparator that can be used with DiffUtil. */ /** A comparator that can be used with DiffUtil. */
val DIFF_CALLBACK = val DIFF_CALLBACK =
object : SimpleItemCallback<Item>() { object : SimpleDiffCallback<Item>() {
override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean { override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
return when { return when {
oldItem is Header && newItem is Header -> oldItem is Header && newItem is Header ->
@ -116,6 +124,39 @@ abstract class DetailAdapter(
} }
} }
private class DetailListDiffer<T>(
private val updateCallback: ListUpdateCallback,
diffCallback: DiffUtil.ItemCallback<T>
) : ListDiffer<T, DetailListInstructions> {
private val inner =
AsyncListDiffer(updateCallback, AsyncDifferConfig.Builder(diffCallback).build())
override val currentList: List<T>
get() = inner.currentList
override fun submitList(
newList: List<T>,
instructions: DetailListInstructions,
onDone: () -> Unit
) {
when (instructions) {
is DetailListInstructions.Diff -> inner.submitList(newList, onDone)
is DetailListInstructions.ReplaceRest -> {
val amount = newList.size - instructions.at
updateCallback.onRemoved(instructions.at, amount)
inner.overwriteList(newList)
updateCallback.onInserted(instructions.at, amount)
}
}
}
class Factory<T>(private val diffCallback: DiffUtil.ItemCallback<T>) :
ListDiffer.Factory<T, DetailListInstructions>() {
override fun new(adapter: RecyclerView.Adapter<*>) =
DetailListDiffer(AdapterListUpdateCallback(adapter), diffCallback)
}
}
/** /**
* A [RecyclerView.ViewHolder] that displays a [SortHeader], a variation on [Header] that adds a * 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. * button opening a menu for sorting. Use [from] to create an instance.
@ -152,7 +193,7 @@ private class SortHeaderViewHolder(private val binding: ItemSortHeaderBinding) :
/** A comparator that can be used with DiffUtil. */ /** A comparator that can be used with DiffUtil. */
val DIFF_CALLBACK = val DIFF_CALLBACK =
object : SimpleItemCallback<SortHeader>() { object : SimpleDiffCallback<SortHeader>() {
override fun areContentsTheSame(oldItem: SortHeader, newItem: SortHeader) = override fun areContentsTheSame(oldItem: SortHeader, newItem: SortHeader) =
oldItem.titleRes == newItem.titleRes oldItem.titleRes == newItem.titleRes
} }

View file

@ -25,8 +25,8 @@ import org.oxycblt.auxio.IntegerTable
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.ItemDetailBinding import org.oxycblt.auxio.databinding.ItemDetailBinding
import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.list.adapter.SimpleDiffCallback
import org.oxycblt.auxio.list.recycler.ArtistViewHolder import org.oxycblt.auxio.list.recycler.ArtistViewHolder
import org.oxycblt.auxio.list.recycler.SimpleItemCallback
import org.oxycblt.auxio.list.recycler.SongViewHolder import org.oxycblt.auxio.list.recycler.SongViewHolder
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre
@ -46,8 +46,8 @@ class GenreDetailAdapter(private val listener: Listener<Music>) :
override fun getItemViewType(position: Int) = override fun getItemViewType(position: Int) =
when (getItem(position)) { when (getItem(position)) {
// Support the Genre header and generic Artist/Song items. There's nothing about // 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 // a genre that will make the artists/songs specially formatted, so it doesn't matter
// use for their ViewHolders. // what we use for their ViewHolders.
is Genre -> GenreDetailViewHolder.VIEW_TYPE is Genre -> GenreDetailViewHolder.VIEW_TYPE
is Artist -> ArtistViewHolder.VIEW_TYPE is Artist -> ArtistViewHolder.VIEW_TYPE
is Song -> SongViewHolder.VIEW_TYPE is Song -> SongViewHolder.VIEW_TYPE
@ -81,7 +81,7 @@ class GenreDetailAdapter(private val listener: Listener<Music>) :
private companion object { private companion object {
val DIFF_CALLBACK = val DIFF_CALLBACK =
object : SimpleItemCallback<Item>() { object : SimpleDiffCallback<Item>() {
override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean { override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
return when { return when {
oldItem is Genre && newItem is Genre -> oldItem is Genre && newItem is Genre ->
@ -139,7 +139,7 @@ private class GenreDetailViewHolder private constructor(private val binding: Ite
/** A comparator that can be used with DiffUtil. */ /** A comparator that can be used with DiffUtil. */
val DIFF_CALLBACK = val DIFF_CALLBACK =
object : SimpleItemCallback<Genre>() { object : SimpleDiffCallback<Genre>() {
override fun areContentsTheSame(oldItem: Genre, newItem: Genre) = override fun areContentsTheSame(oldItem: Genre, newItem: Genre) =
oldItem.rawName == newItem.rawName && oldItem.rawName == newItem.rawName &&
oldItem.songs.size == newItem.songs.size && oldItem.songs.size == newItem.songs.size &&

View file

@ -22,7 +22,7 @@ import androidx.lifecycle.AndroidViewModel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import org.oxycblt.auxio.home.tabs.Tab import org.oxycblt.auxio.home.tabs.Tab
import org.oxycblt.auxio.list.recycler.BasicInstructions import org.oxycblt.auxio.list.adapter.BasicListInstructions
import org.oxycblt.auxio.music.* import org.oxycblt.auxio.music.*
import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.library.Library import org.oxycblt.auxio.music.library.Library
@ -46,7 +46,7 @@ class HomeViewModel(application: Application) :
val songsList: StateFlow<List<Song>> val songsList: StateFlow<List<Song>>
get() = _songsList get() = _songsList
/** Specifies how to update [songsList] when it changes. */ /** Specifies how to update [songsList] when it changes. */
var songsListInstructions: BasicInstructions? = null var songsListInstructions: BasicListInstructions? = null
private set private set
private val _albumsLists = MutableStateFlow(listOf<Album>()) private val _albumsLists = MutableStateFlow(listOf<Album>())
@ -54,7 +54,7 @@ class HomeViewModel(application: Application) :
val albumsList: StateFlow<List<Album>> val albumsList: StateFlow<List<Album>>
get() = _albumsLists get() = _albumsLists
/** Specifies how to update [albumsList] when it changes. */ /** Specifies how to update [albumsList] when it changes. */
var albumsListInstructions: BasicInstructions? = null var albumsListInstructions: BasicListInstructions? = null
private set private set
private val _artistsList = MutableStateFlow(listOf<Artist>()) private val _artistsList = MutableStateFlow(listOf<Artist>())
@ -66,7 +66,7 @@ class HomeViewModel(application: Application) :
val artistsList: MutableStateFlow<List<Artist>> val artistsList: MutableStateFlow<List<Artist>>
get() = _artistsList get() = _artistsList
/** Specifies how to update [artistsList] when it changes. */ /** Specifies how to update [artistsList] when it changes. */
var artistsListInstructions: BasicInstructions? = null var artistsListInstructions: BasicListInstructions? = null
private set private set
private val _genresList = MutableStateFlow(listOf<Genre>()) private val _genresList = MutableStateFlow(listOf<Genre>())
@ -74,7 +74,7 @@ class HomeViewModel(application: Application) :
val genresList: StateFlow<List<Genre>> val genresList: StateFlow<List<Genre>>
get() = _genresList get() = _genresList
/** Specifies how to update [genresList] when it changes. */ /** Specifies how to update [genresList] when it changes. */
var genresListInstructions: BasicInstructions? = null var genresListInstructions: BasicListInstructions? = null
private set private set
/** The [MusicMode] to use when playing a [Song] from the UI. */ /** The [MusicMode] to use when playing a [Song] from the UI. */
@ -120,11 +120,11 @@ class HomeViewModel(application: Application) :
logD("Library changed, refreshing library") logD("Library changed, refreshing library")
// Get the each list of items in the library to use as our list data. // Get the each list of items in the library to use as our list data.
// Applying the preferred sorting to them. // Applying the preferred sorting to them.
songsListInstructions = BasicInstructions.DIFF songsListInstructions = BasicListInstructions.DIFF
_songsList.value = musicSettings.songSort.songs(library.songs) _songsList.value = musicSettings.songSort.songs(library.songs)
albumsListInstructions = BasicInstructions.DIFF albumsListInstructions = BasicListInstructions.DIFF
_albumsLists.value = musicSettings.albumSort.albums(library.albums) _albumsLists.value = musicSettings.albumSort.albums(library.albums)
artistsListInstructions = BasicInstructions.DIFF artistsListInstructions = BasicListInstructions.DIFF
_artistsList.value = _artistsList.value =
musicSettings.artistSort.artists( musicSettings.artistSort.artists(
if (homeSettings.shouldHideCollaborators) { if (homeSettings.shouldHideCollaborators) {
@ -133,7 +133,7 @@ class HomeViewModel(application: Application) :
} else { } else {
library.artists library.artists
}) })
genresListInstructions = BasicInstructions.DIFF genresListInstructions = BasicListInstructions.DIFF
_genresList.value = musicSettings.genreSort.genres(library.genres) _genresList.value = musicSettings.genreSort.genres(library.genres)
} }
} }
@ -173,45 +173,52 @@ class HomeViewModel(application: Application) :
when (_currentTabMode.value) { when (_currentTabMode.value) {
MusicMode.SONGS -> { MusicMode.SONGS -> {
musicSettings.songSort = sort musicSettings.songSort = sort
songsListInstructions = BasicInstructions.REPLACE songsListInstructions = BasicListInstructions.REPLACE
_songsList.value = sort.songs(_songsList.value) _songsList.value = sort.songs(_songsList.value)
} }
MusicMode.ALBUMS -> { MusicMode.ALBUMS -> {
musicSettings.albumSort = sort musicSettings.albumSort = sort
albumsListInstructions = BasicInstructions.REPLACE albumsListInstructions = BasicListInstructions.REPLACE
_albumsLists.value = sort.albums(_albumsLists.value) _albumsLists.value = sort.albums(_albumsLists.value)
} }
MusicMode.ARTISTS -> { MusicMode.ARTISTS -> {
musicSettings.artistSort = sort musicSettings.artistSort = sort
artistsListInstructions = BasicInstructions.REPLACE artistsListInstructions = BasicListInstructions.REPLACE
_artistsList.value = sort.artists(_artistsList.value) _artistsList.value = sort.artists(_artistsList.value)
} }
MusicMode.GENRES -> { MusicMode.GENRES -> {
musicSettings.genreSort = sort musicSettings.genreSort = sort
genresListInstructions = BasicInstructions.REPLACE genresListInstructions = BasicListInstructions.REPLACE
_genresList.value = sort.genres(_genresList.value) _genresList.value = sort.genres(_genresList.value)
} }
} }
} }
/** Signal that the specified [BasicInstructions] in [songsListInstructions] were performed. */ /**
* Signal that the specified [BasicListInstructions] in [songsListInstructions] were performed.
*/
fun finishSongsListInstructions() { fun finishSongsListInstructions() {
songsListInstructions = null songsListInstructions = null
} }
/** Signal that the specified [BasicInstructions] in [albumsListInstructions] were performed. */ /**
* Signal that the specified [BasicListInstructions] in [albumsListInstructions] were performed.
*/
fun finishAlbumsListInstructions() { fun finishAlbumsListInstructions() {
albumsListInstructions = null albumsListInstructions = null
} }
/** /**
* Signal that the specified [BasicInstructions] in [artistsListInstructions] were performed. * Signal that the specified [BasicListInstructions] in [artistsListInstructions] were
* performed.
*/ */
fun finishArtistsListInstructions() { fun finishArtistsListInstructions() {
artistsListInstructions = null artistsListInstructions = null
} }
/** Signal that the specified [BasicInstructions] in [genresListInstructions] were performed. */ /**
* Signal that the specified [BasicListInstructions] in [genresListInstructions] were performed.
*/
fun finishGenresListInstructions() { fun finishGenresListInstructions() {
genresListInstructions = null genresListInstructions = null
} }

View file

@ -30,10 +30,10 @@ import org.oxycblt.auxio.home.HomeViewModel
import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView
import org.oxycblt.auxio.list.* import org.oxycblt.auxio.list.*
import org.oxycblt.auxio.list.ListFragment import org.oxycblt.auxio.list.ListFragment
import org.oxycblt.auxio.list.adapter.BasicListInstructions
import org.oxycblt.auxio.list.adapter.ListDiffer
import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter
import org.oxycblt.auxio.list.recycler.AlbumViewHolder import org.oxycblt.auxio.list.recycler.AlbumViewHolder
import org.oxycblt.auxio.list.recycler.BasicInstructions
import org.oxycblt.auxio.list.recycler.ListDiffer
import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter
import org.oxycblt.auxio.music.* import org.oxycblt.auxio.music.*
import org.oxycblt.auxio.music.library.Sort import org.oxycblt.auxio.music.library.Sort
import org.oxycblt.auxio.playback.formatDurationMs import org.oxycblt.auxio.playback.formatDurationMs
@ -132,7 +132,8 @@ class AlbumListFragment :
} }
private fun updateList(albums: List<Album>) { private fun updateList(albums: List<Album>) {
albumAdapter.submitList(albums, homeModel.albumsListInstructions ?: BasicInstructions.DIFF) albumAdapter.submitList(
albums, homeModel.albumsListInstructions ?: BasicListInstructions.DIFF)
homeModel.finishAlbumsListInstructions() homeModel.finishAlbumsListInstructions()
} }
@ -150,7 +151,7 @@ class AlbumListFragment :
* @param listener An [SelectableListListener] to bind interactions to. * @param listener An [SelectableListListener] to bind interactions to.
*/ */
private class AlbumAdapter(private val listener: SelectableListListener<Album>) : private class AlbumAdapter(private val listener: SelectableListListener<Album>) :
SelectionIndicatorAdapter<Album, BasicInstructions, AlbumViewHolder>( SelectionIndicatorAdapter<Album, BasicListInstructions, AlbumViewHolder>(
ListDiffer.Async(AlbumViewHolder.DIFF_CALLBACK)) { ListDiffer.Async(AlbumViewHolder.DIFF_CALLBACK)) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =

View file

@ -28,10 +28,10 @@ import org.oxycblt.auxio.home.HomeViewModel
import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView
import org.oxycblt.auxio.list.* import org.oxycblt.auxio.list.*
import org.oxycblt.auxio.list.ListFragment import org.oxycblt.auxio.list.ListFragment
import org.oxycblt.auxio.list.adapter.BasicListInstructions
import org.oxycblt.auxio.list.adapter.ListDiffer
import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter
import org.oxycblt.auxio.list.recycler.ArtistViewHolder import org.oxycblt.auxio.list.recycler.ArtistViewHolder
import org.oxycblt.auxio.list.recycler.BasicInstructions
import org.oxycblt.auxio.list.recycler.ListDiffer
import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicMode import org.oxycblt.auxio.music.MusicMode
@ -111,7 +111,7 @@ class ArtistListFragment :
private fun updateList(artists: List<Artist>) { private fun updateList(artists: List<Artist>) {
artistAdapter.submitList( artistAdapter.submitList(
artists, homeModel.artistsListInstructions ?: BasicInstructions.DIFF) artists, homeModel.artistsListInstructions ?: BasicListInstructions.DIFF)
homeModel.finishArtistsListInstructions() homeModel.finishArtistsListInstructions()
} }
@ -129,7 +129,7 @@ class ArtistListFragment :
* @param listener An [SelectableListListener] to bind interactions to. * @param listener An [SelectableListListener] to bind interactions to.
*/ */
private class ArtistAdapter(private val listener: SelectableListListener<Artist>) : private class ArtistAdapter(private val listener: SelectableListListener<Artist>) :
SelectionIndicatorAdapter<Artist, BasicInstructions, ArtistViewHolder>( SelectionIndicatorAdapter<Artist, BasicListInstructions, ArtistViewHolder>(
ListDiffer.Async(ArtistViewHolder.DIFF_CALLBACK)) { ListDiffer.Async(ArtistViewHolder.DIFF_CALLBACK)) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =

View file

@ -28,10 +28,10 @@ import org.oxycblt.auxio.home.HomeViewModel
import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView
import org.oxycblt.auxio.list.* import org.oxycblt.auxio.list.*
import org.oxycblt.auxio.list.ListFragment import org.oxycblt.auxio.list.ListFragment
import org.oxycblt.auxio.list.recycler.BasicInstructions import org.oxycblt.auxio.list.adapter.BasicListInstructions
import org.oxycblt.auxio.list.adapter.ListDiffer
import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter
import org.oxycblt.auxio.list.recycler.GenreViewHolder 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.music.Genre import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicMode import org.oxycblt.auxio.music.MusicMode
@ -109,7 +109,8 @@ class GenreListFragment :
} }
private fun updateList(artists: List<Genre>) { private fun updateList(artists: List<Genre>) {
genreAdapter.submitList(artists, homeModel.genresListInstructions ?: BasicInstructions.DIFF) genreAdapter.submitList(
artists, homeModel.genresListInstructions ?: BasicListInstructions.DIFF)
homeModel.finishGenresListInstructions() homeModel.finishGenresListInstructions()
} }
@ -127,7 +128,7 @@ class GenreListFragment :
* @param listener An [SelectableListListener] to bind interactions to. * @param listener An [SelectableListListener] to bind interactions to.
*/ */
private class GenreAdapter(private val listener: SelectableListListener<Genre>) : private class GenreAdapter(private val listener: SelectableListListener<Genre>) :
SelectionIndicatorAdapter<Genre, BasicInstructions, GenreViewHolder>( SelectionIndicatorAdapter<Genre, BasicListInstructions, GenreViewHolder>(
ListDiffer.Async(GenreViewHolder.DIFF_CALLBACK)) { ListDiffer.Async(GenreViewHolder.DIFF_CALLBACK)) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
GenreViewHolder.from(parent) GenreViewHolder.from(parent)

View file

@ -30,9 +30,9 @@ import org.oxycblt.auxio.home.HomeViewModel
import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView
import org.oxycblt.auxio.list.* import org.oxycblt.auxio.list.*
import org.oxycblt.auxio.list.ListFragment import org.oxycblt.auxio.list.ListFragment
import org.oxycblt.auxio.list.recycler.BasicInstructions import org.oxycblt.auxio.list.adapter.BasicListInstructions
import org.oxycblt.auxio.list.recycler.ListDiffer import org.oxycblt.auxio.list.adapter.ListDiffer
import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter
import org.oxycblt.auxio.list.recycler.SongViewHolder import org.oxycblt.auxio.list.recycler.SongViewHolder
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicMode import org.oxycblt.auxio.music.MusicMode
@ -139,7 +139,7 @@ class SongListFragment :
} }
private fun updateList(songs: List<Song>) { private fun updateList(songs: List<Song>) {
songAdapter.submitList(songs, homeModel.songsListInstructions ?: BasicInstructions.DIFF) songAdapter.submitList(songs, homeModel.songsListInstructions ?: BasicListInstructions.DIFF)
homeModel.finishSongsListInstructions() homeModel.finishSongsListInstructions()
} }
@ -161,7 +161,7 @@ class SongListFragment :
* @param listener An [SelectableListListener] to bind interactions to. * @param listener An [SelectableListListener] to bind interactions to.
*/ */
private class SongAdapter(private val listener: SelectableListListener<Song>) : private class SongAdapter(private val listener: SelectableListListener<Song>) :
SelectionIndicatorAdapter<Song, BasicInstructions, SongViewHolder>( SelectionIndicatorAdapter<Song, BasicListInstructions, SongViewHolder>(
ListDiffer.Async(SongViewHolder.DIFF_CALLBACK)) { ListDiffer.Async(SongViewHolder.DIFF_CALLBACK)) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =

View file

@ -25,6 +25,5 @@ interface Item
/** /**
* A "header" used for delimiting groups of data. * A "header" used for delimiting groups of data.
* @param titleRes The string resource used for the header's title. * @param titleRes The string resource used for the header's title.
* @param withDivider Whether to show a divider on the item.
*/ */
data class Header(@StringRes val titleRes: Int) : Item data class Header(@StringRes val titleRes: Int) : Item

View file

@ -0,0 +1,42 @@
/*
* 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.adapter
import androidx.recyclerview.widget.AsyncListDiffer
import java.lang.reflect.Field
import org.oxycblt.auxio.util.lazyReflectedField
import org.oxycblt.auxio.util.requireIs
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")
/**
* Force-update an [AsyncListDiffer] with new data. It's hard to state how incredibly dangerous this
* is, so only use it when absolutely necessary.
* @param newList The new list to write to the [AsyncListDiffer].
*/
fun <T> AsyncListDiffer<T>.overwriteList(newList: List<T>) {
// Should update the generation field to prevent any previous jobs from conflicting, then
// updates the mutable list to it's nullable value, and then updates the read-only list to
// it's non-nullable value.
ASD_MAX_GENERATION_FIELD.set(this, requireIs<Int>(ASD_MAX_GENERATION_FIELD.get(this)) + 1)
ASD_MUTABLE_LIST_FIELD.set(this, newList.ifEmpty { null })
ASD_READ_ONLY_LIST_FIELD.set(this, newList)
}

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.list.recycler package org.oxycblt.auxio.list.adapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.list.recycler package org.oxycblt.auxio.list.adapter
import androidx.recyclerview.widget.AdapterListUpdateCallback import androidx.recyclerview.widget.AdapterListUpdateCallback
import androidx.recyclerview.widget.AsyncDifferConfig import androidx.recyclerview.widget.AsyncDifferConfig
@ -23,9 +23,6 @@ import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListUpdateCallback import androidx.recyclerview.widget.ListUpdateCallback
import androidx.recyclerview.widget.RecyclerView 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. * List differ wrapper that provides more flexibility regarding the way lists are updated.
@ -38,7 +35,7 @@ interface ListDiffer<T, I> {
/** /**
* Dynamically determine how to update the list based on the given instructions. * Dynamically determine how to update the list based on the given instructions.
* @param newList The new list of [T] items to show. * @param newList The new list of [T] items to show.
* @param instructions The [BasicInstructions] specifying how to update the list. * @param instructions The [BasicListInstructions] specifying how to update the list.
* @param onDone Called when the update process is completed. * @param onDone Called when the update process is completed.
*/ */
fun submitList(newList: List<T>, instructions: I, onDone: () -> Unit) fun submitList(newList: List<T>, instructions: I, onDone: () -> Unit)
@ -62,20 +59,20 @@ interface ListDiffer<T, I> {
* internal list. * internal list.
*/ */
class Async<T>(private val diffCallback: DiffUtil.ItemCallback<T>) : class Async<T>(private val diffCallback: DiffUtil.ItemCallback<T>) :
Factory<T, BasicInstructions>() { Factory<T, BasicListInstructions>() {
override fun new(adapter: RecyclerView.Adapter<*>): ListDiffer<T, BasicInstructions> = override fun new(adapter: RecyclerView.Adapter<*>): ListDiffer<T, BasicListInstructions> =
RealAsyncListDiffer(AdapterListUpdateCallback(adapter), diffCallback) RealAsyncListDiffer(AdapterListUpdateCallback(adapter), diffCallback)
} }
/** /**
* Update lists on the main thread. This is useful when many small, discrete list diffs are * 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. * likely to occur that would cause [Async] to suffer from race conditions.
* @param diffCallback A [DiffUtil.ItemCallback] to use for item comparison when diffing the * @param diffCallback A [DiffUtil.ItemCallback] to use for item comparison when diffing the
* internal list. * internal list.
*/ */
class Blocking<T>(private val diffCallback: DiffUtil.ItemCallback<T>) : class Blocking<T>(private val diffCallback: DiffUtil.ItemCallback<T>) :
Factory<T, BasicInstructions>() { Factory<T, BasicListInstructions>() {
override fun new(adapter: RecyclerView.Adapter<*>): ListDiffer<T, BasicInstructions> = override fun new(adapter: RecyclerView.Adapter<*>): ListDiffer<T, BasicListInstructions> =
RealBlockingListDiffer(AdapterListUpdateCallback(adapter), diffCallback) RealBlockingListDiffer(AdapterListUpdateCallback(adapter), diffCallback)
} }
} }
@ -84,7 +81,7 @@ interface ListDiffer<T, I> {
* Represents the specific way to update a list of items. * Represents the specific way to update a list of items.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
enum class BasicInstructions { enum class BasicListInstructions {
/** /**
* (A)synchronously diff the list. This should be used for small diffs with little item * (A)synchronously diff the list. This should be used for small diffs with little item
* movement. * movement.
@ -98,11 +95,15 @@ enum class BasicInstructions {
REPLACE REPLACE
} }
private abstract class RealListDiffer<T>() : ListDiffer<T, BasicInstructions> { private abstract class BasicListDiffer<T>() : ListDiffer<T, BasicListInstructions> {
override fun submitList(newList: List<T>, instructions: BasicInstructions, onDone: () -> Unit) { override fun submitList(
newList: List<T>,
instructions: BasicListInstructions,
onDone: () -> Unit
) {
when (instructions) { when (instructions) {
BasicInstructions.DIFF -> diffList(newList, onDone) BasicListInstructions.DIFF -> diffList(newList, onDone)
BasicInstructions.REPLACE -> replaceList(newList, onDone) BasicListInstructions.REPLACE -> replaceList(newList, onDone)
} }
} }
@ -113,7 +114,7 @@ private abstract class RealListDiffer<T>() : ListDiffer<T, BasicInstructions> {
private class RealAsyncListDiffer<T>( private class RealAsyncListDiffer<T>(
private val updateCallback: ListUpdateCallback, private val updateCallback: ListUpdateCallback,
diffCallback: DiffUtil.ItemCallback<T> diffCallback: DiffUtil.ItemCallback<T>
) : RealListDiffer<T>() { ) : BasicListDiffer<T>() {
private val inner = private val inner =
AsyncListDiffer(updateCallback, AsyncDifferConfig.Builder(diffCallback).build()) AsyncListDiffer(updateCallback, AsyncDifferConfig.Builder(diffCallback).build())
@ -126,34 +127,19 @@ private class RealAsyncListDiffer<T>(
override fun replaceList(newList: List<T>, onDone: () -> Unit) { override fun replaceList(newList: List<T>, onDone: () -> Unit) {
if (inner.currentList != newList) { if (inner.currentList != newList) {
// 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 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.onRemoved(0, oldListSize)
inner.overwriteList(newList)
updateCallback.onInserted(0, newList.size) updateCallback.onInserted(0, newList.size)
} }
onDone() onDone()
} }
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 class RealBlockingListDiffer<T>(
private val updateCallback: ListUpdateCallback, private val updateCallback: ListUpdateCallback,
private val diffCallback: DiffUtil.ItemCallback<T> private val diffCallback: DiffUtil.ItemCallback<T>
) : RealListDiffer<T>() { ) : BasicListDiffer<T>() {
override var currentList = listOf<T>() override var currentList = listOf<T>()
override fun diffList(newList: List<T>, onDone: () -> Unit) { override fun diffList(newList: List<T>, onDone: () -> Unit) {

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.list.recycler package org.oxycblt.auxio.list.adapter
import android.view.View import android.view.View
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.list.recycler package org.oxycblt.auxio.list.adapter
import android.view.View import android.view.View
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.list.recycler package org.oxycblt.auxio.list.adapter
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.Item
@ -25,6 +25,6 @@ import org.oxycblt.auxio.list.Item
* whenever creating [DiffUtil.ItemCallback] implementations with an [Item] subclass. * whenever creating [DiffUtil.ItemCallback] implementations with an [Item] subclass.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
abstract class SimpleItemCallback<T : Item> : DiffUtil.ItemCallback<T>() { abstract class SimpleDiffCallback<T : Item> : DiffUtil.ItemCallback<T>() {
final override fun areItemsTheSame(oldItem: T, newItem: T) = oldItem == newItem final override fun areItemsTheSame(oldItem: T, newItem: T) = oldItem == newItem
} }

View file

@ -24,6 +24,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.divider.BackportMaterialDividerItemDecoration import com.google.android.material.divider.BackportMaterialDividerItemDecoration
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.list.Header import org.oxycblt.auxio.list.Header
import org.oxycblt.auxio.list.adapter.DiffAdapter
/** /**
* A [BackportMaterialDividerItemDecoration] that sets up the divider configuration to correctly * A [BackportMaterialDividerItemDecoration] that sets up the divider configuration to correctly

View file

@ -26,6 +26,8 @@ import org.oxycblt.auxio.databinding.ItemParentBinding
import org.oxycblt.auxio.databinding.ItemSongBinding import org.oxycblt.auxio.databinding.ItemSongBinding
import org.oxycblt.auxio.list.Header import org.oxycblt.auxio.list.Header
import org.oxycblt.auxio.list.SelectableListListener import org.oxycblt.auxio.list.SelectableListListener
import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter
import org.oxycblt.auxio.list.adapter.SimpleDiffCallback
import org.oxycblt.auxio.music.* import org.oxycblt.auxio.music.*
import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.getPlural import org.oxycblt.auxio.util.getPlural
@ -72,7 +74,7 @@ class SongViewHolder private constructor(private val binding: ItemSongBinding) :
/** A comparator that can be used with DiffUtil. */ /** A comparator that can be used with DiffUtil. */
val DIFF_CALLBACK = val DIFF_CALLBACK =
object : SimpleItemCallback<Song>() { object : SimpleDiffCallback<Song>() {
override fun areContentsTheSame(oldItem: Song, newItem: Song) = override fun areContentsTheSame(oldItem: Song, newItem: Song) =
oldItem.rawName == newItem.rawName && oldItem.areArtistContentsTheSame(newItem) oldItem.rawName == newItem.rawName && oldItem.areArtistContentsTheSame(newItem)
} }
@ -119,7 +121,7 @@ class AlbumViewHolder private constructor(private val binding: ItemParentBinding
/** A comparator that can be used with DiffUtil. */ /** A comparator that can be used with DiffUtil. */
val DIFF_CALLBACK = val DIFF_CALLBACK =
object : SimpleItemCallback<Album>() { object : SimpleDiffCallback<Album>() {
override fun areContentsTheSame(oldItem: Album, newItem: Album) = override fun areContentsTheSame(oldItem: Album, newItem: Album) =
oldItem.rawName == newItem.rawName && oldItem.rawName == newItem.rawName &&
oldItem.areArtistContentsTheSame(newItem) && oldItem.areArtistContentsTheSame(newItem) &&
@ -178,7 +180,7 @@ class ArtistViewHolder private constructor(private val binding: ItemParentBindin
/** A comparator that can be used with DiffUtil. */ /** A comparator that can be used with DiffUtil. */
val DIFF_CALLBACK = val DIFF_CALLBACK =
object : SimpleItemCallback<Artist>() { object : SimpleDiffCallback<Artist>() {
override fun areContentsTheSame(oldItem: Artist, newItem: Artist) = override fun areContentsTheSame(oldItem: Artist, newItem: Artist) =
oldItem.rawName == newItem.rawName && oldItem.rawName == newItem.rawName &&
oldItem.albums.size == newItem.albums.size && oldItem.albums.size == newItem.albums.size &&
@ -231,7 +233,7 @@ class GenreViewHolder private constructor(private val binding: ItemParentBinding
/** A comparator that can be used with DiffUtil. */ /** A comparator that can be used with DiffUtil. */
val DIFF_CALLBACK = val DIFF_CALLBACK =
object : SimpleItemCallback<Genre>() { object : SimpleDiffCallback<Genre>() {
override fun areContentsTheSame(oldItem: Genre, newItem: Genre): Boolean = override fun areContentsTheSame(oldItem: Genre, newItem: Genre): Boolean =
oldItem.rawName == newItem.rawName && oldItem.songs.size == newItem.songs.size oldItem.rawName == newItem.rawName && oldItem.songs.size == newItem.songs.size
} }
@ -267,7 +269,7 @@ class HeaderViewHolder private constructor(private val binding: ItemHeaderBindin
/** A comparator that can be used with DiffUtil. */ /** A comparator that can be used with DiffUtil. */
val DIFF_CALLBACK = val DIFF_CALLBACK =
object : SimpleItemCallback<Header>() { object : SimpleDiffCallback<Header>() {
override fun areContentsTheSame(oldItem: Header, newItem: Header): Boolean = override fun areContentsTheSame(oldItem: Header, newItem: Header): Boolean =
oldItem.titleRes == newItem.titleRes oldItem.titleRes == newItem.titleRes
} }

View file

@ -27,10 +27,10 @@ import com.google.android.material.shape.MaterialShapeDrawable
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.ItemQueueSongBinding import org.oxycblt.auxio.databinding.ItemQueueSongBinding
import org.oxycblt.auxio.list.EditableListListener import org.oxycblt.auxio.list.EditableListListener
import org.oxycblt.auxio.list.recycler.BasicInstructions import org.oxycblt.auxio.list.adapter.BasicListInstructions
import org.oxycblt.auxio.list.recycler.DiffAdapter import org.oxycblt.auxio.list.adapter.DiffAdapter
import org.oxycblt.auxio.list.recycler.ListDiffer import org.oxycblt.auxio.list.adapter.ListDiffer
import org.oxycblt.auxio.list.recycler.PlayingIndicatorAdapter import org.oxycblt.auxio.list.adapter.PlayingIndicatorAdapter
import org.oxycblt.auxio.list.recycler.SongViewHolder import org.oxycblt.auxio.list.recycler.SongViewHolder
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.util.* import org.oxycblt.auxio.util.*
@ -41,7 +41,7 @@ import org.oxycblt.auxio.util.*
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class QueueAdapter(private val listener: EditableListListener<Song>) : class QueueAdapter(private val listener: EditableListListener<Song>) :
DiffAdapter<Song, BasicInstructions, QueueSongViewHolder>( DiffAdapter<Song, BasicListInstructions, QueueSongViewHolder>(
ListDiffer.Blocking(QueueSongViewHolder.DIFF_CALLBACK)) { ListDiffer.Blocking(QueueSongViewHolder.DIFF_CALLBACK)) {
// Since PlayingIndicator adapter relies on an item value, we cannot use it for this // 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 // adapter, as one item can appear at several points in the UI. Use a similar implementation

View file

@ -27,7 +27,7 @@ import androidx.recyclerview.widget.RecyclerView
import kotlin.math.min import kotlin.math.min
import org.oxycblt.auxio.databinding.FragmentQueueBinding import org.oxycblt.auxio.databinding.FragmentQueueBinding
import org.oxycblt.auxio.list.EditableListListener import org.oxycblt.auxio.list.EditableListListener
import org.oxycblt.auxio.list.recycler.BasicInstructions import org.oxycblt.auxio.list.adapter.BasicListInstructions
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.ViewBindingFragment import org.oxycblt.auxio.ui.ViewBindingFragment
@ -101,7 +101,7 @@ class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), EditableListL
// Replace or diff the queue depending on the type of change it is. // Replace or diff the queue depending on the type of change it is.
val instructions = queueModel.instructions val instructions = queueModel.instructions
queueAdapter.submitList(queue, instructions?.update ?: BasicInstructions.DIFF) queueAdapter.submitList(queue, instructions?.update ?: BasicListInstructions.DIFF)
// Update position in list (and thus past/future items) // Update position in list (and thus past/future items)
queueAdapter.setPosition(index, isPlaying) queueAdapter.setPosition(index, isPlaying)

View file

@ -20,7 +20,7 @@ package org.oxycblt.auxio.playback.queue
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import org.oxycblt.auxio.list.recycler.BasicInstructions import org.oxycblt.auxio.list.adapter.BasicListInstructions
import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.PlaybackStateManager
@ -57,7 +57,7 @@ class QueueViewModel : ViewModel(), PlaybackStateManager.Listener {
override fun onQueueChanged(queue: Queue, change: Queue.ChangeResult) { override fun onQueueChanged(queue: Queue, change: Queue.ChangeResult) {
// Queue changed trivially due to item mo -> Diff queue, stay at current index. // Queue changed trivially due to item mo -> Diff queue, stay at current index.
instructions = Instructions(BasicInstructions.DIFF, null) instructions = Instructions(BasicListInstructions.DIFF, null)
_queue.value = queue.resolve() _queue.value = queue.resolve()
if (change != Queue.ChangeResult.MAPPING) { if (change != Queue.ChangeResult.MAPPING) {
// Index changed, make sure it remains updated without actually scrolling to it. // Index changed, make sure it remains updated without actually scrolling to it.
@ -67,14 +67,14 @@ class QueueViewModel : ViewModel(), PlaybackStateManager.Listener {
override fun onQueueReordered(queue: Queue) { override fun onQueueReordered(queue: Queue) {
// Queue changed completely -> Replace queue, update index // Queue changed completely -> Replace queue, update index
instructions = Instructions(BasicInstructions.REPLACE, queue.index) instructions = Instructions(BasicListInstructions.REPLACE, queue.index)
_queue.value = queue.resolve() _queue.value = queue.resolve()
_index.value = queue.index _index.value = queue.index
} }
override fun onNewPlayback(queue: Queue, parent: MusicParent?) { override fun onNewPlayback(queue: Queue, parent: MusicParent?) {
// Entirely new queue -> Replace queue, update index // Entirely new queue -> Replace queue, update index
instructions = Instructions(BasicInstructions.REPLACE, queue.index) instructions = Instructions(BasicListInstructions.REPLACE, queue.index)
_queue.value = queue.resolve() _queue.value = queue.resolve()
_index.value = queue.index _index.value = queue.index
} }
@ -124,5 +124,5 @@ class QueueViewModel : ViewModel(), PlaybackStateManager.Listener {
instructions = null instructions = null
} }
class Instructions(val update: BasicInstructions?, val scrollTo: Int?) class Instructions(val update: BasicListInstructions?, val scrollTo: Int?)
} }

View file

@ -20,6 +20,10 @@ package org.oxycblt.auxio.search
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.list.* import org.oxycblt.auxio.list.*
import org.oxycblt.auxio.list.adapter.BasicListInstructions
import org.oxycblt.auxio.list.adapter.ListDiffer
import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter
import org.oxycblt.auxio.list.adapter.SimpleDiffCallback
import org.oxycblt.auxio.list.recycler.* import org.oxycblt.auxio.list.recycler.*
import org.oxycblt.auxio.music.* import org.oxycblt.auxio.music.*
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
@ -30,7 +34,7 @@ import org.oxycblt.auxio.util.logD
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class SearchAdapter(private val listener: SelectableListListener<Music>) : class SearchAdapter(private val listener: SelectableListListener<Music>) :
SelectionIndicatorAdapter<Item, BasicInstructions, RecyclerView.ViewHolder>( SelectionIndicatorAdapter<Item, BasicListInstructions, RecyclerView.ViewHolder>(
ListDiffer.Async(DIFF_CALLBACK)), ListDiffer.Async(DIFF_CALLBACK)),
AuxioRecyclerView.SpanSizeLookup { AuxioRecyclerView.SpanSizeLookup {
@ -79,7 +83,7 @@ class SearchAdapter(private val listener: SelectableListListener<Music>) :
val PAYLOAD_UPDATE_DIVIDER = 102249124 val PAYLOAD_UPDATE_DIVIDER = 102249124
/** A comparator that can be used with DiffUtil. */ /** A comparator that can be used with DiffUtil. */
val DIFF_CALLBACK = val DIFF_CALLBACK =
object : SimpleItemCallback<Item>() { object : SimpleDiffCallback<Item>() {
override fun areContentsTheSame(oldItem: Item, newItem: Item) = override fun areContentsTheSame(oldItem: Item, newItem: Item) =
when { when {
oldItem is Song && newItem is Song -> oldItem is Song && newItem is Song ->

View file

@ -31,7 +31,7 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentSearchBinding import org.oxycblt.auxio.databinding.FragmentSearchBinding
import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.list.ListFragment import org.oxycblt.auxio.list.ListFragment
import org.oxycblt.auxio.list.recycler.BasicInstructions import org.oxycblt.auxio.list.adapter.BasicListInstructions
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre
@ -154,7 +154,7 @@ class SearchFragment : ListFragment<Music, FragmentSearchBinding>() {
// Don't show the RecyclerView (and it's stray overscroll effects) when there // Don't show the RecyclerView (and it's stray overscroll effects) when there
// are no results. // are no results.
binding.searchRecycler.isInvisible = results.isEmpty() binding.searchRecycler.isInvisible = results.isEmpty()
searchAdapter.submitList(results.toMutableList(), BasicInstructions.DIFF) { searchAdapter.submitList(results.toMutableList(), BasicListInstructions.DIFF) {
// I would make it so that the position is only scrolled back to the top when // 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 // the query actually changes instead of once every re-creation event, but sadly
// that doesn't seem possible. // that doesn't seem possible.