list: re-add fine-grained updates
Re-add the fine-grained updates that were removed prior due to state concerns. This time they should be safe enough to use, as the differ has been fully vendored. The change is not complete enough however, as the queue view has not been properly ported to use these updates just yet.
This commit is contained in:
parent
6226b0a830
commit
86ca6d577e
31 changed files with 585 additions and 539 deletions
|
@ -33,7 +33,6 @@ 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.Sort
|
import org.oxycblt.auxio.list.Sort
|
||||||
import org.oxycblt.auxio.list.adapter.BasicListInstructions
|
|
||||||
import org.oxycblt.auxio.list.selection.SelectionViewModel
|
import org.oxycblt.auxio.list.selection.SelectionViewModel
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
|
@ -104,6 +103,9 @@ class AlbumDetailFragment :
|
||||||
super.onDestroyBinding(binding)
|
super.onDestroyBinding(binding)
|
||||||
binding.detailToolbar.setOnMenuItemClickListener(null)
|
binding.detailToolbar.setOnMenuItemClickListener(null)
|
||||||
binding.detailRecycler.adapter = null
|
binding.detailRecycler.adapter = null
|
||||||
|
// Avoid possible race conditions that could cause a bad replace instruction to be consumed
|
||||||
|
// during list initialization and crash the app. Could happen if the user is fast enough.
|
||||||
|
detailModel.albumInstructions.consume()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMenuItemClick(item: MenuItem): Boolean {
|
override fun onMenuItemClick(item: MenuItem): Boolean {
|
||||||
|
@ -273,8 +275,8 @@ class AlbumDetailFragment :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateList(items: List<Item>) {
|
private fun updateList(list: List<Item>) {
|
||||||
detailAdapter.submitList(items, BasicListInstructions.DIFF)
|
detailAdapter.update(list, detailModel.albumInstructions.consume())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateSelection(selected: List<Music>) {
|
private fun updateSelection(selected: List<Music>) {
|
||||||
|
|
|
@ -33,7 +33,6 @@ 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.Sort
|
import org.oxycblt.auxio.list.Sort
|
||||||
import org.oxycblt.auxio.list.adapter.BasicListInstructions
|
|
||||||
import org.oxycblt.auxio.list.selection.SelectionViewModel
|
import org.oxycblt.auxio.list.selection.SelectionViewModel
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
|
@ -107,6 +106,9 @@ class ArtistDetailFragment :
|
||||||
super.onDestroyBinding(binding)
|
super.onDestroyBinding(binding)
|
||||||
binding.detailToolbar.setOnMenuItemClickListener(null)
|
binding.detailToolbar.setOnMenuItemClickListener(null)
|
||||||
binding.detailRecycler.adapter = null
|
binding.detailRecycler.adapter = null
|
||||||
|
// Avoid possible race conditions that could cause a bad replace instruction to be consumed
|
||||||
|
// during list initialization and crash the app. Could happen if the user is fast enough.
|
||||||
|
detailModel.artistInstructions.consume()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMenuItemClick(item: MenuItem): Boolean {
|
override fun onMenuItemClick(item: MenuItem): Boolean {
|
||||||
|
@ -249,8 +251,8 @@ class ArtistDetailFragment :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateList(items: List<Item>) {
|
private fun updateList(list: List<Item>) {
|
||||||
detailAdapter.submitList(items, BasicListInstructions.DIFF)
|
detailAdapter.update(list, detailModel.artistInstructions.consume())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateSelection(selected: List<Music>) {
|
private fun updateSelection(selected: List<Music>) {
|
||||||
|
|
|
@ -33,6 +33,7 @@ import org.oxycblt.auxio.detail.recycler.SortHeader
|
||||||
import org.oxycblt.auxio.list.BasicHeader
|
import org.oxycblt.auxio.list.BasicHeader
|
||||||
import org.oxycblt.auxio.list.Item
|
import org.oxycblt.auxio.list.Item
|
||||||
import org.oxycblt.auxio.list.Sort
|
import org.oxycblt.auxio.list.Sort
|
||||||
|
import org.oxycblt.auxio.list.adapter.UpdateInstructions
|
||||||
import org.oxycblt.auxio.music.*
|
import org.oxycblt.auxio.music.*
|
||||||
import org.oxycblt.auxio.music.metadata.AudioInfo
|
import org.oxycblt.auxio.music.metadata.AudioInfo
|
||||||
import org.oxycblt.auxio.music.metadata.Disc
|
import org.oxycblt.auxio.music.metadata.Disc
|
||||||
|
@ -80,6 +81,10 @@ constructor(
|
||||||
/** 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
|
||||||
|
private val _albumInstructions = MutableEvent<UpdateInstructions>()
|
||||||
|
/** Instructions for updating [albumList] in the UI. */
|
||||||
|
val albumInstructions: Event<UpdateInstructions>
|
||||||
|
get() = _albumInstructions
|
||||||
|
|
||||||
/** The current [Sort] used for [Song]s in [albumList]. */
|
/** The current [Sort] used for [Song]s in [albumList]. */
|
||||||
var albumSongSort: Sort
|
var albumSongSort: Sort
|
||||||
|
@ -87,7 +92,7 @@ constructor(
|
||||||
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.
|
||||||
currentAlbum.value?.let(::refreshAlbumList)
|
currentAlbum.value?.let { refreshAlbumList(it, true) }
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- ARTIST ---
|
// --- ARTIST ---
|
||||||
|
@ -100,6 +105,10 @@ constructor(
|
||||||
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
|
||||||
|
private val _artistInstructions = MutableEvent<UpdateInstructions>()
|
||||||
|
/** Instructions for updating [artistList] in the UI. */
|
||||||
|
val artistInstructions: Event<UpdateInstructions>
|
||||||
|
get() = _artistInstructions
|
||||||
|
|
||||||
/** The current [Sort] used for [Song]s in [artistList]. */
|
/** The current [Sort] used for [Song]s in [artistList]. */
|
||||||
var artistSongSort: Sort
|
var artistSongSort: Sort
|
||||||
|
@ -107,7 +116,7 @@ constructor(
|
||||||
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.
|
||||||
currentArtist.value?.let(::refreshArtistList)
|
currentArtist.value?.let { refreshArtistList(it, true) }
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- GENRE ---
|
// --- GENRE ---
|
||||||
|
@ -120,6 +129,10 @@ constructor(
|
||||||
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
|
||||||
|
private val _genreInstructions = MutableEvent<UpdateInstructions>()
|
||||||
|
/** Instructions for updating [artistList] in the UI. */
|
||||||
|
val genreInstructions: Event<UpdateInstructions>
|
||||||
|
get() = _genreInstructions
|
||||||
|
|
||||||
/** The current [Sort] used for [Song]s in [genreList]. */
|
/** The current [Sort] used for [Song]s in [genreList]. */
|
||||||
var genreSongSort: Sort
|
var genreSongSort: Sort
|
||||||
|
@ -127,7 +140,7 @@ constructor(
|
||||||
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.
|
||||||
currentGenre.value?.let(::refreshGenreList)
|
currentGenre.value?.let { refreshGenreList(it, true) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -242,11 +255,6 @@ constructor(
|
||||||
|
|
||||||
private fun <T : Music> requireMusic(uid: Music.UID) = musicRepository.library?.find<T>(uid)
|
private fun <T : Music> requireMusic(uid: Music.UID) = musicRepository.library?.find<T>(uid)
|
||||||
|
|
||||||
/**
|
|
||||||
* Start a new job to load a given [Song]'s [AudioInfo]. Result is pushed to [songAudioInfo].
|
|
||||||
*
|
|
||||||
* @param song The song to load.
|
|
||||||
*/
|
|
||||||
private fun refreshAudioInfo(song: Song) {
|
private fun refreshAudioInfo(song: Song) {
|
||||||
// Clear any previous job in order to avoid stale data from appearing in the UI.
|
// Clear any previous job in order to avoid stale data from appearing in the UI.
|
||||||
currentSongJob?.cancel()
|
currentSongJob?.cancel()
|
||||||
|
@ -259,10 +267,17 @@ constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshAlbumList(album: Album) {
|
private fun refreshAlbumList(album: Album, replace: Boolean = false) {
|
||||||
logD("Refreshing album data")
|
logD("Refreshing album data")
|
||||||
val data = mutableListOf<Item>(album)
|
val list = mutableListOf<Item>(album)
|
||||||
data.add(SortHeader(R.string.lbl_songs))
|
list.add(SortHeader(R.string.lbl_songs))
|
||||||
|
val instructions =
|
||||||
|
if (replace) {
|
||||||
|
// Intentional so that the header item isn't replaced with the songs
|
||||||
|
UpdateInstructions.Replace(list.size)
|
||||||
|
} else {
|
||||||
|
UpdateInstructions.Diff
|
||||||
|
}
|
||||||
|
|
||||||
// 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.
|
||||||
|
@ -272,20 +287,21 @@ constructor(
|
||||||
if (byDisc.size > 1) {
|
if (byDisc.size > 1) {
|
||||||
logD("Album has more than one disc, interspersing headers")
|
logD("Album has more than one disc, interspersing headers")
|
||||||
for (entry in byDisc.entries) {
|
for (entry in byDisc.entries) {
|
||||||
data.add(entry.key)
|
list.add(entry.key)
|
||||||
data.addAll(entry.value)
|
list.addAll(entry.value)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 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)
|
list.addAll(songs)
|
||||||
}
|
}
|
||||||
|
|
||||||
_albumList.value = data
|
_albumInstructions.put(instructions)
|
||||||
|
_albumList.value = list
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshArtistList(artist: Artist) {
|
private fun refreshArtistList(artist: Artist, replace: Boolean = false) {
|
||||||
logD("Refreshing artist data")
|
logD("Refreshing artist data")
|
||||||
val data = mutableListOf<Item>(artist)
|
val list = mutableListOf<Item>(artist)
|
||||||
val albums = Sort(Sort.Mode.ByDate, Sort.Direction.DESCENDING).albums(artist.albums)
|
val albums = Sort(Sort.Mode.ByDate, Sort.Direction.DESCENDING).albums(artist.albums)
|
||||||
|
|
||||||
val byReleaseGroup =
|
val byReleaseGroup =
|
||||||
|
@ -312,29 +328,43 @@ constructor(
|
||||||
logD("Release groups for this artist: ${byReleaseGroup.keys}")
|
logD("Release groups for this artist: ${byReleaseGroup.keys}")
|
||||||
|
|
||||||
for (entry in byReleaseGroup.entries.sortedBy { it.key }) {
|
for (entry in byReleaseGroup.entries.sortedBy { it.key }) {
|
||||||
data.add(BasicHeader(entry.key.headerTitleRes))
|
list.add(BasicHeader(entry.key.headerTitleRes))
|
||||||
data.addAll(entry.value)
|
list.addAll(entry.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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.
|
||||||
|
var instructions: UpdateInstructions = UpdateInstructions.Diff
|
||||||
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))
|
list.add(SortHeader(R.string.lbl_songs))
|
||||||
data.addAll(artistSongSort.songs(artist.songs))
|
if (replace) {
|
||||||
|
// Intentional so that the header item isn't replaced with the songs
|
||||||
|
instructions = UpdateInstructions.Replace(list.size)
|
||||||
|
}
|
||||||
|
list.addAll(artistSongSort.songs(artist.songs))
|
||||||
}
|
}
|
||||||
|
|
||||||
_artistList.value = data.toList()
|
_artistInstructions.put(instructions)
|
||||||
|
_artistList.value = list.toList()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshGenreList(genre: Genre) {
|
private fun refreshGenreList(genre: Genre, replace: Boolean = false) {
|
||||||
logD("Refreshing genre data")
|
logD("Refreshing genre data")
|
||||||
val data = mutableListOf<Item>(genre)
|
val list = mutableListOf<Item>(genre)
|
||||||
// Genre is guaranteed to always have artists and songs.
|
// Genre is guaranteed to always have artists and songs.
|
||||||
data.add(BasicHeader(R.string.lbl_artists))
|
list.add(BasicHeader(R.string.lbl_artists))
|
||||||
data.addAll(genre.artists)
|
list.addAll(genre.artists)
|
||||||
data.add(SortHeader(R.string.lbl_songs))
|
list.add(SortHeader(R.string.lbl_songs))
|
||||||
data.addAll(genreSongSort.songs(genre.songs))
|
val instructions =
|
||||||
_genreList.value = data
|
if (replace) {
|
||||||
|
// Intentional so that the header item isn't replaced with the songs
|
||||||
|
UpdateInstructions.Replace(list.size)
|
||||||
|
} else {
|
||||||
|
UpdateInstructions.Diff
|
||||||
|
}
|
||||||
|
list.addAll(genreSongSort.songs(genre.songs))
|
||||||
|
_genreInstructions.put(instructions)
|
||||||
|
_genreList.value = list
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -33,7 +33,6 @@ 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.Sort
|
import org.oxycblt.auxio.list.Sort
|
||||||
import org.oxycblt.auxio.list.adapter.BasicListInstructions
|
|
||||||
import org.oxycblt.auxio.list.selection.SelectionViewModel
|
import org.oxycblt.auxio.list.selection.SelectionViewModel
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
|
@ -106,6 +105,9 @@ class GenreDetailFragment :
|
||||||
super.onDestroyBinding(binding)
|
super.onDestroyBinding(binding)
|
||||||
binding.detailToolbar.setOnMenuItemClickListener(null)
|
binding.detailToolbar.setOnMenuItemClickListener(null)
|
||||||
binding.detailRecycler.adapter = null
|
binding.detailRecycler.adapter = null
|
||||||
|
// Avoid possible race conditions that could cause a bad replace instruction to be consumed
|
||||||
|
// during list initialization and crash the app. Could happen if the user is fast enough.
|
||||||
|
detailModel.genreInstructions.consume()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMenuItemClick(item: MenuItem): Boolean {
|
override fun onMenuItemClick(item: MenuItem): Boolean {
|
||||||
|
@ -232,8 +234,8 @@ class GenreDetailFragment :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateList(items: List<Item>) {
|
private fun updateList(list: List<Item>) {
|
||||||
detailAdapter.submitList(items, BasicListInstructions.DIFF)
|
detailAdapter.update(list, detailModel.genreInstructions.consume())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateSelection(selected: List<Music>) {
|
private fun updateSelection(selected: List<Music>) {
|
||||||
|
|
|
@ -30,7 +30,7 @@ import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.DialogSongDetailBinding
|
import org.oxycblt.auxio.databinding.DialogSongDetailBinding
|
||||||
import org.oxycblt.auxio.detail.recycler.SongProperty
|
import org.oxycblt.auxio.detail.recycler.SongProperty
|
||||||
import org.oxycblt.auxio.detail.recycler.SongPropertyAdapter
|
import org.oxycblt.auxio.detail.recycler.SongPropertyAdapter
|
||||||
import org.oxycblt.auxio.list.adapter.BasicListInstructions
|
import org.oxycblt.auxio.list.adapter.UpdateInstructions
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.music.metadata.AudioInfo
|
import org.oxycblt.auxio.music.metadata.AudioInfo
|
||||||
|
@ -79,7 +79,7 @@ class SongDetailDialog : ViewBindingDialogFragment<DialogSongDetailBinding>() {
|
||||||
|
|
||||||
if (info != null) {
|
if (info != null) {
|
||||||
val context = requireContext()
|
val context = requireContext()
|
||||||
detailAdapter.submitList(
|
detailAdapter.update(
|
||||||
buildList {
|
buildList {
|
||||||
add(SongProperty(R.string.lbl_name, song.zipName(context)))
|
add(SongProperty(R.string.lbl_name, song.zipName(context)))
|
||||||
add(SongProperty(R.string.lbl_album, song.album.zipName(context)))
|
add(SongProperty(R.string.lbl_album, song.album.zipName(context)))
|
||||||
|
@ -120,7 +120,7 @@ class SongDetailDialog : ViewBindingDialogFragment<DialogSongDetailBinding>() {
|
||||||
R.string.lbl_sample_rate, getString(R.string.fmt_sample_rate, it)))
|
R.string.lbl_sample_rate, getString(R.string.fmt_sample_rate, it)))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
BasicListInstructions.REPLACE)
|
UpdateInstructions.Replace(0))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -39,16 +39,14 @@ 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 diffCallback A [DiffUtil.ItemCallback] to use for item comparison when diffing the
|
* @param diffCallback A [DiffUtil.ItemCallback] to compare list updates with.
|
||||||
* internal list.
|
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
abstract class DetailAdapter(
|
abstract class DetailAdapter(
|
||||||
private val listener: Listener<*>,
|
private val listener: Listener<*>,
|
||||||
diffCallback: DiffUtil.ItemCallback<Item>
|
private val diffCallback: DiffUtil.ItemCallback<Item>
|
||||||
) :
|
) :
|
||||||
SelectionIndicatorAdapter<Item, BasicListInstructions, RecyclerView.ViewHolder>(
|
SelectionIndicatorAdapter<Item, RecyclerView.ViewHolder>(diffCallback),
|
||||||
ListDiffer.Async(diffCallback)),
|
|
||||||
AuxioRecyclerView.SpanSizeLookup {
|
AuxioRecyclerView.SpanSizeLookup {
|
||||||
|
|
||||||
override fun getItemViewType(position: Int) =
|
override fun getItemViewType(position: Int) =
|
||||||
|
|
|
@ -23,10 +23,7 @@ import androidx.annotation.StringRes
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import org.oxycblt.auxio.databinding.ItemSongPropertyBinding
|
import org.oxycblt.auxio.databinding.ItemSongPropertyBinding
|
||||||
import org.oxycblt.auxio.list.Item
|
import org.oxycblt.auxio.list.Item
|
||||||
import org.oxycblt.auxio.list.adapter.BasicListInstructions
|
import org.oxycblt.auxio.list.adapter.*
|
||||||
import org.oxycblt.auxio.list.adapter.DiffAdapter
|
|
||||||
import org.oxycblt.auxio.list.adapter.ListDiffer
|
|
||||||
import org.oxycblt.auxio.list.adapter.SimpleDiffCallback
|
|
||||||
import org.oxycblt.auxio.list.recycler.DialogRecyclerView
|
import org.oxycblt.auxio.list.recycler.DialogRecyclerView
|
||||||
import org.oxycblt.auxio.util.context
|
import org.oxycblt.auxio.util.context
|
||||||
import org.oxycblt.auxio.util.inflater
|
import org.oxycblt.auxio.util.inflater
|
||||||
|
@ -37,8 +34,8 @@ import org.oxycblt.auxio.util.inflater
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
class SongPropertyAdapter :
|
class SongPropertyAdapter :
|
||||||
DiffAdapter<SongProperty, BasicListInstructions, SongPropertyViewHolder>(
|
FlexibleListAdapter<SongProperty, SongPropertyViewHolder>(
|
||||||
ListDiffer.Blocking(SongPropertyViewHolder.DIFF_CALLBACK)) {
|
SongPropertyViewHolder.DIFF_CALLBACK) {
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
||||||
SongPropertyViewHolder.from(parent)
|
SongPropertyViewHolder.from(parent)
|
||||||
|
|
||||||
|
|
|
@ -154,7 +154,7 @@ class HomeFragment :
|
||||||
binding.homeFab.setOnClickListener { playbackModel.shuffleAll() }
|
binding.homeFab.setOnClickListener { playbackModel.shuffleAll() }
|
||||||
|
|
||||||
// --- VIEWMODEL SETUP ---
|
// --- VIEWMODEL SETUP ---
|
||||||
collect(homeModel.shouldRecreate, ::handleRecreate)
|
collect(homeModel.shouldRecreate.flow, ::handleRecreate)
|
||||||
collectImmediately(homeModel.currentTabMode, ::updateCurrentTab)
|
collectImmediately(homeModel.currentTabMode, ::updateCurrentTab)
|
||||||
collectImmediately(homeModel.songsList, homeModel.isFastScrolling, ::updateFab)
|
collectImmediately(homeModel.songsList, homeModel.isFastScrolling, ::updateFab)
|
||||||
collectImmediately(musicModel.indexerState, ::updateIndexerState)
|
collectImmediately(musicModel.indexerState, ::updateIndexerState)
|
||||||
|
@ -329,18 +329,14 @@ class HomeFragment :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleRecreate(recreate: Boolean) {
|
private fun handleRecreate(recreate: Unit?) {
|
||||||
if (!recreate) {
|
if (recreate == null) return
|
||||||
// Nothing to do
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val binding = requireBinding()
|
val binding = requireBinding()
|
||||||
// Move back to position zero, as there must be a tab there.
|
// Move back to position zero, as there must be a tab there.
|
||||||
binding.homePager.currentItem = 0
|
binding.homePager.currentItem = 0
|
||||||
// Make sure tabs are set up to also follow the new ViewPager configuration.
|
// Make sure tabs are set up to also follow the new ViewPager configuration.
|
||||||
setupPager(binding)
|
setupPager(binding)
|
||||||
homeModel.finishRecreate()
|
homeModel.shouldRecreate.consume()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateIndexerState(state: Indexer.State?) {
|
private fun updateIndexerState(state: Indexer.State?) {
|
||||||
|
|
|
@ -24,9 +24,12 @@ 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.Sort
|
import org.oxycblt.auxio.list.Sort
|
||||||
|
import org.oxycblt.auxio.list.adapter.UpdateInstructions
|
||||||
import org.oxycblt.auxio.music.*
|
import org.oxycblt.auxio.music.*
|
||||||
import org.oxycblt.auxio.music.model.Library
|
import org.oxycblt.auxio.music.model.Library
|
||||||
import org.oxycblt.auxio.playback.PlaybackSettings
|
import org.oxycblt.auxio.playback.PlaybackSettings
|
||||||
|
import org.oxycblt.auxio.util.Event
|
||||||
|
import org.oxycblt.auxio.util.MutableEvent
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -48,11 +51,19 @@ constructor(
|
||||||
/** A list of [Song]s, sorted by the preferred [Sort], to be shown in the home view. */
|
/** A list of [Song]s, sorted by the preferred [Sort], to be shown in the home view. */
|
||||||
val songsList: StateFlow<List<Song>>
|
val songsList: StateFlow<List<Song>>
|
||||||
get() = _songsList
|
get() = _songsList
|
||||||
|
private val _songsInstructions = MutableEvent<UpdateInstructions>()
|
||||||
|
/** Instructions for how to update [songsList] in the UI. */
|
||||||
|
val songsInstructions: Event<UpdateInstructions>
|
||||||
|
get() = _songsInstructions
|
||||||
|
|
||||||
private val _albumsLists = MutableStateFlow(listOf<Album>())
|
private val _albumsLists = MutableStateFlow(listOf<Album>())
|
||||||
/** A list of [Album]s, sorted by the preferred [Sort], to be shown in the home view. */
|
/** A list of [Album]s, sorted by the preferred [Sort], to be shown in the home view. */
|
||||||
val albumsList: StateFlow<List<Album>>
|
val albumsList: StateFlow<List<Album>>
|
||||||
get() = _albumsLists
|
get() = _albumsLists
|
||||||
|
private val _albumsInstructions = MutableEvent<UpdateInstructions>()
|
||||||
|
/** Instructions for how to update [albumsList] in the UI. */
|
||||||
|
val albumsInstructions: Event<UpdateInstructions>
|
||||||
|
get() = _albumsInstructions
|
||||||
|
|
||||||
private val _artistsList = MutableStateFlow(listOf<Artist>())
|
private val _artistsList = MutableStateFlow(listOf<Artist>())
|
||||||
/**
|
/**
|
||||||
|
@ -62,11 +73,19 @@ constructor(
|
||||||
*/
|
*/
|
||||||
val artistsList: MutableStateFlow<List<Artist>>
|
val artistsList: MutableStateFlow<List<Artist>>
|
||||||
get() = _artistsList
|
get() = _artistsList
|
||||||
|
private val _artistsInstructions = MutableEvent<UpdateInstructions>()
|
||||||
|
/** Instructions for how to update [artistsList] in the UI. */
|
||||||
|
val artistsInstructions: Event<UpdateInstructions>
|
||||||
|
get() = _artistsInstructions
|
||||||
|
|
||||||
private val _genresList = MutableStateFlow(listOf<Genre>())
|
private val _genresList = MutableStateFlow(listOf<Genre>())
|
||||||
/** A list of [Genre]s, sorted by the preferred [Sort], to be shown in the home view. */
|
/** A list of [Genre]s, sorted by the preferred [Sort], to be shown in the home view. */
|
||||||
val genresList: StateFlow<List<Genre>>
|
val genresList: StateFlow<List<Genre>>
|
||||||
get() = _genresList
|
get() = _genresList
|
||||||
|
private val _genresInstructions = MutableEvent<UpdateInstructions>()
|
||||||
|
/** Instructions for how to update [genresList] in the UI. */
|
||||||
|
val genresInstructions: Event<UpdateInstructions>
|
||||||
|
get() = _genresInstructions
|
||||||
|
|
||||||
/** The [MusicMode] to use when playing a [Song] from the UI. */
|
/** The [MusicMode] to use when playing a [Song] from the UI. */
|
||||||
val playbackMode: MusicMode
|
val playbackMode: MusicMode
|
||||||
|
@ -83,13 +102,14 @@ constructor(
|
||||||
/** The [MusicMode] of the currently shown [Tab]. */
|
/** The [MusicMode] of the currently shown [Tab]. */
|
||||||
val currentTabMode: StateFlow<MusicMode> = _currentTabMode
|
val currentTabMode: StateFlow<MusicMode> = _currentTabMode
|
||||||
|
|
||||||
private val _shouldRecreate = MutableStateFlow(false)
|
private val _shouldRecreate = MutableEvent<Unit>()
|
||||||
/**
|
/**
|
||||||
* A marker to re-create all library tabs, usually initiated by a settings change. When this
|
* A marker to re-create all library tabs, usually initiated by a settings change. When this
|
||||||
* flag is true, all tabs (and their respective ViewPager2 fragments) will be re-created from
|
* flag is true, all tabs (and their respective ViewPager2 fragments) will be re-created from
|
||||||
* scratch.
|
* scratch.
|
||||||
*/
|
*/
|
||||||
val shouldRecreate: StateFlow<Boolean> = _shouldRecreate
|
val shouldRecreate: Event<Unit>
|
||||||
|
get() = _shouldRecreate
|
||||||
|
|
||||||
private val _isFastScrolling = MutableStateFlow(false)
|
private val _isFastScrolling = MutableStateFlow(false)
|
||||||
/** A marker for whether the user is fast-scrolling in the home view or not. */
|
/** A marker for whether the user is fast-scrolling in the home view or not. */
|
||||||
|
@ -111,8 +131,11 @@ constructor(
|
||||||
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.
|
||||||
|
_songsInstructions.put(UpdateInstructions.Diff)
|
||||||
_songsList.value = musicSettings.songSort.songs(library.songs)
|
_songsList.value = musicSettings.songSort.songs(library.songs)
|
||||||
|
_albumsInstructions.put(UpdateInstructions.Diff)
|
||||||
_albumsLists.value = musicSettings.albumSort.albums(library.albums)
|
_albumsLists.value = musicSettings.albumSort.albums(library.albums)
|
||||||
|
_artistsInstructions.put(UpdateInstructions.Diff)
|
||||||
_artistsList.value =
|
_artistsList.value =
|
||||||
musicSettings.artistSort.artists(
|
musicSettings.artistSort.artists(
|
||||||
if (homeSettings.shouldHideCollaborators) {
|
if (homeSettings.shouldHideCollaborators) {
|
||||||
|
@ -121,6 +144,7 @@ constructor(
|
||||||
} else {
|
} else {
|
||||||
library.artists
|
library.artists
|
||||||
})
|
})
|
||||||
|
_genresInstructions.put(UpdateInstructions.Diff)
|
||||||
_genresList.value = musicSettings.genreSort.genres(library.genres)
|
_genresList.value = musicSettings.genreSort.genres(library.genres)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -128,7 +152,7 @@ constructor(
|
||||||
override fun onTabsChanged() {
|
override fun onTabsChanged() {
|
||||||
// Tabs changed, update the current tabs and set up a re-create event.
|
// Tabs changed, update the current tabs and set up a re-create event.
|
||||||
currentTabModes = makeTabModes()
|
currentTabModes = makeTabModes()
|
||||||
_shouldRecreate.value = true
|
_shouldRecreate.put(Unit)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onHideCollaboratorsChanged() {
|
override fun onHideCollaboratorsChanged() {
|
||||||
|
@ -162,18 +186,22 @@ constructor(
|
||||||
when (_currentTabMode.value) {
|
when (_currentTabMode.value) {
|
||||||
MusicMode.SONGS -> {
|
MusicMode.SONGS -> {
|
||||||
musicSettings.songSort = sort
|
musicSettings.songSort = sort
|
||||||
|
_songsInstructions.put(UpdateInstructions.Replace(0))
|
||||||
_songsList.value = sort.songs(_songsList.value)
|
_songsList.value = sort.songs(_songsList.value)
|
||||||
}
|
}
|
||||||
MusicMode.ALBUMS -> {
|
MusicMode.ALBUMS -> {
|
||||||
musicSettings.albumSort = sort
|
musicSettings.albumSort = sort
|
||||||
|
_albumsInstructions.put(UpdateInstructions.Replace(0))
|
||||||
_albumsLists.value = sort.albums(_albumsLists.value)
|
_albumsLists.value = sort.albums(_albumsLists.value)
|
||||||
}
|
}
|
||||||
MusicMode.ARTISTS -> {
|
MusicMode.ARTISTS -> {
|
||||||
musicSettings.artistSort = sort
|
musicSettings.artistSort = sort
|
||||||
|
_artistsInstructions.put(UpdateInstructions.Replace(0))
|
||||||
_artistsList.value = sort.artists(_artistsList.value)
|
_artistsList.value = sort.artists(_artistsList.value)
|
||||||
}
|
}
|
||||||
MusicMode.GENRES -> {
|
MusicMode.GENRES -> {
|
||||||
musicSettings.genreSort = sort
|
musicSettings.genreSort = sort
|
||||||
|
_genresInstructions.put(UpdateInstructions.Replace(0))
|
||||||
_genresList.value = sort.genres(_genresList.value)
|
_genresList.value = sort.genres(_genresList.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -189,15 +217,6 @@ constructor(
|
||||||
_currentTabMode.value = currentTabModes[pagerPos]
|
_currentTabMode.value = currentTabModes[pagerPos]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Mark the recreation process as complete.
|
|
||||||
*
|
|
||||||
* @see shouldRecreate
|
|
||||||
*/
|
|
||||||
fun finishRecreate() {
|
|
||||||
_shouldRecreate.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update whether the user is fast scrolling or not in the home view.
|
* Update whether the user is fast scrolling or not in the home view.
|
||||||
*
|
*
|
||||||
|
|
|
@ -32,8 +32,6 @@ 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.Sort
|
import org.oxycblt.auxio.list.Sort
|
||||||
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.SelectionIndicatorAdapter
|
||||||
import org.oxycblt.auxio.list.recycler.AlbumViewHolder
|
import org.oxycblt.auxio.list.recycler.AlbumViewHolder
|
||||||
import org.oxycblt.auxio.list.selection.SelectionViewModel
|
import org.oxycblt.auxio.list.selection.SelectionViewModel
|
||||||
|
@ -76,7 +74,7 @@ class AlbumListFragment :
|
||||||
listener = this@AlbumListFragment
|
listener = this@AlbumListFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
collectImmediately(homeModel.albumsList, ::updateList)
|
collectImmediately(homeModel.albumsList, ::updateAlbums)
|
||||||
collectImmediately(selectionModel.selected, ::updateSelection)
|
collectImmediately(selectionModel.selected, ::updateSelection)
|
||||||
collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
||||||
}
|
}
|
||||||
|
@ -140,8 +138,8 @@ class AlbumListFragment :
|
||||||
openMusicMenu(anchor, R.menu.menu_album_actions, item)
|
openMusicMenu(anchor, R.menu.menu_album_actions, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateList(albums: List<Album>) {
|
private fun updateAlbums(albums: List<Album>) {
|
||||||
albumAdapter.submitList(albums, BasicListInstructions.REPLACE)
|
albumAdapter.update(albums, homeModel.albumsInstructions.consume())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateSelection(selection: List<Music>) {
|
private fun updateSelection(selection: List<Music>) {
|
||||||
|
@ -159,8 +157,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, BasicListInstructions, AlbumViewHolder>(
|
SelectionIndicatorAdapter<Album, AlbumViewHolder>(AlbumViewHolder.DIFF_CALLBACK) {
|
||||||
ListDiffer.Blocking(AlbumViewHolder.DIFF_CALLBACK)) {
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
||||||
AlbumViewHolder.from(parent)
|
AlbumViewHolder.from(parent)
|
||||||
|
|
|
@ -30,8 +30,6 @@ 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.Sort
|
import org.oxycblt.auxio.list.Sort
|
||||||
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.SelectionIndicatorAdapter
|
||||||
import org.oxycblt.auxio.list.recycler.ArtistViewHolder
|
import org.oxycblt.auxio.list.recycler.ArtistViewHolder
|
||||||
import org.oxycblt.auxio.list.selection.SelectionViewModel
|
import org.oxycblt.auxio.list.selection.SelectionViewModel
|
||||||
|
@ -43,6 +41,7 @@ import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.playback.formatDurationMs
|
import org.oxycblt.auxio.playback.formatDurationMs
|
||||||
import org.oxycblt.auxio.ui.NavigationViewModel
|
import org.oxycblt.auxio.ui.NavigationViewModel
|
||||||
import org.oxycblt.auxio.util.collectImmediately
|
import org.oxycblt.auxio.util.collectImmediately
|
||||||
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.nonZeroOrNull
|
import org.oxycblt.auxio.util.nonZeroOrNull
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -74,7 +73,7 @@ class ArtistListFragment :
|
||||||
listener = this@ArtistListFragment
|
listener = this@ArtistListFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
collectImmediately(homeModel.artistsList, ::updateList)
|
collectImmediately(homeModel.artistsList, ::updateArtists)
|
||||||
collectImmediately(selectionModel.selected, ::updateSelection)
|
collectImmediately(selectionModel.selected, ::updateSelection)
|
||||||
collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
||||||
}
|
}
|
||||||
|
@ -118,8 +117,8 @@ class ArtistListFragment :
|
||||||
openMusicMenu(anchor, R.menu.menu_artist_actions, item)
|
openMusicMenu(anchor, R.menu.menu_artist_actions, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateList(artists: List<Artist>) {
|
private fun updateArtists(artists: List<Artist>) {
|
||||||
artistAdapter.submitList(artists, BasicListInstructions.REPLACE)
|
artistAdapter.update(artists, homeModel.artistsInstructions.consume().also { logD(it) })
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateSelection(selection: List<Music>) {
|
private fun updateSelection(selection: List<Music>) {
|
||||||
|
@ -137,8 +136,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, BasicListInstructions, ArtistViewHolder>(
|
SelectionIndicatorAdapter<Artist, ArtistViewHolder>(ArtistViewHolder.DIFF_CALLBACK) {
|
||||||
ListDiffer.Blocking(ArtistViewHolder.DIFF_CALLBACK)) {
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
||||||
ArtistViewHolder.from(parent)
|
ArtistViewHolder.from(parent)
|
||||||
|
|
|
@ -30,8 +30,6 @@ 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.Sort
|
import org.oxycblt.auxio.list.Sort
|
||||||
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.SelectionIndicatorAdapter
|
||||||
import org.oxycblt.auxio.list.recycler.GenreViewHolder
|
import org.oxycblt.auxio.list.recycler.GenreViewHolder
|
||||||
import org.oxycblt.auxio.list.selection.SelectionViewModel
|
import org.oxycblt.auxio.list.selection.SelectionViewModel
|
||||||
|
@ -43,6 +41,7 @@ import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.playback.formatDurationMs
|
import org.oxycblt.auxio.playback.formatDurationMs
|
||||||
import org.oxycblt.auxio.ui.NavigationViewModel
|
import org.oxycblt.auxio.ui.NavigationViewModel
|
||||||
import org.oxycblt.auxio.util.collectImmediately
|
import org.oxycblt.auxio.util.collectImmediately
|
||||||
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [ListFragment] that shows a list of [Genre]s.
|
* A [ListFragment] that shows a list of [Genre]s.
|
||||||
|
@ -73,7 +72,7 @@ class GenreListFragment :
|
||||||
listener = this@GenreListFragment
|
listener = this@GenreListFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
collectImmediately(homeModel.genresList, ::updateList)
|
collectImmediately(homeModel.genresList, ::updateGenres)
|
||||||
collectImmediately(selectionModel.selected, ::updateSelection)
|
collectImmediately(selectionModel.selected, ::updateSelection)
|
||||||
collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
||||||
}
|
}
|
||||||
|
@ -117,8 +116,8 @@ class GenreListFragment :
|
||||||
openMusicMenu(anchor, R.menu.menu_artist_actions, item)
|
openMusicMenu(anchor, R.menu.menu_artist_actions, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateList(artists: List<Genre>) {
|
private fun updateGenres(genres: List<Genre>) {
|
||||||
genreAdapter.submitList(artists, BasicListInstructions.REPLACE)
|
genreAdapter.update(genres, homeModel.genresInstructions.consume().also { logD(it) })
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateSelection(selection: List<Music>) {
|
private fun updateSelection(selection: List<Music>) {
|
||||||
|
@ -136,8 +135,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, BasicListInstructions, GenreViewHolder>(
|
SelectionIndicatorAdapter<Genre, GenreViewHolder>(GenreViewHolder.DIFF_CALLBACK) {
|
||||||
ListDiffer.Blocking(GenreViewHolder.DIFF_CALLBACK)) {
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
||||||
GenreViewHolder.from(parent)
|
GenreViewHolder.from(parent)
|
||||||
|
|
||||||
|
|
|
@ -32,8 +32,6 @@ 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.Sort
|
import org.oxycblt.auxio.list.Sort
|
||||||
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.SelectionIndicatorAdapter
|
||||||
import org.oxycblt.auxio.list.recycler.SongViewHolder
|
import org.oxycblt.auxio.list.recycler.SongViewHolder
|
||||||
import org.oxycblt.auxio.list.selection.SelectionViewModel
|
import org.oxycblt.auxio.list.selection.SelectionViewModel
|
||||||
|
@ -79,7 +77,7 @@ class SongListFragment :
|
||||||
listener = this@SongListFragment
|
listener = this@SongListFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
collectImmediately(homeModel.songsList, ::updateList)
|
collectImmediately(homeModel.songsList, ::updateSongs)
|
||||||
collectImmediately(selectionModel.selected, ::updateSelection)
|
collectImmediately(selectionModel.selected, ::updateSelection)
|
||||||
collectImmediately(
|
collectImmediately(
|
||||||
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
||||||
|
@ -147,8 +145,8 @@ class SongListFragment :
|
||||||
openMusicMenu(anchor, R.menu.menu_song_actions, item)
|
openMusicMenu(anchor, R.menu.menu_song_actions, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateList(songs: List<Song>) {
|
private fun updateSongs(songs: List<Song>) {
|
||||||
songAdapter.submitList(songs, BasicListInstructions.REPLACE)
|
songAdapter.update(songs, homeModel.songsInstructions.consume())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateSelection(selection: List<Music>) {
|
private fun updateSelection(selection: List<Music>) {
|
||||||
|
@ -170,8 +168,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, BasicListInstructions, SongViewHolder>(
|
SelectionIndicatorAdapter<Song, SongViewHolder>(SongViewHolder.DIFF_CALLBACK) {
|
||||||
ListDiffer.Blocking(SongViewHolder.DIFF_CALLBACK)) {
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
||||||
SongViewHolder.from(parent)
|
SongViewHolder.from(parent)
|
||||||
|
|
|
@ -37,6 +37,11 @@ import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.logW
|
import org.oxycblt.auxio.util.logW
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stateless interface for loading [Album] cover image data.
|
||||||
|
*
|
||||||
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
|
*/
|
||||||
interface CoverExtractor {
|
interface CoverExtractor {
|
||||||
/**
|
/**
|
||||||
* Fetch an album cover, respecting the current cover configuration.
|
* Fetch an album cover, respecting the current cover configuration.
|
||||||
|
|
|
@ -1,56 +0,0 @@
|
||||||
/*
|
|
||||||
* 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.RecyclerView
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A [RecyclerView.Adapter] with [ListDiffer] integration.
|
|
||||||
*
|
|
||||||
* @param differFactory The [ListDiffer.Factory] that defines the type of [ListDiffer] to use.
|
|
||||||
*/
|
|
||||||
abstract class DiffAdapter<T, I, VH : RecyclerView.ViewHolder>(
|
|
||||||
differFactory: ListDiffer.Factory<T, I>
|
|
||||||
) : 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 instructions.
|
|
||||||
*
|
|
||||||
* @param newList The new list of [T] items to show.
|
|
||||||
* @param instructions The instructions specifying how to update the list.
|
|
||||||
* @param onDone Called when the update process is completed. Defaults to a no-op.
|
|
||||||
*/
|
|
||||||
fun submitList(newList: List<T>, instructions: I, onDone: () -> Unit = {}) {
|
|
||||||
differ.submitList(newList, instructions, onDone)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,237 @@
|
||||||
|
/*
|
||||||
|
* 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 android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import androidx.recyclerview.widget.*
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import java.util.concurrent.Executor
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A variant of ListDiffer with more flexible updates.
|
||||||
|
*
|
||||||
|
* @param diffCallback A [DiffUtil.ItemCallback] to compare list updates with.
|
||||||
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
|
*/
|
||||||
|
abstract class FlexibleListAdapter<T, VH : RecyclerView.ViewHolder>(
|
||||||
|
diffCallback: DiffUtil.ItemCallback<T>
|
||||||
|
) : RecyclerView.Adapter<VH>() {
|
||||||
|
private val differ = FlexibleListDiffer(this, diffCallback)
|
||||||
|
final override fun getItemCount() = differ.currentList.size
|
||||||
|
/** The current list stored by the adapter's differ instance. */
|
||||||
|
val currentList: List<T>
|
||||||
|
get() = differ.currentList
|
||||||
|
/** @see currentList */
|
||||||
|
fun getItem(at: Int) = differ.currentList[at]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the adapter with new data.
|
||||||
|
*
|
||||||
|
* @param newData The new list of data to update with.
|
||||||
|
* @param instructions The [UpdateInstructions] to visually update the list with.
|
||||||
|
* @param callback Called when the update is completed. May be done asynchronously.
|
||||||
|
*/
|
||||||
|
fun update(
|
||||||
|
newData: List<T>,
|
||||||
|
instructions: UpdateInstructions?,
|
||||||
|
callback: (() -> Unit)? = null
|
||||||
|
) = differ.update(newData, instructions, callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Arbitrary instructions that can be given to a [FlexibleListAdapter] to direct how it updates
|
||||||
|
* data.
|
||||||
|
*
|
||||||
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
|
*/
|
||||||
|
sealed class UpdateInstructions {
|
||||||
|
/** Use an asynchronous diff. Useful for unpredictable updates, but looks chaotic and janky. */
|
||||||
|
object Diff : UpdateInstructions()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visually replace all items from a given point. More visually coherent than [Diff].
|
||||||
|
*
|
||||||
|
* @param from The index at which to start replacing items (inclusive)
|
||||||
|
*/
|
||||||
|
data class Replace(val from: Int) : UpdateInstructions()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move one item to another location.
|
||||||
|
*
|
||||||
|
* @param from The index of the item to move.
|
||||||
|
* @param to The index to move the item to.
|
||||||
|
*/
|
||||||
|
data class Move(val from: Int, val to: Int) : UpdateInstructions()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove an item.
|
||||||
|
*
|
||||||
|
* @param at The location that the item should be removed from.
|
||||||
|
*/
|
||||||
|
data class Remove(val at: Int) : UpdateInstructions()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vendor of AsyncListDiffer with more flexible update functionality.
|
||||||
|
*
|
||||||
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
|
*/
|
||||||
|
private class FlexibleListDiffer<T>(
|
||||||
|
adapter: RecyclerView.Adapter<*>,
|
||||||
|
diffCallback: DiffUtil.ItemCallback<T>
|
||||||
|
) {
|
||||||
|
private val updateCallback = AdapterListUpdateCallback(adapter)
|
||||||
|
private val config = AsyncDifferConfig.Builder(diffCallback).build()
|
||||||
|
private val mainThreadExecutor = sMainThreadExecutor
|
||||||
|
|
||||||
|
private class MainThreadExecutor : Executor {
|
||||||
|
val mHandler = Handler(Looper.getMainLooper())
|
||||||
|
override fun execute(command: Runnable) {
|
||||||
|
mHandler.post(command)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentList = emptyList<T>()
|
||||||
|
private set
|
||||||
|
|
||||||
|
private var maxScheduledGeneration = 0
|
||||||
|
|
||||||
|
fun update(newList: List<T>, instructions: UpdateInstructions?, callback: (() -> Unit)?) {
|
||||||
|
// incrementing generation means any currently-running diffs are discarded when they finish
|
||||||
|
val runGeneration = ++maxScheduledGeneration
|
||||||
|
if (currentList == newList) {
|
||||||
|
callback?.invoke()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
when (instructions) {
|
||||||
|
is UpdateInstructions.Replace -> {
|
||||||
|
updateCallback.onRemoved(instructions.from, currentList.size - instructions.from)
|
||||||
|
currentList = newList
|
||||||
|
if (newList.lastIndex >= instructions.from) {
|
||||||
|
// Need to re-insert the new data.
|
||||||
|
updateCallback.onInserted(instructions.from, newList.size - instructions.from)
|
||||||
|
}
|
||||||
|
callback?.invoke()
|
||||||
|
}
|
||||||
|
is UpdateInstructions.Move -> {
|
||||||
|
currentList = newList
|
||||||
|
updateCallback.onMoved(instructions.from, instructions.to)
|
||||||
|
callback?.invoke()
|
||||||
|
}
|
||||||
|
is UpdateInstructions.Remove -> {
|
||||||
|
currentList = newList
|
||||||
|
updateCallback.onRemoved(instructions.at, 1)
|
||||||
|
callback?.invoke()
|
||||||
|
}
|
||||||
|
is UpdateInstructions.Diff,
|
||||||
|
null -> diffList(currentList, newList, runGeneration, callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun diffList(
|
||||||
|
oldList: List<T>,
|
||||||
|
newList: List<T>,
|
||||||
|
runGeneration: Int,
|
||||||
|
callback: (() -> Unit)?
|
||||||
|
) {
|
||||||
|
// fast simple remove all
|
||||||
|
if (newList.isEmpty()) {
|
||||||
|
val countRemoved = oldList.size
|
||||||
|
currentList = emptyList()
|
||||||
|
// notify last, after list is updated
|
||||||
|
updateCallback.onRemoved(0, countRemoved)
|
||||||
|
callback?.invoke()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// fast simple first insert
|
||||||
|
if (oldList.isEmpty()) {
|
||||||
|
currentList = newList
|
||||||
|
// notify last, after list is updated
|
||||||
|
updateCallback.onInserted(0, newList.size)
|
||||||
|
callback?.invoke()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
config.backgroundThreadExecutor.execute {
|
||||||
|
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) {
|
||||||
|
config.diffCallback.areItemsTheSame(oldItem, newItem)
|
||||||
|
} else oldItem == null && newItem == null
|
||||||
|
// If both items are null we consider them the same.
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(
|
||||||
|
oldItemPosition: Int,
|
||||||
|
newItemPosition: Int
|
||||||
|
): Boolean {
|
||||||
|
val oldItem: T? = oldList[oldItemPosition]
|
||||||
|
val newItem: T? = newList[newItemPosition]
|
||||||
|
if (oldItem != null && newItem != null) {
|
||||||
|
return config.diffCallback.areContentsTheSame(oldItem, newItem)
|
||||||
|
}
|
||||||
|
if (oldItem == null && newItem == null) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
throw AssertionError()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getChangePayload(
|
||||||
|
oldItemPosition: Int,
|
||||||
|
newItemPosition: Int
|
||||||
|
): Any? {
|
||||||
|
val oldItem: T? = oldList[oldItemPosition]
|
||||||
|
val newItem: T? = newList[newItemPosition]
|
||||||
|
if (oldItem != null && newItem != null) {
|
||||||
|
return config.diffCallback.getChangePayload(oldItem, newItem)
|
||||||
|
}
|
||||||
|
throw AssertionError()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
mainThreadExecutor.execute {
|
||||||
|
if (maxScheduledGeneration == runGeneration) {
|
||||||
|
currentList = newList
|
||||||
|
result.dispatchUpdatesTo(updateCallback)
|
||||||
|
callback?.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val sMainThreadExecutor: Executor = MainThreadExecutor()
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,232 +0,0 @@
|
||||||
/*
|
|
||||||
* 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.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
|
|
||||||
|
|
||||||
// TODO: Re-add list instructions with a less dangerous framework.
|
|
||||||
|
|
||||||
/**
|
|
||||||
* List differ wrapper that provides more flexibility regarding the way lists are updated.
|
|
||||||
*
|
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
|
||||||
*/
|
|
||||||
interface ListDiffer<T, I> {
|
|
||||||
/** The current list of [T] items. */
|
|
||||||
val currentList: List<T>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dynamically determine how to update the list based on the given instructions.
|
|
||||||
*
|
|
||||||
* @param newList The new list of [T] items to show.
|
|
||||||
* @param instructions The [BasicListInstructions] specifying how to update the list.
|
|
||||||
* @param onDone Called when the update process is completed.
|
|
||||||
*/
|
|
||||||
fun submitList(newList: List<T>, instructions: I, onDone: () -> Unit)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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, I> {
|
|
||||||
/**
|
|
||||||
* 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, I>
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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, BasicListInstructions>() {
|
|
||||||
override fun new(adapter: RecyclerView.Adapter<*>): ListDiffer<T, BasicListInstructions> =
|
|
||||||
AsyncListDifferImpl(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 suffer from 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, BasicListInstructions>() {
|
|
||||||
override fun new(adapter: RecyclerView.Adapter<*>): ListDiffer<T, BasicListInstructions> =
|
|
||||||
BlockingListDifferImpl(AdapterListUpdateCallback(adapter), diffCallback)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents the specific way to update a list of items.
|
|
||||||
*
|
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
|
||||||
*/
|
|
||||||
enum class BasicListInstructions {
|
|
||||||
/**
|
|
||||||
* (A)synchronously diff the list. This should be used for small diffs with little item
|
|
||||||
* movement.
|
|
||||||
*/
|
|
||||||
DIFF,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Synchronously remove the current list and replace it with a new one. This should be used for
|
|
||||||
* large diffs with that would cause erratic scroll behavior or in-efficiency.
|
|
||||||
*/
|
|
||||||
REPLACE
|
|
||||||
}
|
|
||||||
|
|
||||||
private abstract class BasicListDiffer<T> : ListDiffer<T, BasicListInstructions> {
|
|
||||||
override fun submitList(
|
|
||||||
newList: List<T>,
|
|
||||||
instructions: BasicListInstructions,
|
|
||||||
onDone: () -> Unit
|
|
||||||
) {
|
|
||||||
when (instructions) {
|
|
||||||
BasicListInstructions.DIFF -> diffList(newList, onDone)
|
|
||||||
BasicListInstructions.REPLACE -> replaceList(newList, onDone)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract fun diffList(newList: List<T>, onDone: () -> Unit)
|
|
||||||
protected abstract fun replaceList(newList: List<T>, onDone: () -> Unit)
|
|
||||||
}
|
|
||||||
|
|
||||||
private class AsyncListDifferImpl<T>(
|
|
||||||
updateCallback: ListUpdateCallback,
|
|
||||||
diffCallback: DiffUtil.ItemCallback<T>
|
|
||||||
) : BasicListDiffer<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>, onDone: () -> Unit) {
|
|
||||||
inner.submitList(null) { inner.submitList(newList, onDone) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class BlockingListDifferImpl<T>(
|
|
||||||
private val updateCallback: ListUpdateCallback,
|
|
||||||
private val diffCallback: DiffUtil.ItemCallback<T>
|
|
||||||
) : BasicListDiffer<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>, onDone: () -> Unit) {
|
|
||||||
if (currentList != newList) {
|
|
||||||
diffList(listOf()) { diffList(newList, onDone) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -18,18 +18,19 @@
|
||||||
package org.oxycblt.auxio.list.adapter
|
package org.oxycblt.auxio.list.adapter
|
||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
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.
|
* @param diffCallback A [DiffUtil.ItemCallback] to compare list updates with.
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
abstract class PlayingIndicatorAdapter<T, I, VH : RecyclerView.ViewHolder>(
|
abstract class PlayingIndicatorAdapter<T, VH : RecyclerView.ViewHolder>(
|
||||||
differFactory: ListDiffer.Factory<T, I>
|
diffCallback: DiffUtil.ItemCallback<T>
|
||||||
) : DiffAdapter<T, I, VH>(differFactory) {
|
) : FlexibleListAdapter<T, VH>(diffCallback) {
|
||||||
// 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
|
||||||
|
@ -40,7 +41,7 @@ abstract class PlayingIndicatorAdapter<T, I, VH : RecyclerView.ViewHolder>(
|
||||||
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] == currentItem, isPlaying)
|
holder.updatePlayingIndicator(getItem(position) == currentItem, isPlaying)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (payloads.isEmpty()) {
|
if (payloads.isEmpty()) {
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
package org.oxycblt.auxio.list.adapter
|
package org.oxycblt.auxio.list.adapter
|
||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
|
|
||||||
|
@ -25,12 +26,12 @@ 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.
|
* @param diffCallback A [DiffUtil.ItemCallback] to compare list updates with.
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
abstract class SelectionIndicatorAdapter<T, I, VH : RecyclerView.ViewHolder>(
|
abstract class SelectionIndicatorAdapter<T, VH : RecyclerView.ViewHolder>(
|
||||||
differFactory: ListDiffer.Factory<T, I>
|
diffCallback: DiffUtil.ItemCallback<T>
|
||||||
) : PlayingIndicatorAdapter<T, I, VH>(differFactory) {
|
) : PlayingIndicatorAdapter<T, VH>(diffCallback) {
|
||||||
private var selectedItems = setOf<T>()
|
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>) {
|
||||||
|
@ -64,9 +65,7 @@ abstract class SelectionIndicatorAdapter<T, I, VH : RecyclerView.ViewHolder>(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only update items that were added or removed from the list.
|
// Only update items that were added or removed from the list.
|
||||||
val added = !oldSelectedItems.contains(item) && newSelectedItems.contains(item)
|
if (oldSelectedItems.contains(item) xor newSelectedItems.contains(item)) {
|
||||||
val removed = oldSelectedItems.contains(item) && !newSelectedItems.contains(item)
|
|
||||||
if (added || removed) {
|
|
||||||
notifyItemChanged(i, PAYLOAD_SELECTION_INDICATOR_CHANGED)
|
notifyItemChanged(i, PAYLOAD_SELECTION_INDICATOR_CHANGED)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +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
|
import org.oxycblt.auxio.list.adapter.FlexibleListAdapter
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [BackportMaterialDividerItemDecoration] that sets up the divider configuration to correctly
|
* A [BackportMaterialDividerItemDecoration] that sets up the divider configuration to correctly
|
||||||
|
@ -45,7 +45,7 @@ constructor(
|
||||||
// Add a divider if the next item is a header. This organizes the divider to separate
|
// Add a divider if the next item is a header. This organizes the divider to separate
|
||||||
// the ends of content rather than the beginning of content, alongside an added benefit
|
// the ends of content rather than the beginning of content, alongside an added benefit
|
||||||
// of preventing top headers from having a divider applied.
|
// of preventing top headers from having a divider applied.
|
||||||
(adapter as DiffAdapter<*, *, *>).getItem(position + 1) is Header
|
(adapter as FlexibleListAdapter<*, *>).getItem(position + 1) is Header
|
||||||
} catch (e: ClassCastException) {
|
} catch (e: ClassCastException) {
|
||||||
false
|
false
|
||||||
} catch (e: IndexOutOfBoundsException) {
|
} catch (e: IndexOutOfBoundsException) {
|
||||||
|
|
|
@ -29,20 +29,35 @@ import org.oxycblt.auxio.music.storage.toAudioUri
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.logW
|
import org.oxycblt.auxio.util.logW
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An processing abstraction over the [MetadataRetriever] and [TextTags] workflow that operates on
|
||||||
|
* [RawSong] instances.
|
||||||
|
*
|
||||||
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
|
*/
|
||||||
interface TagWorker {
|
interface TagWorker {
|
||||||
|
/**
|
||||||
|
* Poll to see if this worker is done processing.
|
||||||
|
*
|
||||||
|
* @return A completed [RawSong] if done, null otherwise.
|
||||||
|
*/
|
||||||
fun poll(): RawSong?
|
fun poll(): RawSong?
|
||||||
|
|
||||||
|
/** Factory for new [TagWorker] jobs. */
|
||||||
interface Factory {
|
interface Factory {
|
||||||
|
/**
|
||||||
|
* Create a new [TagWorker] to complete the given [RawSong].
|
||||||
|
*
|
||||||
|
* @param rawSong The [RawSong] to assign a new [TagWorker] to.
|
||||||
|
* @return A new [TagWorker] wrapping the given [RawSong].
|
||||||
|
*/
|
||||||
fun create(rawSong: RawSong): TagWorker
|
fun create(rawSong: RawSong): TagWorker
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TagWorkerImpl(private val rawSong: RawSong, private val future: Future<TrackGroupArray>) :
|
class TagWorkerImpl
|
||||||
|
private constructor(private val rawSong: RawSong, private val future: Future<TrackGroupArray>) :
|
||||||
TagWorker {
|
TagWorker {
|
||||||
// Note that we do not leverage future callbacks. This is because errors in the
|
|
||||||
// (highly fallible) extraction process will not bubble up to Indexer when a
|
|
||||||
// listener is used, instead crashing the app entirely.
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Try to get a completed song from this [TagWorker], if it has finished processing.
|
* Try to get a completed song from this [TagWorker], if it has finished processing.
|
||||||
*
|
*
|
||||||
|
@ -246,6 +261,9 @@ class TagWorkerImpl(private val rawSong: RawSong, private val future: Future<Tra
|
||||||
class Factory @Inject constructor(private val mediaSourceFactory: MediaSource.Factory) :
|
class Factory @Inject constructor(private val mediaSourceFactory: MediaSource.Factory) :
|
||||||
TagWorker.Factory {
|
TagWorker.Factory {
|
||||||
override fun create(rawSong: RawSong) =
|
override fun create(rawSong: RawSong) =
|
||||||
|
// Note that we do not leverage future callbacks. This is because errors in the
|
||||||
|
// (highly fallible) extraction process will not bubble up to Indexer when a
|
||||||
|
// listener is used, instead crashing the app entirely.
|
||||||
TagWorkerImpl(
|
TagWorkerImpl(
|
||||||
rawSong,
|
rawSong,
|
||||||
MetadataRetriever.retrieveMetadata(
|
MetadataRetriever.retrieveMetadata(
|
||||||
|
|
|
@ -98,6 +98,26 @@ interface Queue {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class QueueChange(val internal: InternalChange, val externalChange: ExternalChange) {
|
||||||
|
enum class InternalChange {
|
||||||
|
/** Only the mapping has changed. */
|
||||||
|
MAPPING,
|
||||||
|
/** The mapping has changed, and the index also changed to align with it. */
|
||||||
|
INDEX,
|
||||||
|
/**
|
||||||
|
* The current song has changed, possibly alongside the mapping and index depending on the
|
||||||
|
* context.
|
||||||
|
*/
|
||||||
|
SONG
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class ExternalChange {
|
||||||
|
data class Add(val at: Int, val amount: Int) : ExternalChange()
|
||||||
|
data class Remove(val at: Int) : ExternalChange()
|
||||||
|
data class Move(val from: Int, val to: Int) : ExternalChange()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class EditableQueue : Queue {
|
class EditableQueue : Queue {
|
||||||
@Volatile private var heap = mutableListOf<Song>()
|
@Volatile private var heap = mutableListOf<Song>()
|
||||||
@Volatile private var orderedMapping = mutableListOf<Int>()
|
@Volatile private var orderedMapping = mutableListOf<Int>()
|
||||||
|
|
|
@ -27,10 +27,7 @@ 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.adapter.BasicListInstructions
|
import org.oxycblt.auxio.list.adapter.*
|
||||||
import org.oxycblt.auxio.list.adapter.DiffAdapter
|
|
||||||
import org.oxycblt.auxio.list.adapter.ListDiffer
|
|
||||||
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.music.resolveNames
|
import org.oxycblt.auxio.music.resolveNames
|
||||||
|
@ -43,8 +40,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, BasicListInstructions, QueueSongViewHolder>(
|
FlexibleListAdapter<Song, QueueSongViewHolder>(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
|
||||||
// with an index value instead.
|
// with an index value instead.
|
||||||
|
|
|
@ -28,7 +28,6 @@ import dagger.hilt.android.AndroidEntryPoint
|
||||||
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.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
|
||||||
|
@ -102,13 +101,12 @@ class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), EditableListL
|
||||||
val binding = requireBinding()
|
val binding = requireBinding()
|
||||||
|
|
||||||
// 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.queueListInstructions
|
queueAdapter.update(queue, queueModel.queueInstructions.consume())
|
||||||
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)
|
||||||
|
|
||||||
// If requested, scroll to a new item (occurs when the index moves)
|
// If requested, scroll to a new item (occurs when the index moves)
|
||||||
val scrollTo = instructions?.scrollTo
|
val scrollTo = queueModel.scrollTo.consume()
|
||||||
if (scrollTo != null) {
|
if (scrollTo != null) {
|
||||||
val lmm = binding.queueRecycler.layoutManager as LinearLayoutManager
|
val lmm = binding.queueRecycler.layoutManager as LinearLayoutManager
|
||||||
val start = lmm.findFirstCompletelyVisibleItemPosition()
|
val start = lmm.findFirstCompletelyVisibleItemPosition()
|
||||||
|
@ -129,7 +127,5 @@ class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), EditableListL
|
||||||
min(queue.lastIndex, scrollTo + (end - start)))
|
min(queue.lastIndex, scrollTo + (end - start)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
queueModel.finishInstructions()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,10 +22,12 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
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.adapter.BasicListInstructions
|
import org.oxycblt.auxio.list.adapter.UpdateInstructions
|
||||||
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
|
||||||
|
import org.oxycblt.auxio.util.Event
|
||||||
|
import org.oxycblt.auxio.util.MutableEvent
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [ViewModel] that manages the current queue state and allows navigation through the queue.
|
* A [ViewModel] that manages the current queue state and allows navigation through the queue.
|
||||||
|
@ -39,27 +41,32 @@ class QueueViewModel @Inject constructor(private val playbackManager: PlaybackSt
|
||||||
private val _queue = MutableStateFlow(listOf<Song>())
|
private val _queue = MutableStateFlow(listOf<Song>())
|
||||||
/** The current queue. */
|
/** The current queue. */
|
||||||
val queue: StateFlow<List<Song>> = _queue
|
val queue: StateFlow<List<Song>> = _queue
|
||||||
|
private val _queueInstructions = MutableEvent<UpdateInstructions>()
|
||||||
|
/** Instructions for how to update [queue] in the UI. */
|
||||||
|
val queueInstructions: Event<UpdateInstructions> = _queueInstructions
|
||||||
|
private val _scrollTo = MutableEvent<Int>()
|
||||||
|
/** Controls whether the queue should be force-scrolled to a particular location. */
|
||||||
|
val scrollTo: Event<Int>
|
||||||
|
get() = _scrollTo
|
||||||
|
|
||||||
private val _index = MutableStateFlow(playbackManager.queue.index)
|
private val _index = MutableStateFlow(playbackManager.queue.index)
|
||||||
/** The index of the currently playing song in the queue. */
|
/** The index of the currently playing song in the queue. */
|
||||||
val index: StateFlow<Int>
|
val index: StateFlow<Int>
|
||||||
get() = _index
|
get() = _index
|
||||||
|
|
||||||
/** Specifies how to update the list when the queue changes. */
|
|
||||||
var queueListInstructions: ListInstructions? = null
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
playbackManager.addListener(this)
|
playbackManager.addListener(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onIndexMoved(queue: Queue) {
|
override fun onIndexMoved(queue: Queue) {
|
||||||
queueListInstructions = ListInstructions(null, queue.index)
|
_scrollTo.put(queue.index)
|
||||||
_index.value = queue.index
|
_index.value = queue.index
|
||||||
}
|
}
|
||||||
|
|
||||||
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.
|
||||||
queueListInstructions = ListInstructions(BasicListInstructions.DIFF, null)
|
// TODO: Terrible idea, need to manually deliver updates
|
||||||
|
_queueInstructions.put(UpdateInstructions.Diff)
|
||||||
_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.
|
||||||
|
@ -69,14 +76,16 @@ class QueueViewModel @Inject constructor(private val playbackManager: PlaybackSt
|
||||||
|
|
||||||
override fun onQueueReordered(queue: Queue) {
|
override fun onQueueReordered(queue: Queue) {
|
||||||
// Queue changed completely -> Replace queue, update index
|
// Queue changed completely -> Replace queue, update index
|
||||||
queueListInstructions = ListInstructions(BasicListInstructions.REPLACE, queue.index)
|
_queueInstructions.put(UpdateInstructions.Replace(0))
|
||||||
|
_scrollTo.put(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
|
||||||
queueListInstructions = ListInstructions(BasicListInstructions.REPLACE, queue.index)
|
_queueInstructions.put(UpdateInstructions.Replace(0))
|
||||||
|
_scrollTo.put(queue.index)
|
||||||
_queue.value = queue.resolve()
|
_queue.value = queue.resolve()
|
||||||
_index.value = queue.index
|
_index.value = queue.index
|
||||||
}
|
}
|
||||||
|
@ -123,11 +132,4 @@ class QueueViewModel @Inject constructor(private val playbackManager: PlaybackSt
|
||||||
playbackManager.moveQueueItem(adapterFrom, adapterTo)
|
playbackManager.moveQueueItem(adapterFrom, adapterTo)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Signal that the specified [ListInstructions] in [queueListInstructions] were performed. */
|
|
||||||
fun finishInstructions() {
|
|
||||||
queueListInstructions = null
|
|
||||||
}
|
|
||||||
|
|
||||||
class ListInstructions(val update: BasicListInstructions?, val scrollTo: Int?)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -114,9 +114,6 @@ class PlaybackService :
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
// Define our own extractors so we can exclude non-audio parsers.
|
|
||||||
// Ordering is derived from the DefaultExtractorsFactory's optimized ordering:
|
|
||||||
// https://docs.google.com/document/d/1w2mKaWMxfz2Ei8-LdxqbPs1VLe_oudB-eryXXw9OvQQ.
|
|
||||||
// Since Auxio is a music player, only specify an audio renderer to save
|
// Since Auxio is a music player, only specify an audio renderer to save
|
||||||
// battery/apk size/cache size
|
// battery/apk size/cache size
|
||||||
val audioRenderer = RenderersFactory { handler, _, audioListener, _, _ ->
|
val audioRenderer = RenderersFactory { handler, _, audioListener, _, _ ->
|
||||||
|
|
|
@ -20,8 +20,6 @@ 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.SelectionIndicatorAdapter
|
||||||
import org.oxycblt.auxio.list.adapter.SimpleDiffCallback
|
import org.oxycblt.auxio.list.adapter.SimpleDiffCallback
|
||||||
import org.oxycblt.auxio.list.recycler.*
|
import org.oxycblt.auxio.list.recycler.*
|
||||||
|
@ -35,8 +33,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, BasicListInstructions, RecyclerView.ViewHolder>(
|
SelectionIndicatorAdapter<Item, RecyclerView.ViewHolder>(DIFF_CALLBACK),
|
||||||
ListDiffer.Async(DIFF_CALLBACK)),
|
|
||||||
AuxioRecyclerView.SpanSizeLookup {
|
AuxioRecyclerView.SpanSizeLookup {
|
||||||
|
|
||||||
override fun getItemViewType(position: Int) =
|
override fun getItemViewType(position: Int) =
|
||||||
|
|
|
@ -34,7 +34,6 @@ 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.adapter.BasicListInstructions
|
|
||||||
import org.oxycblt.auxio.list.selection.SelectionViewModel
|
import org.oxycblt.auxio.list.selection.SelectionViewModel
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
|
@ -163,7 +162,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(), BasicListInstructions.DIFF) {
|
searchAdapter.update(results.toMutableList(), null) {
|
||||||
// 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.
|
||||||
|
|
|
@ -28,16 +28,8 @@ import androidx.appcompat.widget.AppCompatButton
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import androidx.core.graphics.Insets
|
import androidx.core.graphics.Insets
|
||||||
import androidx.core.graphics.drawable.DrawableCompat
|
import androidx.core.graphics.drawable.DrawableCompat
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.lifecycle.Lifecycle
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.viewbinding.ViewBinding
|
import androidx.viewbinding.ViewBinding
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.flow.combine
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get if this [View] contains the given [PointF], with optional leeway.
|
* Get if this [View] contains the given [PointF], with optional leeway.
|
||||||
|
@ -127,88 +119,6 @@ fun AppCompatButton.fixDoubleRipple() {
|
||||||
val View.coordinatorLayoutBehavior: CoordinatorLayout.Behavior<View>?
|
val View.coordinatorLayoutBehavior: CoordinatorLayout.Behavior<View>?
|
||||||
get() = (layoutParams as? CoordinatorLayout.LayoutParams)?.behavior
|
get() = (layoutParams as? CoordinatorLayout.LayoutParams)?.behavior
|
||||||
|
|
||||||
/**
|
|
||||||
* Collect a [StateFlow] into [block] in a lifecycle-aware manner *eventually.* Due to co-routine
|
|
||||||
* launching, the initializing call will occur ~100ms after draw time. If this is not desirable, use
|
|
||||||
* [collectImmediately].
|
|
||||||
*
|
|
||||||
* @param stateFlow The [StateFlow] to collect.
|
|
||||||
* @param block The code to run when the [StateFlow] updates.
|
|
||||||
*/
|
|
||||||
fun <T> Fragment.collect(stateFlow: StateFlow<T>, block: (T) -> Unit) {
|
|
||||||
launch { stateFlow.collect(block) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Collect a [StateFlow] into a [block] in a lifecycle-aware manner *immediately.* This will
|
|
||||||
* immediately run an initializing call to ensure the UI is set up before draw-time. Note that this
|
|
||||||
* will result in two initializing calls.
|
|
||||||
*
|
|
||||||
* @param stateFlow The [StateFlow] to collect.
|
|
||||||
* @param block The code to run when the [StateFlow] updates.
|
|
||||||
*/
|
|
||||||
fun <T> Fragment.collectImmediately(stateFlow: StateFlow<T>, block: (T) -> Unit) {
|
|
||||||
block(stateFlow.value)
|
|
||||||
launch { stateFlow.collect(block) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Like [collectImmediately], but with two [StateFlow] instances that are collected with the same
|
|
||||||
* block.
|
|
||||||
*
|
|
||||||
* @param a The first [StateFlow] to collect.
|
|
||||||
* @param b The second [StateFlow] to collect.
|
|
||||||
* @param block The code to run when either [StateFlow] updates.
|
|
||||||
*/
|
|
||||||
fun <T1, T2> Fragment.collectImmediately(
|
|
||||||
a: StateFlow<T1>,
|
|
||||||
b: StateFlow<T2>,
|
|
||||||
block: (T1, T2) -> Unit
|
|
||||||
) {
|
|
||||||
block(a.value, b.value)
|
|
||||||
// We can combine flows, but only if we transform them into one flow output.
|
|
||||||
// Thus, we have to first combine the two flow values into a Pair, and then
|
|
||||||
// decompose it when we collect the values.
|
|
||||||
val combine = a.combine(b) { first, second -> Pair(first, second) }
|
|
||||||
launch { combine.collect { block(it.first, it.second) } }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Like [collectImmediately], but with three [StateFlow] instances that are collected with the same
|
|
||||||
* block.
|
|
||||||
*
|
|
||||||
* @param a The first [StateFlow] to collect.
|
|
||||||
* @param b The second [StateFlow] to collect.
|
|
||||||
* @param c The third [StateFlow] to collect.
|
|
||||||
* @param block The code to run when any of the [StateFlow]s update.
|
|
||||||
*/
|
|
||||||
fun <T1, T2, T3> Fragment.collectImmediately(
|
|
||||||
a: StateFlow<T1>,
|
|
||||||
b: StateFlow<T2>,
|
|
||||||
c: StateFlow<T3>,
|
|
||||||
block: (T1, T2, T3) -> Unit
|
|
||||||
) {
|
|
||||||
block(a.value, b.value, c.value)
|
|
||||||
val combine = combine(a, b, c) { a1, b2, c3 -> Triple(a1, b2, c3) }
|
|
||||||
launch { combine.collect { block(it.first, it.second, it.third) } }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Launch a [Fragment] co-routine whenever the [Lifecycle] hits the given [Lifecycle.State]. This
|
|
||||||
* should always been used when launching [Fragment] co-routines was it will not result in
|
|
||||||
* unexpected behavior.
|
|
||||||
*
|
|
||||||
* @param state The [Lifecycle.State] to launch the co-routine in.
|
|
||||||
* @param block The block to run in the co-routine.
|
|
||||||
* @see repeatOnLifecycle
|
|
||||||
*/
|
|
||||||
private fun Fragment.launch(
|
|
||||||
state: Lifecycle.State = Lifecycle.State.STARTED,
|
|
||||||
block: suspend CoroutineScope.() -> Unit
|
|
||||||
) {
|
|
||||||
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(state, block) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the "System Bar" [Insets] in this [WindowInsets] instance in a version-compatible manner This
|
* Get the "System Bar" [Insets] in this [WindowInsets] instance in a version-compatible manner This
|
||||||
* can be used to prevent [View] elements from intersecting with the navigation bars.
|
* can be used to prevent [View] elements from intersecting with the navigation bars.
|
||||||
|
|
123
app/src/main/java/org/oxycblt/auxio/util/StateUtil.kt
Normal file
123
app/src/main/java/org/oxycblt/auxio/util/StateUtil.kt
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Auxio Project
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.oxycblt.auxio.util
|
||||||
|
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
interface Event<T> {
|
||||||
|
val flow: StateFlow<T?>
|
||||||
|
fun consume(): T?
|
||||||
|
}
|
||||||
|
|
||||||
|
class MutableEvent<T> : Event<T> {
|
||||||
|
override val flow = MutableStateFlow<T?>(null)
|
||||||
|
fun put(v: T) {
|
||||||
|
flow.value = v
|
||||||
|
}
|
||||||
|
override fun consume() = flow.value?.also { flow.value = null }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collect a [StateFlow] into [block] in a lifecycle-aware manner *eventually.* Due to co-routine
|
||||||
|
* launching, the initializing call will occur ~100ms after draw time. If this is not desirable, use
|
||||||
|
* [collectImmediately].
|
||||||
|
*
|
||||||
|
* @param stateFlow The [StateFlow] to collect.
|
||||||
|
* @param block The code to run when the [StateFlow] updates.
|
||||||
|
*/
|
||||||
|
fun <T> Fragment.collect(stateFlow: StateFlow<T>, block: (T) -> Unit) {
|
||||||
|
launch { stateFlow.collect(block) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collect a [StateFlow] into a [block] in a lifecycle-aware manner *immediately.* This will
|
||||||
|
* immediately run an initializing call to ensure the UI is set up before draw-time. Note that this
|
||||||
|
* will result in two initializing calls.
|
||||||
|
*
|
||||||
|
* @param stateFlow The [StateFlow] to collect.
|
||||||
|
* @param block The code to run when the [StateFlow] updates.
|
||||||
|
*/
|
||||||
|
fun <T> Fragment.collectImmediately(stateFlow: StateFlow<T>, block: (T) -> Unit) {
|
||||||
|
block(stateFlow.value)
|
||||||
|
launch { stateFlow.collect(block) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Like [collectImmediately], but with two [StateFlow] instances that are collected with the same
|
||||||
|
* block.
|
||||||
|
*
|
||||||
|
* @param a The first [StateFlow] to collect.
|
||||||
|
* @param b The second [StateFlow] to collect.
|
||||||
|
* @param block The code to run when either [StateFlow] updates.
|
||||||
|
*/
|
||||||
|
fun <T1, T2> Fragment.collectImmediately(
|
||||||
|
a: StateFlow<T1>,
|
||||||
|
b: StateFlow<T2>,
|
||||||
|
block: (T1, T2) -> Unit
|
||||||
|
) {
|
||||||
|
block(a.value, b.value)
|
||||||
|
// We can combine flows, but only if we transform them into one flow output.
|
||||||
|
// Thus, we have to first combine the two flow values into a Pair, and then
|
||||||
|
// decompose it when we collect the values.
|
||||||
|
val combine = a.combine(b) { first, second -> Pair(first, second) }
|
||||||
|
launch { combine.collect { block(it.first, it.second) } }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Like [collectImmediately], but with three [StateFlow] instances that are collected with the same
|
||||||
|
* block.
|
||||||
|
*
|
||||||
|
* @param a The first [StateFlow] to collect.
|
||||||
|
* @param b The second [StateFlow] to collect.
|
||||||
|
* @param c The third [StateFlow] to collect.
|
||||||
|
* @param block The code to run when any of the [StateFlow]s update.
|
||||||
|
*/
|
||||||
|
fun <T1, T2, T3> Fragment.collectImmediately(
|
||||||
|
a: StateFlow<T1>,
|
||||||
|
b: StateFlow<T2>,
|
||||||
|
c: StateFlow<T3>,
|
||||||
|
block: (T1, T2, T3) -> Unit
|
||||||
|
) {
|
||||||
|
block(a.value, b.value, c.value)
|
||||||
|
val combine = combine(a, b, c) { a1, b2, c3 -> Triple(a1, b2, c3) }
|
||||||
|
launch { combine.collect { block(it.first, it.second, it.third) } }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Launch a [Fragment] co-routine whenever the [Lifecycle] hits the given [Lifecycle.State]. This
|
||||||
|
* should always been used when launching [Fragment] co-routines was it will not result in
|
||||||
|
* unexpected behavior.
|
||||||
|
*
|
||||||
|
* @param state The [Lifecycle.State] to launch the co-routine in.
|
||||||
|
* @param block The block to run in the co-routine.
|
||||||
|
* @see repeatOnLifecycle
|
||||||
|
*/
|
||||||
|
private fun Fragment.launch(
|
||||||
|
state: Lifecycle.State = Lifecycle.State.STARTED,
|
||||||
|
block: suspend CoroutineScope.() -> Unit
|
||||||
|
) {
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(state, block) }
|
||||||
|
}
|
|
@ -92,13 +92,11 @@ constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
return if (cornerRadius > 0) {
|
return if (cornerRadius > 0) {
|
||||||
// If rounded, educe the bitmap size further to obtain more pronounced
|
// If rounded, reduce the bitmap size further to obtain more pronounced
|
||||||
// rounded corners.
|
// rounded corners.
|
||||||
builder
|
builder.transformations(
|
||||||
.size(getSafeRemoteViewsImageSize(context, 10f))
|
SquareFrameTransform.INSTANCE,
|
||||||
.transformations(
|
RoundedCornersTransformation(cornerRadius.toFloat()))
|
||||||
SquareFrameTransform.INSTANCE,
|
|
||||||
RoundedCornersTransformation(cornerRadius.toFloat()))
|
|
||||||
} else {
|
} else {
|
||||||
builder.size(getSafeRemoteViewsImageSize(context))
|
builder.size(getSafeRemoteViewsImageSize(context))
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue