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:
parent
f7bf12c4a5
commit
0c69a35e80
31 changed files with 315 additions and 148 deletions
|
@ -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
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>) {
|
||||||
|
|
|
@ -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>) {
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>) {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 &&
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) =
|
||||||
|
|
|
@ -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) =
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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) =
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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) {
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||||
}
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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?)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 ->
|
||||||
|
|
|
@ -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.
|
||||||
|
|
Loading…
Reference in a new issue