list: rework diffing abstraction
Make all adapters relying on diffing unified into a DiffAdapter superclass that can then accurately respond to the new UpdateInstructions data. UpdateInstructions is still not fully used everywhere, but will be soon.
This commit is contained in:
parent
b524beb0ac
commit
df98bb535f
32 changed files with 468 additions and 230 deletions
|
@ -9,6 +9,7 @@
|
||||||
- Added ability to edit previously played or currently playing items in the queue
|
- Added ability to edit previously played or currently playing items in the queue
|
||||||
- Added support for date values formatted as "YYYYMMDD"
|
- Added support for date values formatted as "YYYYMMDD"
|
||||||
- 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
|
||||||
|
|
||||||
#### What's Fixed
|
#### What's Fixed
|
||||||
- Fixed unreliable ReplayGain adjustment application in certain situations
|
- Fixed unreliable ReplayGain adjustment application in certain situations
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
<queries />
|
<queries />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".AuxioApp"
|
android:name=".Auxio"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:fullBackupContent="@xml/backup_descriptor"
|
android:fullBackupContent="@xml/backup_descriptor"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
|
|
@ -38,7 +38,7 @@ import org.oxycblt.auxio.ui.UISettings
|
||||||
* Auxio: A simple, rational music player for android.
|
* Auxio: A simple, rational music player for android.
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
class AuxioApp : Application(), ImageLoaderFactory {
|
class Auxio : Application(), ImageLoaderFactory {
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
// Migrate any settings that may have changed in an app update.
|
// Migrate any settings that may have changed in an app update.
|
|
@ -131,7 +131,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
val action =
|
val action =
|
||||||
when (intent.action) {
|
when (intent.action) {
|
||||||
Intent.ACTION_VIEW -> InternalPlayer.Action.Open(intent.data ?: return false)
|
Intent.ACTION_VIEW -> InternalPlayer.Action.Open(intent.data ?: return false)
|
||||||
AuxioApp.INTENT_KEY_SHORTCUT_SHUFFLE -> InternalPlayer.Action.ShuffleAll
|
Auxio.INTENT_KEY_SHORTCUT_SHUFFLE -> InternalPlayer.Action.ShuffleAll
|
||||||
else -> return false
|
else -> return false
|
||||||
}
|
}
|
||||||
playbackModel.startAction(action)
|
playbackModel.startAction(action)
|
||||||
|
|
|
@ -235,8 +235,8 @@ class MainFragment :
|
||||||
tryHideAllSheets()
|
tryHideAllSheets()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Since the listener is also reliant on the bottom sheets, we must also update it
|
// Since the navigation listener is also reliant on the bottom sheets, we must also update
|
||||||
// every frame.
|
// it every frame.
|
||||||
callback.invalidateEnabled()
|
callback.invalidateEnabled()
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
|
|
@ -82,7 +82,7 @@ class AlbumDetailFragment :
|
||||||
// DetailViewModel handles most initialization from the navigation argument.
|
// DetailViewModel handles most initialization from the navigation argument.
|
||||||
detailModel.setAlbumUid(args.albumUid)
|
detailModel.setAlbumUid(args.albumUid)
|
||||||
collectImmediately(detailModel.currentAlbum, ::updateAlbum)
|
collectImmediately(detailModel.currentAlbum, ::updateAlbum)
|
||||||
collectImmediately(detailModel.albumList, detailAdapter::submitList)
|
collectImmediately(detailModel.albumList, detailAdapter::diffList)
|
||||||
collectImmediately(
|
collectImmediately(
|
||||||
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
||||||
collect(navModel.exploreNavigationItem, ::handleNavigation)
|
collect(navModel.exploreNavigationItem, ::handleNavigation)
|
||||||
|
@ -170,10 +170,10 @@ class AlbumDetailFragment :
|
||||||
|
|
||||||
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
|
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
|
||||||
if (parent is Album && parent == unlikelyToBeNull(detailModel.currentAlbum.value)) {
|
if (parent is Album && parent == unlikelyToBeNull(detailModel.currentAlbum.value)) {
|
||||||
detailAdapter.setPlayingItem(song, isPlaying)
|
detailAdapter.setPlaying(song, isPlaying)
|
||||||
} else {
|
} else {
|
||||||
// Clear the ViewHolders if the mode isn't ALL_SONGS
|
// Clear the ViewHolders if the mode isn't ALL_SONGS
|
||||||
detailAdapter.setPlayingItem(null, isPlaying)
|
detailAdapter.setPlaying(null, isPlaying)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -258,7 +258,7 @@ class AlbumDetailFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateSelection(selected: List<Music>) {
|
private fun updateSelection(selected: List<Music>) {
|
||||||
detailAdapter.setSelectedItems(selected)
|
detailAdapter.setSelected(selected.toSet())
|
||||||
requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size)
|
requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,7 +85,7 @@ class ArtistDetailFragment :
|
||||||
// DetailViewModel handles most initialization from the navigation argument.
|
// DetailViewModel handles most initialization from the navigation argument.
|
||||||
detailModel.setArtistUid(args.artistUid)
|
detailModel.setArtistUid(args.artistUid)
|
||||||
collectImmediately(detailModel.currentArtist, ::updateItem)
|
collectImmediately(detailModel.currentArtist, ::updateItem)
|
||||||
collectImmediately(detailModel.artistList, detailAdapter::submitList)
|
collectImmediately(detailModel.artistList, detailAdapter::diffList)
|
||||||
collectImmediately(
|
collectImmediately(
|
||||||
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
||||||
collect(navModel.exploreNavigationItem, ::handleNavigation)
|
collect(navModel.exploreNavigationItem, ::handleNavigation)
|
||||||
|
@ -195,7 +195,7 @@ class ArtistDetailFragment :
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
detailAdapter.setPlayingItem(playingItem, isPlaying)
|
detailAdapter.setPlaying(playingItem, isPlaying)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleNavigation(item: Music?) {
|
private fun handleNavigation(item: Music?) {
|
||||||
|
@ -234,7 +234,7 @@ class ArtistDetailFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateSelection(selected: List<Music>) {
|
private fun updateSelection(selected: List<Music>) {
|
||||||
detailAdapter.setSelectedItems(selected)
|
detailAdapter.setSelected(selected.toSet())
|
||||||
requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size)
|
requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -84,7 +84,7 @@ class GenreDetailFragment :
|
||||||
// DetailViewModel handles most initialization from the navigation argument.
|
// DetailViewModel handles most initialization from the navigation argument.
|
||||||
detailModel.setGenreUid(args.genreUid)
|
detailModel.setGenreUid(args.genreUid)
|
||||||
collectImmediately(detailModel.currentGenre, ::updateItem)
|
collectImmediately(detailModel.currentGenre, ::updateItem)
|
||||||
collectImmediately(detailModel.genreList, detailAdapter::submitList)
|
collectImmediately(detailModel.genreList, detailAdapter::diffList)
|
||||||
collectImmediately(
|
collectImmediately(
|
||||||
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
||||||
collect(navModel.exploreNavigationItem, ::handleNavigation)
|
collect(navModel.exploreNavigationItem, ::handleNavigation)
|
||||||
|
@ -189,7 +189,7 @@ class GenreDetailFragment :
|
||||||
if (parent is Genre && parent.uid == unlikelyToBeNull(detailModel.currentGenre.value).uid) {
|
if (parent is Genre && parent.uid == unlikelyToBeNull(detailModel.currentGenre.value).uid) {
|
||||||
playingMusic = song
|
playingMusic = song
|
||||||
}
|
}
|
||||||
detailAdapter.setPlayingItem(playingMusic, isPlaying)
|
detailAdapter.setPlaying(playingMusic, isPlaying)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleNavigation(item: Music?) {
|
private fun handleNavigation(item: Music?) {
|
||||||
|
@ -217,7 +217,7 @@ class GenreDetailFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateSelection(selected: List<Music>) {
|
private fun updateSelection(selected: List<Music>) {
|
||||||
detailAdapter.setSelectedItems(selected)
|
detailAdapter.setSelected(selected.toSet())
|
||||||
requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size)
|
requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,7 +57,7 @@ class AlbumDetailAdapter(private val listener: Listener) : DetailAdapter(listene
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemViewType(position: Int) =
|
override fun getItemViewType(position: Int) =
|
||||||
when (differ.currentList[position]) {
|
when (getItem(position)) {
|
||||||
// Support the Album header, sub-headers for each disc, and special album songs.
|
// Support the Album header, sub-headers for each disc, and special album songs.
|
||||||
is Album -> AlbumDetailViewHolder.VIEW_TYPE
|
is Album -> AlbumDetailViewHolder.VIEW_TYPE
|
||||||
is DiscHeader -> DiscHeaderViewHolder.VIEW_TYPE
|
is DiscHeader -> DiscHeaderViewHolder.VIEW_TYPE
|
||||||
|
@ -75,7 +75,7 @@ class AlbumDetailAdapter(private val listener: Listener) : DetailAdapter(listene
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||||
super.onBindViewHolder(holder, position)
|
super.onBindViewHolder(holder, position)
|
||||||
when (val item = differ.currentList[position]) {
|
when (val item = getItem(position)) {
|
||||||
is Album -> (holder as AlbumDetailViewHolder).bind(item, listener)
|
is Album -> (holder as AlbumDetailViewHolder).bind(item, listener)
|
||||||
is DiscHeader -> (holder as DiscHeaderViewHolder).bind(item)
|
is DiscHeader -> (holder as DiscHeaderViewHolder).bind(item)
|
||||||
is Song -> (holder as AlbumSongViewHolder).bind(item, listener)
|
is Song -> (holder as AlbumSongViewHolder).bind(item, listener)
|
||||||
|
@ -83,9 +83,12 @@ class AlbumDetailAdapter(private val listener: Listener) : DetailAdapter(listene
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isItemFullWidth(position: Int): Boolean {
|
override fun isItemFullWidth(position: Int): Boolean {
|
||||||
|
if (super.isItemFullWidth(position)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
// The album and disc headers should be full-width in all configurations.
|
// The album and disc headers should be full-width in all configurations.
|
||||||
val item = differ.currentList[position]
|
val item = getItem(position)
|
||||||
return super.isItemFullWidth(position) || item is Album || item is DiscHeader
|
return item is Album || item is DiscHeader
|
||||||
}
|
}
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
|
|
|
@ -46,7 +46,7 @@ import org.oxycblt.auxio.util.inflater
|
||||||
class ArtistDetailAdapter(private val listener: Listener<Music>) :
|
class ArtistDetailAdapter(private val listener: Listener<Music>) :
|
||||||
DetailAdapter(listener, DIFF_CALLBACK) {
|
DetailAdapter(listener, DIFF_CALLBACK) {
|
||||||
override fun getItemViewType(position: Int) =
|
override fun getItemViewType(position: Int) =
|
||||||
when (differ.currentList[position]) {
|
when (getItem(position)) {
|
||||||
// Support an artist header, and special artist albums/songs.
|
// Support an artist header, and special artist albums/songs.
|
||||||
is Artist -> ArtistDetailViewHolder.VIEW_TYPE
|
is Artist -> ArtistDetailViewHolder.VIEW_TYPE
|
||||||
is Album -> ArtistAlbumViewHolder.VIEW_TYPE
|
is Album -> ArtistAlbumViewHolder.VIEW_TYPE
|
||||||
|
@ -65,7 +65,7 @@ class ArtistDetailAdapter(private val listener: Listener<Music>) :
|
||||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||||
super.onBindViewHolder(holder, position)
|
super.onBindViewHolder(holder, position)
|
||||||
// Re-binding an item with new data and not just a changed selection/playing state.
|
// Re-binding an item with new data and not just a changed selection/playing state.
|
||||||
when (val item = differ.currentList[position]) {
|
when (val item = getItem(position)) {
|
||||||
is Artist -> (holder as ArtistDetailViewHolder).bind(item, listener)
|
is Artist -> (holder as ArtistDetailViewHolder).bind(item, listener)
|
||||||
is Album -> (holder as ArtistAlbumViewHolder).bind(item, listener)
|
is Album -> (holder as ArtistAlbumViewHolder).bind(item, listener)
|
||||||
is Song -> (holder as ArtistSongViewHolder).bind(item, listener)
|
is Song -> (holder as ArtistSongViewHolder).bind(item, listener)
|
||||||
|
@ -73,9 +73,11 @@ class ArtistDetailAdapter(private val listener: Listener<Music>) :
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isItemFullWidth(position: Int): Boolean {
|
override fun isItemFullWidth(position: Int): Boolean {
|
||||||
|
if (super.isItemFullWidth(position)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
// Artist headers should be full-width in all configurations.
|
// Artist headers should be full-width in all configurations.
|
||||||
val item = differ.currentList[position]
|
return getItem(position) is Artist
|
||||||
return super.isItemFullWidth(position) || item is Artist
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
|
|
|
@ -20,7 +20,6 @@ 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.AsyncListDiffer
|
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import org.oxycblt.auxio.IntegerTable
|
import org.oxycblt.auxio.IntegerTable
|
||||||
|
@ -37,19 +36,19 @@ import org.oxycblt.auxio.util.inflater
|
||||||
/**
|
/**
|
||||||
* A [RecyclerView.Adapter] that implements behavior shared across each detail view's adapters.
|
* A [RecyclerView.Adapter] that implements behavior shared across each detail view's adapters.
|
||||||
* @param listener A [Listener] to bind interactions to.
|
* @param listener A [Listener] to bind interactions to.
|
||||||
* @param itemCallback A [DiffUtil.ItemCallback] to use with [AsyncListDiffer] when updating the
|
* @param diffCallback A [DiffUtil.ItemCallback] to use for item comparison when diffing the
|
||||||
* internal list.
|
* internal list.
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
abstract class DetailAdapter(
|
abstract class DetailAdapter(
|
||||||
private val listener: Listener<*>,
|
private val listener: Listener<*>,
|
||||||
itemCallback: DiffUtil.ItemCallback<Item>
|
diffCallback: DiffUtil.ItemCallback<Item>
|
||||||
) : SelectionIndicatorAdapter<RecyclerView.ViewHolder>(), AuxioRecyclerView.SpanSizeLookup {
|
) :
|
||||||
// Safe to leak this since the listener will not fire during initialization
|
SelectionIndicatorAdapter<Item, RecyclerView.ViewHolder>(ListDiffer.Async(diffCallback)),
|
||||||
@Suppress("LeakingThis") protected val differ = AsyncListDiffer(this, itemCallback)
|
AuxioRecyclerView.SpanSizeLookup {
|
||||||
|
|
||||||
override fun getItemViewType(position: Int) =
|
override fun getItemViewType(position: Int) =
|
||||||
when (differ.currentList[position]) {
|
when (getItem(position)) {
|
||||||
// Implement support for headers and sort headers
|
// Implement support for headers and sort headers
|
||||||
is Header -> HeaderViewHolder.VIEW_TYPE
|
is Header -> HeaderViewHolder.VIEW_TYPE
|
||||||
is SortHeader -> SortHeaderViewHolder.VIEW_TYPE
|
is SortHeader -> SortHeaderViewHolder.VIEW_TYPE
|
||||||
|
@ -64,7 +63,7 @@ abstract class DetailAdapter(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||||
when (val item = differ.currentList[position]) {
|
when (val item = getItem(position)) {
|
||||||
is Header -> (holder as HeaderViewHolder).bind(item)
|
is Header -> (holder as HeaderViewHolder).bind(item)
|
||||||
is SortHeader -> (holder as SortHeaderViewHolder).bind(item, listener)
|
is SortHeader -> (holder as SortHeaderViewHolder).bind(item, listener)
|
||||||
}
|
}
|
||||||
|
@ -72,22 +71,10 @@ abstract class DetailAdapter(
|
||||||
|
|
||||||
override fun isItemFullWidth(position: Int): Boolean {
|
override fun isItemFullWidth(position: Int): Boolean {
|
||||||
// Headers should be full-width in all configurations.
|
// Headers should be full-width in all configurations.
|
||||||
val item = differ.currentList[position]
|
val item = getItem(position)
|
||||||
return item is Header || item is SortHeader
|
return item is Header || item is SortHeader
|
||||||
}
|
}
|
||||||
|
|
||||||
override val currentList: List<Item>
|
|
||||||
get() = differ.currentList
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asynchronously update the list with new items. Assumes that the list only contains data
|
|
||||||
* supported by the concrete [DetailAdapter] implementation.
|
|
||||||
* @param newList The new [Item]s for the adapter to display.
|
|
||||||
*/
|
|
||||||
fun submitList(newList: List<Item>) {
|
|
||||||
differ.submitList(newList)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** An extended [SelectableListListener] for [DetailAdapter] implementations. */
|
/** An extended [SelectableListListener] for [DetailAdapter] implementations. */
|
||||||
interface Listener<in T : Music> : SelectableListListener<T> {
|
interface Listener<in T : Music> : SelectableListListener<T> {
|
||||||
// TODO: Split off into sub-listeners if a collapsing toolbar is implemented.
|
// TODO: Split off into sub-listeners if a collapsing toolbar is implemented.
|
||||||
|
|
|
@ -44,7 +44,7 @@ import org.oxycblt.auxio.util.inflater
|
||||||
class GenreDetailAdapter(private val listener: Listener<Music>) :
|
class GenreDetailAdapter(private val listener: Listener<Music>) :
|
||||||
DetailAdapter(listener, DIFF_CALLBACK) {
|
DetailAdapter(listener, DIFF_CALLBACK) {
|
||||||
override fun getItemViewType(position: Int) =
|
override fun getItemViewType(position: Int) =
|
||||||
when (differ.currentList[position]) {
|
when (getItem(position)) {
|
||||||
// Support the Genre header and generic Artist/Song items. There's nothing about
|
// 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 homogeneous, so it doesn't matter what we
|
||||||
// use for their ViewHolders.
|
// use for their ViewHolders.
|
||||||
|
@ -64,7 +64,7 @@ class GenreDetailAdapter(private val listener: Listener<Music>) :
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||||
super.onBindViewHolder(holder, position)
|
super.onBindViewHolder(holder, position)
|
||||||
when (val item = differ.currentList[position]) {
|
when (val item = getItem(position)) {
|
||||||
is Genre -> (holder as GenreDetailViewHolder).bind(item, listener)
|
is Genre -> (holder as GenreDetailViewHolder).bind(item, listener)
|
||||||
is Artist -> (holder as ArtistViewHolder).bind(item, listener)
|
is Artist -> (holder as ArtistViewHolder).bind(item, listener)
|
||||||
is Song -> (holder as SongViewHolder).bind(item, listener)
|
is Song -> (holder as SongViewHolder).bind(item, listener)
|
||||||
|
@ -72,9 +72,11 @@ class GenreDetailAdapter(private val listener: Listener<Music>) :
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isItemFullWidth(position: Int): Boolean {
|
override fun isItemFullWidth(position: Int): Boolean {
|
||||||
|
if (super.isItemFullWidth(position)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
// Genre headers should be full-width in all configurations
|
// Genre headers should be full-width in all configurations
|
||||||
val item = differ.currentList[position]
|
return getItem(position) is Genre
|
||||||
return super.isItemFullWidth(position) || item is Genre
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
|
|
|
@ -31,8 +31,8 @@ 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.AlbumViewHolder
|
import org.oxycblt.auxio.list.recycler.AlbumViewHolder
|
||||||
|
import org.oxycblt.auxio.list.recycler.ListDiffer
|
||||||
import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter
|
import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter
|
||||||
import org.oxycblt.auxio.list.recycler.SyncListDiffer
|
|
||||||
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
|
||||||
|
@ -67,7 +67,7 @@ class AlbumListFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
collectImmediately(homeModel.albumsList, albumAdapter::replaceList)
|
collectImmediately(homeModel.albumsList, albumAdapter::replaceList)
|
||||||
collectImmediately(selectionModel.selected, albumAdapter::setSelectedItems)
|
collectImmediately(selectionModel.selected, ::updateSelection)
|
||||||
collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -130,9 +130,13 @@ class AlbumListFragment :
|
||||||
openMusicMenu(anchor, R.menu.menu_album_actions, item)
|
openMusicMenu(anchor, R.menu.menu_album_actions, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun updateSelection(selection: List<Music>) {
|
||||||
|
albumAdapter.setSelected(selection.filterIsInstanceTo(mutableSetOf()))
|
||||||
|
}
|
||||||
|
|
||||||
private fun updatePlayback(parent: MusicParent?, isPlaying: Boolean) {
|
private fun updatePlayback(parent: MusicParent?, isPlaying: Boolean) {
|
||||||
// If an album is playing, highlight it within this adapter.
|
// If an album is playing, highlight it within this adapter.
|
||||||
albumAdapter.setPlayingItem(parent as? Album, isPlaying)
|
albumAdapter.setPlaying(parent as? Album, isPlaying)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -140,25 +144,14 @@ class AlbumListFragment :
|
||||||
* @param listener An [SelectableListListener] to bind interactions to.
|
* @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<AlbumViewHolder>() {
|
SelectionIndicatorAdapter<Album, AlbumViewHolder>(
|
||||||
private val differ = SyncListDiffer(this, AlbumViewHolder.DIFF_CALLBACK)
|
ListDiffer.Async(AlbumViewHolder.DIFF_CALLBACK)) {
|
||||||
|
|
||||||
override val currentList: List<Item>
|
|
||||||
get() = differ.currentList
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
||||||
AlbumViewHolder.from(parent)
|
AlbumViewHolder.from(parent)
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: AlbumViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: AlbumViewHolder, position: Int) {
|
||||||
holder.bind(differ.currentList[position], listener)
|
holder.bind(getItem(position), listener)
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asynchronously update the list with new [Album]s.
|
|
||||||
* @param newList The new [Album]s for the adapter to display.
|
|
||||||
*/
|
|
||||||
fun replaceList(newList: List<Album>) {
|
|
||||||
differ.replaceList(newList)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,9 +29,10 @@ 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.ArtistViewHolder
|
import org.oxycblt.auxio.list.recycler.ArtistViewHolder
|
||||||
|
import org.oxycblt.auxio.list.recycler.ListDiffer
|
||||||
import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter
|
import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter
|
||||||
import org.oxycblt.auxio.list.recycler.SyncListDiffer
|
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.MusicMode
|
import org.oxycblt.auxio.music.MusicMode
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
import org.oxycblt.auxio.music.library.Sort
|
import org.oxycblt.auxio.music.library.Sort
|
||||||
|
@ -48,7 +49,7 @@ class ArtistListFragment :
|
||||||
FastScrollRecyclerView.PopupProvider,
|
FastScrollRecyclerView.PopupProvider,
|
||||||
FastScrollRecyclerView.Listener {
|
FastScrollRecyclerView.Listener {
|
||||||
private val homeModel: HomeViewModel by activityViewModels()
|
private val homeModel: HomeViewModel by activityViewModels()
|
||||||
private val homeAdapter = ArtistAdapter(this)
|
private val artistAdapter = ArtistAdapter(this)
|
||||||
|
|
||||||
override fun onCreateBinding(inflater: LayoutInflater) =
|
override fun onCreateBinding(inflater: LayoutInflater) =
|
||||||
FragmentHomeListBinding.inflate(inflater)
|
FragmentHomeListBinding.inflate(inflater)
|
||||||
|
@ -58,13 +59,13 @@ class ArtistListFragment :
|
||||||
|
|
||||||
binding.homeRecycler.apply {
|
binding.homeRecycler.apply {
|
||||||
id = R.id.home_artist_recycler
|
id = R.id.home_artist_recycler
|
||||||
adapter = homeAdapter
|
adapter = artistAdapter
|
||||||
popupProvider = this@ArtistListFragment
|
popupProvider = this@ArtistListFragment
|
||||||
listener = this@ArtistListFragment
|
listener = this@ArtistListFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
collectImmediately(homeModel.artistsList, homeAdapter::replaceList)
|
collectImmediately(homeModel.artistsList, artistAdapter::replaceList)
|
||||||
collectImmediately(selectionModel.selected, homeAdapter::setSelectedItems)
|
collectImmediately(selectionModel.selected, ::updateSelection)
|
||||||
collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,9 +108,13 @@ class ArtistListFragment :
|
||||||
openMusicMenu(anchor, R.menu.menu_artist_actions, item)
|
openMusicMenu(anchor, R.menu.menu_artist_actions, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun updateSelection(selection: List<Music>) {
|
||||||
|
artistAdapter.setSelected(selection.filterIsInstanceTo(mutableSetOf()))
|
||||||
|
}
|
||||||
|
|
||||||
private fun updatePlayback(parent: MusicParent?, isPlaying: Boolean) {
|
private fun updatePlayback(parent: MusicParent?, isPlaying: Boolean) {
|
||||||
// If an artist is playing, highlight it within this adapter.
|
// If an artist is playing, highlight it within this adapter.
|
||||||
homeAdapter.setPlayingItem(parent as? Artist, isPlaying)
|
artistAdapter.setPlaying(parent as? Artist, isPlaying)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -117,25 +122,14 @@ class ArtistListFragment :
|
||||||
* @param listener An [SelectableListListener] to bind interactions to.
|
* @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<ArtistViewHolder>() {
|
SelectionIndicatorAdapter<Artist, ArtistViewHolder>(
|
||||||
private val differ = SyncListDiffer(this, ArtistViewHolder.DIFF_CALLBACK)
|
ListDiffer.Async(ArtistViewHolder.DIFF_CALLBACK)) {
|
||||||
|
|
||||||
override val currentList: List<Item>
|
|
||||||
get() = differ.currentList
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
||||||
ArtistViewHolder.from(parent)
|
ArtistViewHolder.from(parent)
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: ArtistViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: ArtistViewHolder, position: Int) {
|
||||||
holder.bind(differ.currentList[position], listener)
|
holder.bind(getItem(position), listener)
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asynchronously update the list with new [Artist]s.
|
|
||||||
* @param newList The new [Artist]s for the adapter to display.
|
|
||||||
*/
|
|
||||||
fun replaceList(newList: List<Artist>) {
|
|
||||||
differ.replaceList(newList)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,9 +29,10 @@ 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.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.list.recycler.SelectionIndicatorAdapter
|
||||||
import org.oxycblt.auxio.list.recycler.SyncListDiffer
|
|
||||||
import org.oxycblt.auxio.music.Genre
|
import org.oxycblt.auxio.music.Genre
|
||||||
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.MusicMode
|
import org.oxycblt.auxio.music.MusicMode
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
import org.oxycblt.auxio.music.library.Sort
|
import org.oxycblt.auxio.music.library.Sort
|
||||||
|
@ -47,7 +48,7 @@ class GenreListFragment :
|
||||||
FastScrollRecyclerView.PopupProvider,
|
FastScrollRecyclerView.PopupProvider,
|
||||||
FastScrollRecyclerView.Listener {
|
FastScrollRecyclerView.Listener {
|
||||||
private val homeModel: HomeViewModel by activityViewModels()
|
private val homeModel: HomeViewModel by activityViewModels()
|
||||||
private val homeAdapter = GenreAdapter(this)
|
private val genreAdapter = GenreAdapter(this)
|
||||||
|
|
||||||
override fun onCreateBinding(inflater: LayoutInflater) =
|
override fun onCreateBinding(inflater: LayoutInflater) =
|
||||||
FragmentHomeListBinding.inflate(inflater)
|
FragmentHomeListBinding.inflate(inflater)
|
||||||
|
@ -57,13 +58,13 @@ class GenreListFragment :
|
||||||
|
|
||||||
binding.homeRecycler.apply {
|
binding.homeRecycler.apply {
|
||||||
id = R.id.home_genre_recycler
|
id = R.id.home_genre_recycler
|
||||||
adapter = homeAdapter
|
adapter = genreAdapter
|
||||||
popupProvider = this@GenreListFragment
|
popupProvider = this@GenreListFragment
|
||||||
listener = this@GenreListFragment
|
listener = this@GenreListFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
collectImmediately(homeModel.genresList, homeAdapter::replaceList)
|
collectImmediately(homeModel.genresList, genreAdapter::replaceList)
|
||||||
collectImmediately(selectionModel.selected, homeAdapter::setSelectedItems)
|
collectImmediately(selectionModel.selected, ::updateSelection)
|
||||||
collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,9 +107,13 @@ class GenreListFragment :
|
||||||
openMusicMenu(anchor, R.menu.menu_artist_actions, item)
|
openMusicMenu(anchor, R.menu.menu_artist_actions, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun updateSelection(selection: List<Music>) {
|
||||||
|
genreAdapter.setSelected(selection.filterIsInstanceTo(mutableSetOf()))
|
||||||
|
}
|
||||||
|
|
||||||
private fun updatePlayback(parent: MusicParent?, isPlaying: Boolean) {
|
private fun updatePlayback(parent: MusicParent?, isPlaying: Boolean) {
|
||||||
// If a genre is playing, highlight it within this adapter.
|
// If a genre is playing, highlight it within this adapter.
|
||||||
homeAdapter.setPlayingItem(parent as? Genre, isPlaying)
|
genreAdapter.setPlaying(parent as? Genre, isPlaying)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -116,25 +121,13 @@ class GenreListFragment :
|
||||||
* @param listener An [SelectableListListener] to bind interactions to.
|
* @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<GenreViewHolder>() {
|
SelectionIndicatorAdapter<Genre, GenreViewHolder>(
|
||||||
private val differ = SyncListDiffer(this, GenreViewHolder.DIFF_CALLBACK)
|
ListDiffer.Async(GenreViewHolder.DIFF_CALLBACK)) {
|
||||||
|
|
||||||
override val currentList: List<Item>
|
|
||||||
get() = differ.currentList
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
||||||
GenreViewHolder.from(parent)
|
GenreViewHolder.from(parent)
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: GenreViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: GenreViewHolder, position: Int) {
|
||||||
holder.bind(differ.currentList[position], listener)
|
holder.bind(getItem(position), listener)
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asynchronously update the list with new [Genre]s.
|
|
||||||
* @param newList The new [Genre]s for the adapter to display.
|
|
||||||
*/
|
|
||||||
fun replaceList(newList: List<Genre>) {
|
|
||||||
differ.replaceList(newList)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,9 +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.recycler.ListDiffer
|
||||||
import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter
|
import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter
|
||||||
import org.oxycblt.auxio.list.recycler.SongViewHolder
|
import org.oxycblt.auxio.list.recycler.SongViewHolder
|
||||||
import org.oxycblt.auxio.list.recycler.SyncListDiffer
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.MusicMode
|
import org.oxycblt.auxio.music.MusicMode
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
|
@ -50,7 +51,7 @@ class SongListFragment :
|
||||||
FastScrollRecyclerView.PopupProvider,
|
FastScrollRecyclerView.PopupProvider,
|
||||||
FastScrollRecyclerView.Listener {
|
FastScrollRecyclerView.Listener {
|
||||||
private val homeModel: HomeViewModel by activityViewModels()
|
private val homeModel: HomeViewModel by activityViewModels()
|
||||||
private val homeAdapter = SongAdapter(this)
|
private val songAdapter = SongAdapter(this)
|
||||||
// Save memory by re-using the same formatter and string builder when creating popup text
|
// Save memory by re-using the same formatter and string builder when creating popup text
|
||||||
private val formatterSb = StringBuilder(64)
|
private val formatterSb = StringBuilder(64)
|
||||||
private val formatter = Formatter(formatterSb)
|
private val formatter = Formatter(formatterSb)
|
||||||
|
@ -63,13 +64,13 @@ class SongListFragment :
|
||||||
|
|
||||||
binding.homeRecycler.apply {
|
binding.homeRecycler.apply {
|
||||||
id = R.id.home_song_recycler
|
id = R.id.home_song_recycler
|
||||||
adapter = homeAdapter
|
adapter = songAdapter
|
||||||
popupProvider = this@SongListFragment
|
popupProvider = this@SongListFragment
|
||||||
listener = this@SongListFragment
|
listener = this@SongListFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
collectImmediately(homeModel.songLists, homeAdapter::replaceList)
|
collectImmediately(homeModel.songLists, songAdapter::replaceList)
|
||||||
collectImmediately(selectionModel.selected, homeAdapter::setSelectedItems)
|
collectImmediately(selectionModel.selected, ::updateSelection)
|
||||||
collectImmediately(
|
collectImmediately(
|
||||||
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
||||||
}
|
}
|
||||||
|
@ -136,12 +137,16 @@ class SongListFragment :
|
||||||
openMusicMenu(anchor, R.menu.menu_song_actions, item)
|
openMusicMenu(anchor, R.menu.menu_song_actions, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun updateSelection(selection: List<Music>) {
|
||||||
|
songAdapter.setSelected(selection.filterIsInstanceTo(mutableSetOf()))
|
||||||
|
}
|
||||||
|
|
||||||
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
|
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
|
||||||
if (parent == null) {
|
if (parent == null) {
|
||||||
homeAdapter.setPlayingItem(song, isPlaying)
|
songAdapter.setPlaying(song, isPlaying)
|
||||||
} else {
|
} else {
|
||||||
// Ignore playback that is not from all songs
|
// Ignore playback that is not from all songs
|
||||||
homeAdapter.setPlayingItem(null, isPlaying)
|
songAdapter.setPlaying(null, isPlaying)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,25 +155,14 @@ class SongListFragment :
|
||||||
* @param listener An [SelectableListListener] to bind interactions to.
|
* @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<SongViewHolder>() {
|
SelectionIndicatorAdapter<Song, SongViewHolder>(
|
||||||
private val differ = SyncListDiffer(this, SongViewHolder.DIFF_CALLBACK)
|
ListDiffer.Async(SongViewHolder.DIFF_CALLBACK)) {
|
||||||
|
|
||||||
override val currentList: List<Item>
|
|
||||||
get() = differ.currentList
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
||||||
SongViewHolder.from(parent)
|
SongViewHolder.from(parent)
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: SongViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: SongViewHolder, position: Int) {
|
||||||
holder.bind(differ.currentList[position], listener)
|
holder.bind(getItem(position), listener)
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asynchronously update the list with new [Song]s.
|
|
||||||
* @param newList The new [Song]s for the adapter to display.
|
|
||||||
*/
|
|
||||||
fun replaceList(newList: List<Song>) {
|
|
||||||
differ.replaceList(newList)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,20 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Auxio Project
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.list
|
package org.oxycblt.auxio.list
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -12,8 +29,8 @@ enum class UpdateInstructions {
|
||||||
DIFF,
|
DIFF,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Synchronously remove the current list and replace it with a new one. This should be used
|
* Synchronously remove the current list and replace it with a new one. This should be used for
|
||||||
* for large diffs with that would cause erratic scroll behavior or in-efficiency.
|
* large diffs with that would cause erratic scroll behavior or in-efficiency.
|
||||||
*/
|
*/
|
||||||
REPLACE
|
REPLACE
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Auxio Project
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.oxycblt.auxio.list.recycler
|
||||||
|
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import org.oxycblt.auxio.list.UpdateInstructions
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [RecyclerView.Adapter] with [ListDiffer] integration.
|
||||||
|
* @param differFactory The [ListDiffer.Factory] that defines the type of [ListDiffer] to use.
|
||||||
|
*/
|
||||||
|
abstract class DiffAdapter<T, VH : RecyclerView.ViewHolder>(differFactory: ListDiffer.Factory<T>) :
|
||||||
|
RecyclerView.Adapter<VH>() {
|
||||||
|
private val differ = differFactory.new(@Suppress("LeakingThis") this)
|
||||||
|
|
||||||
|
final override fun getItemCount() = differ.currentList.size
|
||||||
|
|
||||||
|
/** The current list of [T] items. */
|
||||||
|
val currentList: List<T>
|
||||||
|
get() = differ.currentList
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a [T] item at the given position.
|
||||||
|
* @param at The position to get the item at.
|
||||||
|
* @throws IndexOutOfBoundsException If the index is not in the list bounds/
|
||||||
|
*/
|
||||||
|
fun getItem(at: Int) = differ.currentList[at]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dynamically determine how to update the list based on the given [UpdateInstructions].
|
||||||
|
* @param newList The new list of [T] items to show.
|
||||||
|
* @param instructions The [UpdateInstructions] specifying how to update the list.
|
||||||
|
*/
|
||||||
|
fun submitList(newList: List<T>, instructions: UpdateInstructions) {
|
||||||
|
when (instructions) {
|
||||||
|
UpdateInstructions.DIFF -> diffList(newList)
|
||||||
|
UpdateInstructions.REPLACE -> replaceList(newList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update this list using [DiffUtil]. This can simplify the work of updating the list, but can
|
||||||
|
* also cause erratic behavior.
|
||||||
|
* @param newList The new list of [T] items to show.
|
||||||
|
* @param onDone Callback that will be invoked when the update is completed, allowing means to
|
||||||
|
* reset the state.
|
||||||
|
*/
|
||||||
|
fun diffList(newList: List<T>, onDone: () -> Unit = {}) = differ.diffList(newList, onDone)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visually replace the previous list with a new list. This is useful for large diffs that are
|
||||||
|
* too erratic for [diffList].
|
||||||
|
* @param newList The new list of [T] items to show.
|
||||||
|
*/
|
||||||
|
fun replaceList(newList: List<T>) = differ.replaceList(newList)
|
||||||
|
}
|
224
app/src/main/java/org/oxycblt/auxio/list/recycler/ListDiffer.kt
Normal file
224
app/src/main/java/org/oxycblt/auxio/list/recycler/ListDiffer.kt
Normal file
|
@ -0,0 +1,224 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Auxio Project
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.oxycblt.auxio.list.recycler
|
||||||
|
|
||||||
|
import androidx.recyclerview.widget.AdapterListUpdateCallback
|
||||||
|
import androidx.recyclerview.widget.AsyncDifferConfig
|
||||||
|
import androidx.recyclerview.widget.AsyncListDiffer
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.ListUpdateCallback
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import java.lang.reflect.Field
|
||||||
|
import org.oxycblt.auxio.util.lazyReflectedField
|
||||||
|
import org.oxycblt.auxio.util.requireIs
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List differ wrapper that provides more flexibility regarding the way lists are updated.
|
||||||
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
|
*/
|
||||||
|
interface ListDiffer<T> {
|
||||||
|
/** The current list of [T] items. */
|
||||||
|
val currentList: List<T>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update this list using [DiffUtil]. This can simplify the work of updating the list, but can
|
||||||
|
* also cause erratic behavior.
|
||||||
|
* @param newList The new list of [T] items to show.
|
||||||
|
* @param onDone Callback that will be invoked when the update is completed, allowing means to
|
||||||
|
* reset the state.
|
||||||
|
*/
|
||||||
|
fun diffList(newList: List<T>, onDone: () -> Unit = {})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visually replace the previous list with a new list. This is useful for large diffs that are
|
||||||
|
* too erratic for [diffList].
|
||||||
|
* @param newList The new list of [T] items to show.
|
||||||
|
*/
|
||||||
|
fun replaceList(newList: List<T>)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the creation of new [ListDiffer] instances. Allows such [ListDiffer]s to be passed as
|
||||||
|
* arguments without reliance on a `this` [RecyclerView.Adapter].
|
||||||
|
*/
|
||||||
|
abstract class Factory<T> {
|
||||||
|
/**
|
||||||
|
* Create a new [ListDiffer] bound to the given [RecyclerView.Adapter].
|
||||||
|
* @param adapter The [RecyclerView.Adapter] to bind to.
|
||||||
|
*/
|
||||||
|
abstract fun new(adapter: RecyclerView.Adapter<*>): ListDiffer<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update lists on another thread. This is useful when large diffs are likely to occur in this
|
||||||
|
* list that would be exceedingly slow with [Blocking].
|
||||||
|
* @param diffCallback A [DiffUtil.ItemCallback] to use for item comparison when diffing the
|
||||||
|
* internal list.
|
||||||
|
*/
|
||||||
|
class Async<T>(private val diffCallback: DiffUtil.ItemCallback<T>) : Factory<T>() {
|
||||||
|
override fun new(adapter: RecyclerView.Adapter<*>): ListDiffer<T> =
|
||||||
|
RealAsyncListDiffer(AdapterListUpdateCallback(adapter), diffCallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update lists on the main thread. This is useful when many small, discrete list diffs are
|
||||||
|
* likely to occur that would cause [Async] to get race conditions.
|
||||||
|
* @param diffCallback A [DiffUtil.ItemCallback] to use for item comparison when diffing the
|
||||||
|
* internal list.
|
||||||
|
*/
|
||||||
|
class Blocking<T>(private val diffCallback: DiffUtil.ItemCallback<T>) : Factory<T>() {
|
||||||
|
override fun new(adapter: RecyclerView.Adapter<*>): ListDiffer<T> =
|
||||||
|
RealBlockingListDiffer(AdapterListUpdateCallback(adapter), diffCallback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class RealAsyncListDiffer<T>(
|
||||||
|
private val updateCallback: ListUpdateCallback,
|
||||||
|
diffCallback: DiffUtil.ItemCallback<T>
|
||||||
|
) : ListDiffer<T> {
|
||||||
|
private val inner =
|
||||||
|
AsyncListDiffer(updateCallback, AsyncDifferConfig.Builder(diffCallback).build())
|
||||||
|
|
||||||
|
override val currentList: List<T>
|
||||||
|
get() = inner.currentList
|
||||||
|
|
||||||
|
override fun diffList(newList: List<T>, onDone: () -> Unit) {
|
||||||
|
inner.submitList(newList, onDone)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun replaceList(newList: List<T>) {
|
||||||
|
if (inner.currentList == newList) {
|
||||||
|
// Nothing to do.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Do possibly the most idiotic thing possible and mutate the internal differ state
|
||||||
|
// so we don't have to deal with any disjoint list garbage. This should cancel any prior
|
||||||
|
// updates and correctly set up the list values while still allowing for the same
|
||||||
|
// visual animation as the blocking replaceList.
|
||||||
|
val oldListSize = inner.currentList.size
|
||||||
|
ASD_MAX_GENERATION_FIELD.set(inner, requireIs<Int>(ASD_MAX_GENERATION_FIELD.get(inner)) + 1)
|
||||||
|
ASD_MUTABLE_LIST_FIELD.set(inner, newList.ifEmpty { null })
|
||||||
|
ASD_READ_ONLY_LIST_FIELD.set(inner, newList)
|
||||||
|
updateCallback.onRemoved(0, oldListSize)
|
||||||
|
updateCallback.onInserted(0, newList.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
val ASD_MAX_GENERATION_FIELD: Field by
|
||||||
|
lazyReflectedField(AsyncListDiffer::class, "mMaxScheduledGeneration")
|
||||||
|
val ASD_MUTABLE_LIST_FIELD: Field by lazyReflectedField(AsyncListDiffer::class, "mList")
|
||||||
|
val ASD_READ_ONLY_LIST_FIELD: Field by
|
||||||
|
lazyReflectedField(AsyncListDiffer::class, "mReadOnlyList")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class RealBlockingListDiffer<T>(
|
||||||
|
private val updateCallback: ListUpdateCallback,
|
||||||
|
private val diffCallback: DiffUtil.ItemCallback<T>
|
||||||
|
) : ListDiffer<T> {
|
||||||
|
override var currentList = listOf<T>()
|
||||||
|
|
||||||
|
override fun diffList(newList: List<T>, onDone: () -> Unit) {
|
||||||
|
if (newList === currentList || newList.isEmpty() && currentList.isEmpty()) {
|
||||||
|
onDone()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newList.isEmpty()) {
|
||||||
|
val oldListSize = currentList.size
|
||||||
|
currentList = listOf()
|
||||||
|
updateCallback.onRemoved(0, oldListSize)
|
||||||
|
onDone()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentList.isEmpty()) {
|
||||||
|
currentList = newList
|
||||||
|
updateCallback.onInserted(0, newList.size)
|
||||||
|
onDone()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val oldList = currentList
|
||||||
|
val result =
|
||||||
|
DiffUtil.calculateDiff(
|
||||||
|
object : DiffUtil.Callback() {
|
||||||
|
override fun getOldListSize(): Int {
|
||||||
|
return oldList.size
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getNewListSize(): Int {
|
||||||
|
return newList.size
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areItemsTheSame(
|
||||||
|
oldItemPosition: Int,
|
||||||
|
newItemPosition: Int
|
||||||
|
): Boolean {
|
||||||
|
val oldItem: T? = oldList[oldItemPosition]
|
||||||
|
val newItem: T? = newList[newItemPosition]
|
||||||
|
return if (oldItem != null && newItem != null) {
|
||||||
|
diffCallback.areItemsTheSame(oldItem, newItem)
|
||||||
|
} else {
|
||||||
|
oldItem == null && newItem == null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(
|
||||||
|
oldItemPosition: Int,
|
||||||
|
newItemPosition: Int
|
||||||
|
): Boolean {
|
||||||
|
val oldItem: T? = oldList[oldItemPosition]
|
||||||
|
val newItem: T? = newList[newItemPosition]
|
||||||
|
return if (oldItem != null && newItem != null) {
|
||||||
|
diffCallback.areContentsTheSame(oldItem, newItem)
|
||||||
|
} else if (oldItem == null && newItem == null) {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
throw AssertionError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getChangePayload(
|
||||||
|
oldItemPosition: Int,
|
||||||
|
newItemPosition: Int
|
||||||
|
): Any? {
|
||||||
|
val oldItem: T? = oldList[oldItemPosition]
|
||||||
|
val newItem: T? = newList[newItemPosition]
|
||||||
|
return if (oldItem != null && newItem != null) {
|
||||||
|
diffCallback.getChangePayload(oldItem, newItem)
|
||||||
|
} else {
|
||||||
|
throw AssertionError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
currentList = newList
|
||||||
|
result.dispatchUpdatesTo(updateCallback)
|
||||||
|
onDone()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun replaceList(newList: List<T>) {
|
||||||
|
if (currentList == newList) {
|
||||||
|
// Nothing to do.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
diffList(listOf())
|
||||||
|
diffList(newList)
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,33 +19,27 @@ package org.oxycblt.auxio.list.recycler
|
||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import org.oxycblt.auxio.list.Item
|
|
||||||
import org.oxycblt.auxio.music.Music
|
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [RecyclerView.Adapter] that supports indicating the playback status of a particular item.
|
* A [RecyclerView.Adapter] that supports indicating the playback status of a particular item.
|
||||||
|
* @param differFactory The [ListDiffer.Factory] that defines the type of [ListDiffer] to use.
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
abstract class PlayingIndicatorAdapter<VH : RecyclerView.ViewHolder> : RecyclerView.Adapter<VH>() {
|
abstract class PlayingIndicatorAdapter<T, VH : RecyclerView.ViewHolder>(
|
||||||
|
differFactory: ListDiffer.Factory<T>
|
||||||
|
) : DiffAdapter<T, VH>(differFactory) {
|
||||||
// There are actually two states for this adapter:
|
// There are actually two states for this adapter:
|
||||||
// - The currently playing item, which is usually marked as "selected" and becomes accented.
|
// - The currently playing item, which is usually marked as "selected" and becomes accented.
|
||||||
// - Whether playback is ongoing, which corresponds to whether the item's ImageGroup is
|
// - Whether playback is ongoing, which corresponds to whether the item's ImageGroup is
|
||||||
// marked as "playing" or not.
|
// marked as "playing" or not.
|
||||||
private var currentMusic: Music? = null
|
private var currentItem: T? = null
|
||||||
private var isPlaying = false
|
private var isPlaying = false
|
||||||
|
|
||||||
/**
|
|
||||||
* The current list of the adapter. This is used to update items if the indicator state changes.
|
|
||||||
*/
|
|
||||||
abstract val currentList: List<Item>
|
|
||||||
|
|
||||||
override fun getItemCount() = currentList.size
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: VH, position: Int, payloads: List<Any>) {
|
override fun onBindViewHolder(holder: VH, position: Int, payloads: List<Any>) {
|
||||||
// Only try to update the playing indicator if the ViewHolder supports it
|
// Only try to update the playing indicator if the ViewHolder supports it
|
||||||
if (holder is ViewHolder) {
|
if (holder is ViewHolder) {
|
||||||
holder.updatePlayingIndicator(currentList[position] == currentMusic, isPlaying)
|
holder.updatePlayingIndicator(currentList[position] == currentItem, isPlaying)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (payloads.isEmpty()) {
|
if (payloads.isEmpty()) {
|
||||||
|
@ -56,14 +50,14 @@ abstract class PlayingIndicatorAdapter<VH : RecyclerView.ViewHolder> : RecyclerV
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Update the currently playing item in the list.
|
* Update the currently playing item in the list.
|
||||||
* @param music The [Music] currently being played, or null if it is not being played.
|
* @param item The [T] currently being played, or null if it is not being played.
|
||||||
* @param isPlaying Whether playback is ongoing or paused.
|
* @param isPlaying Whether playback is ongoing or paused.
|
||||||
*/
|
*/
|
||||||
fun setPlayingItem(music: Music?, isPlaying: Boolean) {
|
fun setPlaying(item: T?, isPlaying: Boolean) {
|
||||||
var updatedItem = false
|
var updatedItem = false
|
||||||
if (currentMusic != music) {
|
if (currentItem != item) {
|
||||||
val oldItem = currentMusic
|
val oldItem = currentItem
|
||||||
currentMusic = music
|
currentItem = item
|
||||||
|
|
||||||
// Remove the playing indicator from the old item
|
// Remove the playing indicator from the old item
|
||||||
if (oldItem != null) {
|
if (oldItem != null) {
|
||||||
|
@ -76,8 +70,8 @@ abstract class PlayingIndicatorAdapter<VH : RecyclerView.ViewHolder> : RecyclerV
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enable the playing indicator on the new item
|
// Enable the playing indicator on the new item
|
||||||
if (music != null) {
|
if (item != null) {
|
||||||
val pos = currentList.indexOfFirst { it == music }
|
val pos = currentList.indexOfFirst { it == item }
|
||||||
if (pos > -1) {
|
if (pos > -1) {
|
||||||
notifyItemChanged(pos, PAYLOAD_PLAYING_INDICATOR_CHANGED)
|
notifyItemChanged(pos, PAYLOAD_PLAYING_INDICATOR_CHANGED)
|
||||||
} else {
|
} else {
|
||||||
|
@ -94,8 +88,8 @@ abstract class PlayingIndicatorAdapter<VH : RecyclerView.ViewHolder> : RecyclerV
|
||||||
// We may have already called notifyItemChanged before when checking
|
// We may have already called notifyItemChanged before when checking
|
||||||
// if the item was being played, so in that case we don't need to
|
// if the item was being played, so in that case we don't need to
|
||||||
// update again here.
|
// update again here.
|
||||||
if (!updatedItem && music != null) {
|
if (!updatedItem && item != null) {
|
||||||
val pos = currentList.indexOfFirst { it == music }
|
val pos = currentList.indexOfFirst { it == item }
|
||||||
if (pos > -1) {
|
if (pos > -1) {
|
||||||
notifyItemChanged(pos, PAYLOAD_PLAYING_INDICATOR_CHANGED)
|
notifyItemChanged(pos, PAYLOAD_PLAYING_INDICATOR_CHANGED)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -24,11 +24,13 @@ import org.oxycblt.auxio.music.Music
|
||||||
/**
|
/**
|
||||||
* A [PlayingIndicatorAdapter] that also supports indicating the selection status of a group of
|
* A [PlayingIndicatorAdapter] that also supports indicating the selection status of a group of
|
||||||
* items.
|
* items.
|
||||||
|
* @param differFactory The [ListDiffer.Factory] that defines the type of [ListDiffer] to use.
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
abstract class SelectionIndicatorAdapter<VH : RecyclerView.ViewHolder> :
|
abstract class SelectionIndicatorAdapter<T, VH : RecyclerView.ViewHolder>(
|
||||||
PlayingIndicatorAdapter<VH>() {
|
differFactory: ListDiffer.Factory<T>
|
||||||
private var selectedItems = setOf<Music>()
|
) : PlayingIndicatorAdapter<T, VH>(differFactory) {
|
||||||
|
private var selectedItems = setOf<T>()
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: VH, position: Int, payloads: List<Any>) {
|
override fun onBindViewHolder(holder: VH, position: Int, payloads: List<Any>) {
|
||||||
super.onBindViewHolder(holder, position, payloads)
|
super.onBindViewHolder(holder, position, payloads)
|
||||||
|
@ -39,9 +41,9 @@ abstract class SelectionIndicatorAdapter<VH : RecyclerView.ViewHolder> :
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the list of selected items.
|
* Update the list of selected items.
|
||||||
* @param items A list of selected [Music].
|
* @param items A set of selected [T] items.
|
||||||
*/
|
*/
|
||||||
fun setSelectedItems(items: List<Music>) {
|
fun setSelected(items: Set<T>) {
|
||||||
val oldSelectedItems = selectedItems
|
val oldSelectedItems = selectedItems
|
||||||
val newSelectedItems = items.toSet()
|
val newSelectedItems = items.toSet()
|
||||||
if (newSelectedItems == oldSelectedItems) {
|
if (newSelectedItems == oldSelectedItems) {
|
||||||
|
|
|
@ -64,8 +64,7 @@ class MetadataExtractor(
|
||||||
/**
|
/**
|
||||||
* Returns a flow that parses all [Song.Raw] instances queued by the sub-extractors. This will
|
* Returns a flow that parses all [Song.Raw] instances queued by the sub-extractors. This will
|
||||||
* first delegate to the sub-extractors before parsing the metadata itself.
|
* first delegate to the sub-extractors before parsing the metadata itself.
|
||||||
* @param emit A listener that will be invoked with every new [Song.Raw] instance when they are
|
* @return A flow of [Song.Raw] instances.
|
||||||
* successfully loaded.
|
|
||||||
*/
|
*/
|
||||||
fun extract() = flow {
|
fun extract() = flow {
|
||||||
while (true) {
|
while (true) {
|
||||||
|
@ -310,8 +309,9 @@ class Task(context: Context, private val raw: Song.Raw) {
|
||||||
// Album artist
|
// Album artist
|
||||||
comments["musicbrainz_albumartistid"]?.let { raw.albumArtistMusicBrainzIds = it }
|
comments["musicbrainz_albumartistid"]?.let { raw.albumArtistMusicBrainzIds = it }
|
||||||
(comments["albumartists"] ?: comments["albumartist"])?.let { raw.albumArtistNames = it }
|
(comments["albumartists"] ?: comments["albumartist"])?.let { raw.albumArtistNames = it }
|
||||||
(comments["albumartists_sort"] ?: comments["albumartistsort"])
|
(comments["albumartists_sort"] ?: comments["albumartistsort"])?.let {
|
||||||
?.let { raw.albumArtistSortNames = it }
|
raw.albumArtistSortNames = it
|
||||||
|
}
|
||||||
|
|
||||||
// Genre
|
// Genre
|
||||||
comments["genre"]?.let { raw.genreNames = it }
|
comments["genre"]?.let { raw.genreNames = it }
|
||||||
|
|
|
@ -199,7 +199,7 @@ interface PlaybackSettings : Settings<PlaybackSettings.Listener> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
private companion object {
|
||||||
const val OLD_KEY_ALT_NOTIF_ACTION = "KEY_ALT_NOTIF_ACTION"
|
const val OLD_KEY_ALT_NOTIF_ACTION = "KEY_ALT_NOTIF_ACTION"
|
||||||
const val OLD_KEY_LIB_PLAYBACK_MODE = "KEY_SONG_PLAY_MODE2"
|
const val OLD_KEY_LIB_PLAYBACK_MODE = "KEY_SONG_PLAY_MODE2"
|
||||||
const val OLD_KEY_DETAIL_PLAYBACK_MODE = "auxio_detail_song_play_mode"
|
const val OLD_KEY_DETAIL_PLAYBACK_MODE = "auxio_detail_song_play_mode"
|
||||||
|
|
|
@ -27,9 +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.DiffAdapter
|
||||||
|
import org.oxycblt.auxio.list.recycler.ListDiffer
|
||||||
import org.oxycblt.auxio.list.recycler.PlayingIndicatorAdapter
|
import org.oxycblt.auxio.list.recycler.PlayingIndicatorAdapter
|
||||||
import org.oxycblt.auxio.list.recycler.SongViewHolder
|
import org.oxycblt.auxio.list.recycler.SongViewHolder
|
||||||
import org.oxycblt.auxio.list.recycler.SyncListDiffer
|
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.util.*
|
import org.oxycblt.auxio.util.*
|
||||||
|
|
||||||
|
@ -39,16 +40,13 @@ 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>) :
|
||||||
RecyclerView.Adapter<QueueSongViewHolder>() {
|
DiffAdapter<Song, QueueSongViewHolder>(ListDiffer.Blocking(QueueSongViewHolder.DIFF_CALLBACK)) {
|
||||||
private var differ = SyncListDiffer(this, 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
|
||||||
// with an index value instead.
|
// with an index value instead.
|
||||||
private var currentIndex = 0
|
private var currentIndex = 0
|
||||||
private var isPlaying = false
|
private var isPlaying = false
|
||||||
|
|
||||||
override fun getItemCount() = differ.currentList.size
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
||||||
QueueSongViewHolder.from(parent)
|
QueueSongViewHolder.from(parent)
|
||||||
|
|
||||||
|
@ -61,31 +59,13 @@ class QueueAdapter(private val listener: EditableListListener<Song>) :
|
||||||
payload: List<Any>
|
payload: List<Any>
|
||||||
) {
|
) {
|
||||||
if (payload.isEmpty()) {
|
if (payload.isEmpty()) {
|
||||||
viewHolder.bind(differ.currentList[position], listener)
|
viewHolder.bind(getItem(position), listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
viewHolder.isFuture = position > currentIndex
|
viewHolder.isFuture = position > currentIndex
|
||||||
viewHolder.updatePlayingIndicator(position == currentIndex, isPlaying)
|
viewHolder.updatePlayingIndicator(position == currentIndex, isPlaying)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Synchronously update the list with new items. This is exceedingly slow for large diffs, so
|
|
||||||
* only use it for trivial updates.
|
|
||||||
* @param newList The new [Song]s for the adapter to display.
|
|
||||||
*/
|
|
||||||
fun submitList(newList: List<Song>) {
|
|
||||||
differ.submitList(newList)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Replace the list with a new list. This is exceedingly slow for large diffs, so only use it
|
|
||||||
* for trivial updates.
|
|
||||||
* @param newList The new [Song]s for the adapter to display.
|
|
||||||
*/
|
|
||||||
fun replaceList(newList: List<Song>) {
|
|
||||||
differ.replaceList(newList)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the position of the currently playing item in the queue. This will mark the item as
|
* Set the position of the currently playing item in the queue. This will mark the item as
|
||||||
* playing and any previous items as played.
|
* playing and any previous items as played.
|
||||||
|
|
|
@ -33,7 +33,6 @@ import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.ui.ViewBindingFragment
|
import org.oxycblt.auxio.ui.ViewBindingFragment
|
||||||
import org.oxycblt.auxio.util.androidActivityViewModels
|
import org.oxycblt.auxio.util.androidActivityViewModels
|
||||||
import org.oxycblt.auxio.util.collectImmediately
|
import org.oxycblt.auxio.util.collectImmediately
|
||||||
import org.oxycblt.auxio.util.logD
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [ViewBindingFragment] that displays an editable queue.
|
* A [ViewBindingFragment] that displays an editable queue.
|
||||||
|
@ -102,13 +101,7 @@ class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), EditableListL
|
||||||
|
|
||||||
// Replace or diff the queue depending on the type of change it is.
|
// Replace or diff the queue depending on the type of change it is.
|
||||||
val instructions = queueModel.instructions
|
val instructions = queueModel.instructions
|
||||||
if (instructions?.update == UpdateInstructions.REPLACE) {
|
queueAdapter.submitList(queue, instructions?.update ?: UpdateInstructions.DIFF)
|
||||||
logD("Replacing queue")
|
|
||||||
queueAdapter.replaceList(queue)
|
|
||||||
} else {
|
|
||||||
logD("Diffing queue")
|
|
||||||
queueAdapter.submitList(queue)
|
|
||||||
}
|
|
||||||
// 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)
|
||||||
|
|
||||||
|
|
|
@ -165,7 +165,7 @@ class PlaybackStateDatabase private constructor(context: Context) :
|
||||||
fun write(state: SavedState?) {
|
fun write(state: SavedState?) {
|
||||||
requireBackgroundThread()
|
requireBackgroundThread()
|
||||||
// Only bother saving a state if a song is actively playing from one.
|
// Only bother saving a state if a song is actively playing from one.
|
||||||
// This is not the case with a null state or a state with an out-of-bounds index.
|
// This is not the case with a null state.
|
||||||
if (state != null) {
|
if (state != null) {
|
||||||
// Transform saved state into raw state, which can then be written to the database.
|
// Transform saved state into raw state, which can then be written to the database.
|
||||||
val rawPlaybackState =
|
val rawPlaybackState =
|
||||||
|
|
|
@ -18,7 +18,6 @@
|
||||||
package org.oxycblt.auxio.search
|
package org.oxycblt.auxio.search
|
||||||
|
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.AsyncListDiffer
|
|
||||||
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.recycler.*
|
import org.oxycblt.auxio.list.recycler.*
|
||||||
|
@ -30,14 +29,11 @@ import org.oxycblt.auxio.music.*
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
class SearchAdapter(private val listener: SelectableListListener<Music>) :
|
class SearchAdapter(private val listener: SelectableListListener<Music>) :
|
||||||
SelectionIndicatorAdapter<RecyclerView.ViewHolder>(), AuxioRecyclerView.SpanSizeLookup {
|
SelectionIndicatorAdapter<Item, RecyclerView.ViewHolder>(ListDiffer.Async(DIFF_CALLBACK)),
|
||||||
private val differ = AsyncListDiffer(this, DIFF_CALLBACK)
|
AuxioRecyclerView.SpanSizeLookup {
|
||||||
|
|
||||||
override val currentList: List<Item>
|
|
||||||
get() = differ.currentList
|
|
||||||
|
|
||||||
override fun getItemViewType(position: Int) =
|
override fun getItemViewType(position: Int) =
|
||||||
when (differ.currentList[position]) {
|
when (getItem(position)) {
|
||||||
is Song -> SongViewHolder.VIEW_TYPE
|
is Song -> SongViewHolder.VIEW_TYPE
|
||||||
is Album -> AlbumViewHolder.VIEW_TYPE
|
is Album -> AlbumViewHolder.VIEW_TYPE
|
||||||
is Artist -> ArtistViewHolder.VIEW_TYPE
|
is Artist -> ArtistViewHolder.VIEW_TYPE
|
||||||
|
@ -57,7 +53,7 @@ class SearchAdapter(private val listener: SelectableListListener<Music>) :
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||||
when (val item = differ.currentList[position]) {
|
when (val item = getItem(position)) {
|
||||||
is Song -> (holder as SongViewHolder).bind(item, listener)
|
is Song -> (holder as SongViewHolder).bind(item, listener)
|
||||||
is Album -> (holder as AlbumViewHolder).bind(item, listener)
|
is Album -> (holder as AlbumViewHolder).bind(item, listener)
|
||||||
is Artist -> (holder as ArtistViewHolder).bind(item, listener)
|
is Artist -> (holder as ArtistViewHolder).bind(item, listener)
|
||||||
|
@ -66,17 +62,7 @@ class SearchAdapter(private val listener: SelectableListListener<Music>) :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isItemFullWidth(position: Int) = differ.currentList[position] is Header
|
override fun isItemFullWidth(position: Int) = getItem(position) is Header
|
||||||
|
|
||||||
/**
|
|
||||||
* Asynchronously update the list with new items. Assumes that the list only contains supported
|
|
||||||
* data..
|
|
||||||
* @param newList The new [Item]s for the adapter to display.
|
|
||||||
* @param callback A block called when the asynchronous update is completed.
|
|
||||||
*/
|
|
||||||
fun submitList(newList: List<Item>, callback: () -> Unit) {
|
|
||||||
differ.submitList(newList, callback)
|
|
||||||
}
|
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
/** A comparator that can be used with DiffUtil. */
|
/** A comparator that can be used with DiffUtil. */
|
||||||
|
|
|
@ -153,7 +153,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()) {
|
searchAdapter.diffList(results.toMutableList()) {
|
||||||
// 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.
|
||||||
|
@ -162,7 +162,7 @@ class SearchFragment : ListFragment<Music, FragmentSearchBinding>() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
|
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
|
||||||
searchAdapter.setPlayingItem(parent ?: song, isPlaying)
|
searchAdapter.setPlaying(parent ?: song, isPlaying)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleNavigation(item: Music?) {
|
private fun handleNavigation(item: Music?) {
|
||||||
|
@ -180,7 +180,7 @@ class SearchFragment : ListFragment<Music, FragmentSearchBinding>() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateSelection(selected: List<Music>) {
|
private fun updateSelection(selected: List<Music>) {
|
||||||
searchAdapter.setSelectedItems(selected)
|
searchAdapter.setSelected(selected.toSet())
|
||||||
if (requireBinding().searchSelectionToolbar.updateSelectionAmount(selected.size) &&
|
if (requireBinding().searchSelectionToolbar.updateSelectionAmount(selected.size) &&
|
||||||
selected.isNotEmpty()) {
|
selected.isNotEmpty()) {
|
||||||
// Make selection of obscured items easier by hiding the keyboard.
|
// Make selection of obscured items easier by hiding the keyboard.
|
||||||
|
|
|
@ -123,14 +123,15 @@ class AboutFragment : ViewBindingFragment<FragmentAboutBinding>() {
|
||||||
if (pkgName == "android") {
|
if (pkgName == "android") {
|
||||||
// No default browser [Must open app chooser, may not be supported]
|
// No default browser [Must open app chooser, may not be supported]
|
||||||
openAppChooser(browserIntent)
|
openAppChooser(browserIntent)
|
||||||
} else try {
|
} else
|
||||||
browserIntent.setPackage(pkgName)
|
try {
|
||||||
startActivity(browserIntent)
|
browserIntent.setPackage(pkgName)
|
||||||
} catch (e: ActivityNotFoundException) {
|
startActivity(browserIntent)
|
||||||
// Not a browser but an app chooser
|
} catch (e: ActivityNotFoundException) {
|
||||||
browserIntent.setPackage(null)
|
// Not a browser but an app chooser
|
||||||
openAppChooser(browserIntent)
|
browserIntent.setPackage(null)
|
||||||
}
|
openAppChooser(browserIntent)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// No app installed to open the link
|
// No app installed to open the link
|
||||||
context.showToast(R.string.err_no_app)
|
context.showToast(R.string.err_no_app)
|
||||||
|
|
|
@ -75,10 +75,8 @@ interface UISettings : Settings<UISettings.Listener> {
|
||||||
var accent = sharedPreferences.getInt(OLD_KEY_ACCENT3, 5)
|
var accent = sharedPreferences.getInt(OLD_KEY_ACCENT3, 5)
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
// Accents were previously frozen as soon as the OS was updated to android
|
// Accents were previously frozen as soon as the OS was updated to android
|
||||||
// twelve,
|
// twelve, as dynamic colors were enabled by default. This is no longer the
|
||||||
// as dynamic colors were enabled by default. This is no longer the case, so we
|
// case, so we need to re-update the setting to dynamic colors here.
|
||||||
// need
|
|
||||||
// to re-update the setting to dynamic colors here.
|
|
||||||
accent = 16
|
accent = 16
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,7 +94,7 @@ interface UISettings : Settings<UISettings.Listener> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
private companion object {
|
||||||
const val OLD_KEY_ACCENT3 = "auxio_accent"
|
const val OLD_KEY_ACCENT3 = "auxio_accent"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,7 +39,8 @@ fun <T> unlikelyToBeNull(value: T?) =
|
||||||
* @return A data casted to [T].
|
* @return A data casted to [T].
|
||||||
* @throws IllegalStateException If the data cannot be casted to [T].
|
* @throws IllegalStateException If the data cannot be casted to [T].
|
||||||
*/
|
*/
|
||||||
inline fun <reified T> requireIs(data: Any): T {
|
inline fun <reified T> requireIs(data: Any?): T {
|
||||||
|
requireNotNull(data) { "Unexpected datatype: null" }
|
||||||
check(data is T) { "Unexpected datatype: ${data::class.simpleName}" }
|
check(data is T) { "Unexpected datatype: ${data::class.simpleName}" }
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,8 @@
|
||||||
android:background="?attr/colorSurface"
|
android:background="?attr/colorSurface"
|
||||||
android:transitionGroup="true">
|
android:transitionGroup="true">
|
||||||
|
|
||||||
|
<!-- TODO: Try to align the queue bar with the playback bar. -->
|
||||||
|
|
||||||
<androidx.fragment.app.FragmentContainerView
|
<androidx.fragment.app.FragmentContainerView
|
||||||
android:id="@+id/explore_nav_host"
|
android:id="@+id/explore_nav_host"
|
||||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||||
|
|
Loading…
Reference in a new issue