home: add selection framework
Add the basic selection flow to the home UI. This has no function yet. Further work needs to be done first.
This commit is contained in:
parent
361ca422e3
commit
6f05697088
9 changed files with 83 additions and 22 deletions
|
@ -18,9 +18,11 @@
|
|||
package org.oxycblt.auxio.home
|
||||
|
||||
import android.app.Application
|
||||
import androidx.collection.arraySetOf
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.getAndUpdate
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.home.tabs.Tab
|
||||
import org.oxycblt.auxio.music.Album
|
||||
|
@ -60,6 +62,10 @@ class HomeViewModel(application: Application) :
|
|||
val genres: StateFlow<List<Genre>>
|
||||
get() = _genres
|
||||
|
||||
private val _selected = MutableStateFlow(listOf<Music>())
|
||||
val selected: StateFlow<List<Music>>
|
||||
get() = _selected
|
||||
|
||||
var tabs: List<MusicMode> = visibleTabs
|
||||
private set
|
||||
|
||||
|
@ -84,6 +90,19 @@ class HomeViewModel(application: Application) :
|
|||
musicStore.addCallback(this)
|
||||
}
|
||||
|
||||
/** Select a music item. */
|
||||
fun select(item: Music) {
|
||||
val items = _selected.value.toMutableList()
|
||||
if (items.remove(item)) {
|
||||
logD("Unselecting item $item")
|
||||
_selected.value = items
|
||||
} else {
|
||||
logD("Selecting item $item")
|
||||
items.add(item)
|
||||
_selected.value = items
|
||||
}
|
||||
}
|
||||
|
||||
/** Update the current tab based off of the new ViewPager position. */
|
||||
fun updateCurrentTab(pos: Int) {
|
||||
logD("Updating current tab to ${tabs[pos]}")
|
||||
|
|
|
@ -25,6 +25,7 @@ import java.util.Formatter
|
|||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.music.MusicMode
|
||||
import org.oxycblt.auxio.music.MusicParent
|
||||
import org.oxycblt.auxio.music.Sort
|
||||
|
@ -97,9 +98,9 @@ class AlbumListFragment : HomeListFragment<Album>() {
|
|||
}
|
||||
}
|
||||
|
||||
override fun onItemClick(item: Item) {
|
||||
check(item is Album) { "Unexpected datatype: ${item::class.java}" }
|
||||
navModel.exploreNavigateTo(item)
|
||||
override fun onItemClick(music: Music) {
|
||||
check(music is Album) { "Unexpected datatype: ${music::class.java}" }
|
||||
navModel.exploreNavigateTo(music)
|
||||
}
|
||||
|
||||
override fun onOpenMenu(item: Item, anchor: View) {
|
||||
|
|
|
@ -23,6 +23,7 @@ import android.view.ViewGroup
|
|||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.music.MusicMode
|
||||
import org.oxycblt.auxio.music.MusicParent
|
||||
import org.oxycblt.auxio.music.Sort
|
||||
|
@ -73,9 +74,9 @@ class ArtistListFragment : HomeListFragment<Artist>() {
|
|||
}
|
||||
}
|
||||
|
||||
override fun onItemClick(item: Item) {
|
||||
check(item is Artist) { "Unexpected datatype: ${item::class.java}" }
|
||||
navModel.exploreNavigateTo(item)
|
||||
override fun onItemClick(music: Music) {
|
||||
check(music is Artist) { "Unexpected datatype: ${music::class.java}" }
|
||||
navModel.exploreNavigateTo(music)
|
||||
}
|
||||
|
||||
override fun onOpenMenu(item: Item, anchor: View) {
|
||||
|
|
|
@ -23,6 +23,7 @@ import android.view.ViewGroup
|
|||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
|
||||
import org.oxycblt.auxio.music.Genre
|
||||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.music.MusicMode
|
||||
import org.oxycblt.auxio.music.MusicParent
|
||||
import org.oxycblt.auxio.music.Sort
|
||||
|
@ -72,9 +73,9 @@ class GenreListFragment : HomeListFragment<Genre>() {
|
|||
}
|
||||
}
|
||||
|
||||
override fun onItemClick(item: Item) {
|
||||
check(item is Genre) { "Unexpected datatype: ${item::class.java}" }
|
||||
navModel.exploreNavigateTo(item)
|
||||
override fun onItemClick(music: Music) {
|
||||
check(music is Genre) { "Unexpected datatype: ${music::class.java}" }
|
||||
navModel.exploreNavigateTo(music)
|
||||
}
|
||||
|
||||
override fun onOpenMenu(item: Item, anchor: View) {
|
||||
|
|
|
@ -22,6 +22,8 @@ import android.view.LayoutInflater
|
|||
import androidx.fragment.app.Fragment
|
||||
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
|
||||
import org.oxycblt.auxio.home.HomeViewModel
|
||||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.music.MusicMode
|
||||
import org.oxycblt.auxio.ui.fastscroll.FastScrollRecyclerView
|
||||
import org.oxycblt.auxio.ui.fragment.MenuFragment
|
||||
import org.oxycblt.auxio.ui.recycler.Item
|
||||
|
@ -63,4 +65,25 @@ abstract class HomeListFragment<T : Item> :
|
|||
override fun onFastScrollStop() {
|
||||
homeModel.updateFastScrolling(false)
|
||||
}
|
||||
|
||||
abstract fun onItemClick(music: Music)
|
||||
|
||||
override fun onItemClick(item: Item) {
|
||||
check(item is Music) { "Unexpected datatype: ${item::class.java}" }
|
||||
if(homeModel.selected.value.isEmpty()) {
|
||||
onItemClick(item)
|
||||
} else {
|
||||
homeModel.select(item)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemLongClick(item: Item) {
|
||||
check(item is Music) { "Unexpected datatype: ${item::class.java}" }
|
||||
if (homeModel.selected.value.isEmpty()) {
|
||||
homeModel.select(item)
|
||||
} else {
|
||||
onItemClick(item)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ import android.view.ViewGroup
|
|||
import java.util.Formatter
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
|
||||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.music.MusicMode
|
||||
import org.oxycblt.auxio.music.MusicParent
|
||||
import org.oxycblt.auxio.music.Song
|
||||
|
@ -34,6 +35,7 @@ import org.oxycblt.auxio.settings.Settings
|
|||
import org.oxycblt.auxio.ui.recycler.PlayingIndicatorAdapter
|
||||
import org.oxycblt.auxio.ui.recycler.Item
|
||||
import org.oxycblt.auxio.ui.recycler.MenuItemListener
|
||||
import org.oxycblt.auxio.ui.recycler.SelectionIndicatorAdapter
|
||||
import org.oxycblt.auxio.ui.recycler.SongViewHolder
|
||||
import org.oxycblt.auxio.ui.recycler.SyncListDiffer
|
||||
import org.oxycblt.auxio.util.collectImmediately
|
||||
|
@ -58,6 +60,7 @@ class SongListFragment : HomeListFragment<Song>() {
|
|||
}
|
||||
|
||||
collectImmediately(homeModel.songs, homeAdapter::replaceList)
|
||||
collectImmediately(homeModel.selected, homeAdapter::updateSelection)
|
||||
collectImmediately(
|
||||
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::handlePlayback)
|
||||
}
|
||||
|
@ -104,12 +107,12 @@ class SongListFragment : HomeListFragment<Song>() {
|
|||
}
|
||||
}
|
||||
|
||||
override fun onItemClick(item: Item) {
|
||||
check(item is Song) { "Unexpected datatype: ${item::class.java}" }
|
||||
override fun onItemClick(music: Music) {
|
||||
check(music is Song) { "Unexpected datatype: ${music::class.java}" }
|
||||
when (settings.libPlaybackMode) {
|
||||
MusicMode.SONGS -> playbackModel.playFromAll(item)
|
||||
MusicMode.ALBUMS -> playbackModel.playFromAlbum(item)
|
||||
MusicMode.ARTISTS -> playbackModel.playFromArtist(item)
|
||||
MusicMode.SONGS -> playbackModel.playFromAll(music)
|
||||
MusicMode.ALBUMS -> playbackModel.playFromAlbum(music)
|
||||
MusicMode.ARTISTS -> playbackModel.playFromArtist(music)
|
||||
else -> error("Unexpected playback mode: ${settings.libPlaybackMode}")
|
||||
}
|
||||
}
|
||||
|
@ -129,7 +132,7 @@ class SongListFragment : HomeListFragment<Song>() {
|
|||
}
|
||||
|
||||
private class SongAdapter(private val listener: MenuItemListener) :
|
||||
PlayingIndicatorAdapter<SongViewHolder>() {
|
||||
SelectionIndicatorAdapter<SongViewHolder>() {
|
||||
private val differ = SyncListDiffer(this, SongViewHolder.DIFFER)
|
||||
|
||||
override val currentList: List<Item>
|
||||
|
|
|
@ -40,6 +40,9 @@ interface ItemClickListener {
|
|||
|
||||
/** An interface for detecting if an item has had it's menu opened. */
|
||||
interface MenuItemListener : ItemClickListener {
|
||||
/** Called when an item is long-clicked. */
|
||||
fun onItemLongClick(item: Item) {}
|
||||
|
||||
/** Called when an item desires to open a menu relating to it. */
|
||||
fun onOpenMenu(item: Item, anchor: View)
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package org.oxycblt.auxio.ui.recycler
|
|||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
||||
/**
|
||||
* An adapter that implements selection indicators.
|
||||
|
@ -23,14 +24,14 @@ abstract class SelectionIndicatorAdapter<VH : RecyclerView.ViewHolder> : Playing
|
|||
}
|
||||
}
|
||||
|
||||
fun updateSelection(items: Set<Music>) {
|
||||
fun updateSelection(items: List<Music>) {
|
||||
val oldSelectedItems = selectedItems
|
||||
|
||||
if (items == oldSelectedItems) {
|
||||
val newSelectedItems = items.toSet()
|
||||
if (newSelectedItems == oldSelectedItems) {
|
||||
return
|
||||
}
|
||||
|
||||
selectedItems = items
|
||||
selectedItems = newSelectedItems
|
||||
for (i in currentList.indices) {
|
||||
// TODO: Perhaps add an optimization that allows me to avoid the O(n) iteration
|
||||
// assuming all list items are unique?
|
||||
|
@ -39,14 +40,16 @@ abstract class SelectionIndicatorAdapter<VH : RecyclerView.ViewHolder> : Playing
|
|||
continue
|
||||
}
|
||||
|
||||
if (oldSelectedItems.contains(item) || items.contains(item)) {
|
||||
val added = !oldSelectedItems.contains(item) && newSelectedItems.contains(item)
|
||||
val removed = oldSelectedItems.contains(item) && !newSelectedItems.contains(item)
|
||||
if (added || removed) {
|
||||
notifyItemChanged(i, PAYLOAD_INDICATOR_CHANGED)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** A ViewHolder that can respond to selection indicator updates. */
|
||||
abstract class ViewHolder(root: View) : RecyclerView.ViewHolder(root) {
|
||||
abstract class ViewHolder(root: View) : PlayingIndicatorAdapter.ViewHolder(root) {
|
||||
abstract fun updateSelectionIndicator(isSelected: Boolean)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,17 +31,19 @@ import org.oxycblt.auxio.music.Song
|
|||
import org.oxycblt.auxio.util.context
|
||||
import org.oxycblt.auxio.util.getPlural
|
||||
import org.oxycblt.auxio.util.inflater
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
||||
/**
|
||||
* The shared ViewHolder for a [Song].
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class SongViewHolder private constructor(private val binding: ItemSongBinding) :
|
||||
PlayingIndicatorAdapter.ViewHolder(binding.root) {
|
||||
SelectionIndicatorAdapter.ViewHolder(binding.root) {
|
||||
fun bind(item: Song, listener: MenuItemListener) {
|
||||
binding.songAlbumCover.bind(item)
|
||||
binding.songName.text = item.resolveName(binding.context)
|
||||
binding.songInfo.text = item.resolveArtistContents(binding.context)
|
||||
|
||||
binding.songMenu.setOnClickListener { listener.onOpenMenu(item, it) }
|
||||
binding.root.setOnClickListener { listener.onItemClick(item) }
|
||||
}
|
||||
|
@ -51,6 +53,11 @@ class SongViewHolder private constructor(private val binding: ItemSongBinding) :
|
|||
binding.songAlbumCover.isPlaying = isPlaying
|
||||
}
|
||||
|
||||
override fun updateSelectionIndicator(isSelected: Boolean) {
|
||||
logD("Selected")
|
||||
binding.root.isActivated = isSelected
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val VIEW_TYPE = IntegerTable.VIEW_TYPE_SONG
|
||||
|
||||
|
|
Loading…
Reference in a new issue