ui: rework base adapter class
Completely rework the base adapter class to require less boilerplate and properly handle cases such as diffing. The major adapters have been migrated to this system, but the other adapters have not been changed so far. This is only part 1 of a multi-part rework, as this is an incredibly complex system.
This commit is contained in:
parent
595a982d59
commit
ee1a234e76
37 changed files with 1579 additions and 1303 deletions
|
@ -9,6 +9,7 @@
|
|||
- Updated translations [Konstantin Tutsch -> German, cccClyde -> Chinese ]
|
||||
- Switched to spotless and ktfmt instead of ktlint
|
||||
- Migrated constants to centralized table
|
||||
- Removed databinding [Greatly reduces compile times]
|
||||
- A bunch of internal view implementation improvements
|
||||
|
||||
## v2.2.2
|
||||
|
|
|
@ -28,7 +28,7 @@ import org.oxycblt.auxio.coil.GenreImageFetcher
|
|||
import org.oxycblt.auxio.coil.MusicKeyer
|
||||
import org.oxycblt.auxio.settings.SettingsManager
|
||||
|
||||
/** TODO: Rework RecyclerView management and item dragging */
|
||||
/** TODO: Rework null-safety/usage of requireNotNull */
|
||||
@Suppress("UNUSED")
|
||||
class AuxioApp : Application(), ImageLoaderFactory {
|
||||
override fun onCreate() {
|
||||
|
|
|
@ -29,8 +29,8 @@ object IntegerTable {
|
|||
const val ITEM_TYPE_GENRE = 0xA003
|
||||
/** HeaderViewHolder */
|
||||
const val ITEM_TYPE_HEADER = 0xA004
|
||||
/** ActionHeaderViewHolder */
|
||||
const val ITEM_TYPE_ACTION_HEADER = 0xA005
|
||||
/** SortHeaderViewHolder */
|
||||
const val ITEM_TYPE_SORT_HEADER = 0xA005
|
||||
|
||||
/** AlbumDetailViewHolder */
|
||||
const val ITEM_TYPE_ALBUM_DETAIL = 0xA006
|
||||
|
|
|
@ -19,6 +19,7 @@ package org.oxycblt.auxio.detail
|
|||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.core.view.children
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
|
@ -26,15 +27,17 @@ import androidx.recyclerview.widget.LinearSmoothScroller
|
|||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.FragmentDetailBinding
|
||||
import org.oxycblt.auxio.detail.recycler.AlbumDetailAdapter
|
||||
import org.oxycblt.auxio.music.ActionHeader
|
||||
import org.oxycblt.auxio.detail.recycler.AlbumDetailItemListener
|
||||
import org.oxycblt.auxio.detail.recycler.SortHeader
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.Header
|
||||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.playback.state.PlaybackMode
|
||||
import org.oxycblt.auxio.ui.ActionMenu
|
||||
import org.oxycblt.auxio.ui.Header
|
||||
import org.oxycblt.auxio.ui.Item
|
||||
import org.oxycblt.auxio.ui.newMenu
|
||||
import org.oxycblt.auxio.util.applySpans
|
||||
import org.oxycblt.auxio.util.canScroll
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.util.logW
|
||||
|
@ -44,28 +47,22 @@ import org.oxycblt.auxio.util.showToast
|
|||
* The [DetailFragment] for an album.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class AlbumDetailFragment : DetailFragment() {
|
||||
class AlbumDetailFragment : DetailFragment(), AlbumDetailItemListener {
|
||||
private val args: AlbumDetailFragmentArgs by navArgs()
|
||||
private val detailAdapter = AlbumDetailAdapter(this)
|
||||
|
||||
override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) {
|
||||
detailModel.setAlbumId(args.albumId)
|
||||
|
||||
val detailAdapter =
|
||||
AlbumDetailAdapter(
|
||||
playbackModel,
|
||||
detailModel,
|
||||
doOnClick = { playbackModel.playSong(it, PlaybackMode.IN_ALBUM) },
|
||||
doOnLongClick = { view, data -> newMenu(view, data, ActionMenu.FLAG_IN_ALBUM) })
|
||||
|
||||
setupToolbar(detailModel.curAlbum.value!!, R.menu.menu_album_detail) { itemId ->
|
||||
setupToolbar(detailModel.currentAlbum.value!!, R.menu.menu_album_detail) { itemId ->
|
||||
when (itemId) {
|
||||
R.id.action_play_next -> {
|
||||
playbackModel.playNext(detailModel.curAlbum.value!!)
|
||||
playbackModel.playNext(detailModel.currentAlbum.value!!)
|
||||
requireContext().showToast(R.string.lbl_queue_added)
|
||||
true
|
||||
}
|
||||
R.id.action_queue_add -> {
|
||||
playbackModel.addToQueue(detailModel.curAlbum.value!!)
|
||||
playbackModel.addToQueue(detailModel.currentAlbum.value!!)
|
||||
requireContext().showToast(R.string.lbl_queue_added)
|
||||
true
|
||||
}
|
||||
|
@ -73,20 +70,17 @@ class AlbumDetailFragment : DetailFragment() {
|
|||
}
|
||||
}
|
||||
|
||||
setupRecycler(detailAdapter) { pos ->
|
||||
val item = detailAdapter.currentList[pos]
|
||||
item is Header || item is ActionHeader || item is Album
|
||||
requireBinding().detailRecycler.apply {
|
||||
adapter = detailAdapter
|
||||
applySpans { pos ->
|
||||
val item = detailAdapter.currentList[pos]
|
||||
item is Header || item is SortHeader || item is Album
|
||||
}
|
||||
}
|
||||
|
||||
// -- VIEWMODEL SETUP ---
|
||||
|
||||
detailModel.albumData.observe(viewLifecycleOwner, detailAdapter::submitList)
|
||||
|
||||
detailModel.showMenu.observe(viewLifecycleOwner) { config ->
|
||||
if (config != null) {
|
||||
showMenu(config) { id -> id == R.id.option_sort_asc }
|
||||
}
|
||||
}
|
||||
detailModel.albumData.observe(viewLifecycleOwner) { list -> detailAdapter.submitList(list) }
|
||||
|
||||
detailModel.navToItem.observe(viewLifecycleOwner) { item ->
|
||||
handleNavigation(item, detailAdapter)
|
||||
|
@ -96,13 +90,46 @@ class AlbumDetailFragment : DetailFragment() {
|
|||
playbackModel.song.observe(viewLifecycleOwner) { song -> updateSong(song, detailAdapter) }
|
||||
}
|
||||
|
||||
override fun onItemClick(item: Item) {
|
||||
if (item is Song) {
|
||||
playbackModel.playSong(item, PlaybackMode.IN_ALBUM)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOpenMenu(item: Item, anchor: View) {
|
||||
newMenu(anchor, item)
|
||||
}
|
||||
|
||||
override fun onPlayParent() {
|
||||
playbackModel.playAlbum(requireNotNull(detailModel.currentAlbum.value), false)
|
||||
}
|
||||
|
||||
override fun onShuffleParent() {
|
||||
playbackModel.playAlbum(requireNotNull(detailModel.currentAlbum.value), true)
|
||||
}
|
||||
|
||||
override fun onShowSortMenu(anchor: View) {
|
||||
showSortMenu(
|
||||
anchor,
|
||||
detailModel.albumSort,
|
||||
onConfirm = { detailModel.albumSort = it },
|
||||
showItem = { it == R.id.option_sort_asc })
|
||||
}
|
||||
|
||||
override fun onNavigateToArtist() {
|
||||
findNavController()
|
||||
.navigate(
|
||||
AlbumDetailFragmentDirections.actionShowArtist(
|
||||
requireNotNull(detailModel.currentAlbum.value).artist.id))
|
||||
}
|
||||
|
||||
private fun handleNavigation(item: Music?, adapter: AlbumDetailAdapter) {
|
||||
val binding = requireBinding()
|
||||
when (item) {
|
||||
// Songs should be scrolled to if the album matches, or a new detail
|
||||
// fragment should be launched otherwise.
|
||||
is Song -> {
|
||||
if (detailModel.curAlbum.value!!.id == item.album.id) {
|
||||
if (detailModel.currentAlbum.value!!.id == item.album.id) {
|
||||
logD("Navigating to a song in this album")
|
||||
scrollToItem(item.id, adapter)
|
||||
detailModel.finishNavToItem()
|
||||
|
@ -116,7 +143,7 @@ class AlbumDetailFragment : DetailFragment() {
|
|||
// If the album matches, no need to do anything. Otherwise launch a new
|
||||
// detail fragment.
|
||||
is Album -> {
|
||||
if (detailModel.curAlbum.value!!.id == item.id) {
|
||||
if (detailModel.currentAlbum.value!!.id == item.id) {
|
||||
logD("Navigating to the top of this album")
|
||||
binding.detailRecycler.scrollToPosition(0)
|
||||
detailModel.finishNavToItem()
|
||||
|
@ -169,7 +196,7 @@ class AlbumDetailFragment : DetailFragment() {
|
|||
}
|
||||
|
||||
if (playbackModel.playbackMode.value == PlaybackMode.IN_ALBUM &&
|
||||
playbackModel.parent.value?.id == detailModel.curAlbum.value!!.id) {
|
||||
playbackModel.parent.value?.id == detailModel.currentAlbum.value!!.id) {
|
||||
adapter.highlightSong(song, binding.detailRecycler)
|
||||
} else {
|
||||
// Clear the ViewHolders if the mode isn't ALL_SONGS
|
||||
|
|
|
@ -18,21 +18,24 @@
|
|||
package org.oxycblt.auxio.detail
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.FragmentDetailBinding
|
||||
import org.oxycblt.auxio.detail.recycler.ArtistDetailAdapter
|
||||
import org.oxycblt.auxio.music.ActionHeader
|
||||
import org.oxycblt.auxio.detail.recycler.DetailItemListener
|
||||
import org.oxycblt.auxio.detail.recycler.SortHeader
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.Header
|
||||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.music.MusicParent
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.playback.state.PlaybackMode
|
||||
import org.oxycblt.auxio.ui.ActionMenu
|
||||
import org.oxycblt.auxio.ui.Header
|
||||
import org.oxycblt.auxio.ui.Item
|
||||
import org.oxycblt.auxio.ui.newMenu
|
||||
import org.oxycblt.auxio.util.applySpans
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.util.logW
|
||||
|
||||
|
@ -40,40 +43,27 @@ import org.oxycblt.auxio.util.logW
|
|||
* The [DetailFragment] for an artist.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class ArtistDetailFragment : DetailFragment() {
|
||||
class ArtistDetailFragment : DetailFragment(), DetailItemListener {
|
||||
private val args: ArtistDetailFragmentArgs by navArgs()
|
||||
private val detailAdapter = ArtistDetailAdapter(this)
|
||||
|
||||
override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) {
|
||||
detailModel.setArtistId(args.artistId)
|
||||
|
||||
val detailAdapter =
|
||||
ArtistDetailAdapter(
|
||||
playbackModel,
|
||||
doOnClick = { data ->
|
||||
if (!detailModel.isNavigating) {
|
||||
detailModel.setNavigating(true)
|
||||
findNavController()
|
||||
.navigate(ArtistDetailFragmentDirections.actionShowAlbum(data.id))
|
||||
}
|
||||
},
|
||||
doOnSongClick = { data -> playbackModel.playSong(data, PlaybackMode.IN_ARTIST) },
|
||||
doOnLongClick = { view, data -> newMenu(view, data, ActionMenu.FLAG_IN_ARTIST) })
|
||||
|
||||
setupToolbar(detailModel.currentArtist.value!!)
|
||||
setupRecycler(detailAdapter) { pos ->
|
||||
// If the item is an ActionHeader we need to also make the item full-width
|
||||
val item = detailAdapter.currentList[pos]
|
||||
item is Header || item is ActionHeader || item is Artist
|
||||
requireBinding().detailRecycler.apply {
|
||||
adapter = detailAdapter
|
||||
applySpans { pos ->
|
||||
// If the item is an ActionHeader we need to also make the item full-width
|
||||
val item = detailAdapter.currentList[pos]
|
||||
item is Header || item is SortHeader || item is Artist
|
||||
}
|
||||
}
|
||||
|
||||
// --- VIEWMODEL SETUP ---
|
||||
|
||||
detailModel.artistData.observe(viewLifecycleOwner, detailAdapter::submitList)
|
||||
|
||||
detailModel.showMenu.observe(viewLifecycleOwner) { config ->
|
||||
if (config != null) {
|
||||
showMenu(config) { id -> id != R.id.option_sort_artist }
|
||||
}
|
||||
detailModel.artistData.observe(viewLifecycleOwner) { list ->
|
||||
detailAdapter.submitList(list)
|
||||
}
|
||||
|
||||
detailModel.navToItem.observe(viewLifecycleOwner, ::handleNavigation)
|
||||
|
@ -87,6 +77,35 @@ class ArtistDetailFragment : DetailFragment() {
|
|||
}
|
||||
}
|
||||
|
||||
override fun onItemClick(item: Item) {
|
||||
when (item) {
|
||||
is Song -> playbackModel.playSong(item, PlaybackMode.IN_ARTIST)
|
||||
is Album ->
|
||||
findNavController()
|
||||
.navigate(ArtistDetailFragmentDirections.actionShowAlbum(item.id))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOpenMenu(item: Item, anchor: View) {
|
||||
newMenu(anchor, item)
|
||||
}
|
||||
|
||||
override fun onPlayParent() {
|
||||
playbackModel.playArtist(requireNotNull(detailModel.currentArtist.value), false)
|
||||
}
|
||||
|
||||
override fun onShuffleParent() {
|
||||
playbackModel.playArtist(requireNotNull(detailModel.currentArtist.value), true)
|
||||
}
|
||||
|
||||
override fun onShowSortMenu(anchor: View) {
|
||||
showSortMenu(
|
||||
anchor,
|
||||
detailModel.artistSort,
|
||||
onConfirm = { detailModel.artistSort = it },
|
||||
showItem = { id -> id != R.id.option_sort_artist })
|
||||
}
|
||||
|
||||
private fun handleNavigation(item: Music?) {
|
||||
val binding = requireBinding()
|
||||
|
||||
|
|
|
@ -18,19 +18,19 @@
|
|||
package org.oxycblt.auxio.detail
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import androidx.annotation.MenuRes
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.core.view.children
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.FragmentDetailBinding
|
||||
import org.oxycblt.auxio.music.MusicParent
|
||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||
import org.oxycblt.auxio.ui.Sort
|
||||
import org.oxycblt.auxio.ui.ViewBindingFragment
|
||||
import org.oxycblt.auxio.util.applySpans
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
||||
/**
|
||||
|
@ -49,10 +49,9 @@ abstract class DetailFragment : ViewBindingFragment<FragmentDetailBinding>() {
|
|||
detailModel.setNavigating(false)
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
// Cancel all pending menus when this fragment stops to prevent bugs/crashes
|
||||
detailModel.finishShowMenu(null)
|
||||
override fun onDestroyBinding(binding: FragmentDetailBinding) {
|
||||
super.onDestroyBinding(binding)
|
||||
binding.detailRecycler.adapter = null
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -81,55 +80,44 @@ abstract class DetailFragment : ViewBindingFragment<FragmentDetailBinding>() {
|
|||
}
|
||||
}
|
||||
|
||||
/** Shortcut method for recyclerview setup */
|
||||
protected fun setupRecycler(
|
||||
detailAdapter: RecyclerView.Adapter<RecyclerView.ViewHolder>,
|
||||
gridLookup: (Int) -> Boolean
|
||||
) {
|
||||
requireBinding().detailRecycler.apply {
|
||||
adapter = detailAdapter
|
||||
setHasFixedSize(true)
|
||||
applySpans(gridLookup)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shortcut method for spinning up the sorting [PopupMenu]
|
||||
* @param config The initial configuration to apply to the menu. This is provided by
|
||||
* [DetailViewModel.showMenu].
|
||||
* @param anchor The view to anchor the sort menu to
|
||||
* @param sort The initial sort
|
||||
* @param onConfirm What to do when the sort is confirmed
|
||||
* @param showItem Which menu items to keep
|
||||
*/
|
||||
protected fun showMenu(
|
||||
config: DetailViewModel.MenuConfig,
|
||||
showItem: ((Int) -> Boolean)? = null
|
||||
protected fun showSortMenu(
|
||||
anchor: View,
|
||||
sort: Sort,
|
||||
onConfirm: (Sort) -> Unit,
|
||||
showItem: ((Int) -> Boolean)? = null,
|
||||
) {
|
||||
logD("Launching menu [$config]")
|
||||
logD("Launching menu")
|
||||
|
||||
PopupMenu(config.anchor.context, config.anchor).apply {
|
||||
PopupMenu(anchor.context, anchor).apply {
|
||||
inflate(R.menu.menu_detail_sort)
|
||||
|
||||
setOnMenuItemClickListener { item ->
|
||||
if (item.itemId == R.id.option_sort_asc) {
|
||||
item.isChecked = !item.isChecked
|
||||
detailModel.finishShowMenu(config.sortMode.ascending(item.isChecked))
|
||||
onConfirm(sort.ascending(item.isChecked))
|
||||
} else {
|
||||
item.isChecked = true
|
||||
detailModel.finishShowMenu(config.sortMode.assignId(item.itemId))
|
||||
onConfirm(requireNotNull(sort.assignId(item.itemId)))
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
setOnDismissListener { detailModel.finishShowMenu(null) }
|
||||
|
||||
if (showItem != null) {
|
||||
for (item in menu.children) {
|
||||
item.isVisible = showItem(item.itemId)
|
||||
}
|
||||
}
|
||||
|
||||
menu.findItem(config.sortMode.itemId).isChecked = true
|
||||
menu.findItem(R.id.option_sort_asc).isChecked = config.sortMode.isAscending
|
||||
menu.findItem(sort.itemId).isChecked = true
|
||||
menu.findItem(R.id.option_sort_asc).isChecked = sort.isAscending
|
||||
|
||||
show()
|
||||
}
|
||||
|
|
|
@ -17,21 +17,19 @@
|
|||
|
||||
package org.oxycblt.auxio.detail
|
||||
|
||||
import android.view.View
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.music.ActionHeader
|
||||
import org.oxycblt.auxio.detail.recycler.SortHeader
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.Genre
|
||||
import org.oxycblt.auxio.music.Header
|
||||
import org.oxycblt.auxio.music.Item
|
||||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.music.MusicStore
|
||||
import org.oxycblt.auxio.settings.SettingsManager
|
||||
import org.oxycblt.auxio.ui.DisplayMode
|
||||
import org.oxycblt.auxio.ui.Header
|
||||
import org.oxycblt.auxio.ui.Item
|
||||
import org.oxycblt.auxio.ui.Sort
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
||||
|
@ -44,14 +42,23 @@ import org.oxycblt.auxio.util.logD
|
|||
* @author OxygenCobalt
|
||||
*/
|
||||
class DetailViewModel : ViewModel() {
|
||||
private val settingsManager = SettingsManager.getInstance()
|
||||
|
||||
private val mCurrentAlbum = MutableLiveData<Album?>()
|
||||
val curAlbum: LiveData<Album?>
|
||||
val currentAlbum: LiveData<Album?>
|
||||
get() = mCurrentAlbum
|
||||
|
||||
private val mAlbumData = MutableLiveData(listOf<Item>())
|
||||
val albumData: LiveData<List<Item>>
|
||||
get() = mAlbumData
|
||||
|
||||
var albumSort: Sort
|
||||
get() = settingsManager.detailAlbumSort
|
||||
set(value) {
|
||||
settingsManager.detailAlbumSort = value
|
||||
refreshAlbumData()
|
||||
}
|
||||
|
||||
private val mCurrentArtist = MutableLiveData<Artist?>()
|
||||
val currentArtist: LiveData<Artist?>
|
||||
get() = mCurrentArtist
|
||||
|
@ -59,6 +66,13 @@ class DetailViewModel : ViewModel() {
|
|||
private val mArtistData = MutableLiveData(listOf<Item>())
|
||||
val artistData: LiveData<List<Item>> = mArtistData
|
||||
|
||||
var artistSort: Sort
|
||||
get() = settingsManager.detailArtistSort
|
||||
set(value) {
|
||||
settingsManager.detailArtistSort = value
|
||||
refreshArtistData()
|
||||
}
|
||||
|
||||
private val mCurrentGenre = MutableLiveData<Genre?>()
|
||||
val currentGenre: LiveData<Genre?>
|
||||
get() = mCurrentGenre
|
||||
|
@ -66,10 +80,12 @@ class DetailViewModel : ViewModel() {
|
|||
private val mGenreData = MutableLiveData(listOf<Item>())
|
||||
val genreData: LiveData<List<Item>> = mGenreData
|
||||
|
||||
data class MenuConfig(val anchor: View, val sortMode: Sort)
|
||||
|
||||
private val mShowMenu = MutableLiveData<MenuConfig?>(null)
|
||||
val showMenu: LiveData<MenuConfig?> = mShowMenu
|
||||
var genreSort: Sort
|
||||
get() = settingsManager.detailGenreSort
|
||||
set(value) {
|
||||
settingsManager.detailGenreSort = value
|
||||
refreshGenreData()
|
||||
}
|
||||
|
||||
private val mNavToItem = MutableLiveData<Music?>()
|
||||
|
||||
|
@ -80,9 +96,6 @@ class DetailViewModel : ViewModel() {
|
|||
var isNavigating = false
|
||||
private set
|
||||
|
||||
private var currentMenuContext: DisplayMode? = null
|
||||
private val settingsManager = SettingsManager.getInstance()
|
||||
|
||||
fun setAlbumId(id: Long) {
|
||||
if (mCurrentAlbum.value?.id == id) return
|
||||
val musicStore = MusicStore.requireInstance()
|
||||
|
@ -104,32 +117,6 @@ class DetailViewModel : ViewModel() {
|
|||
refreshGenreData()
|
||||
}
|
||||
|
||||
/** Mark that the menu process is done with the new [Sort]. Pass null if there was no change. */
|
||||
fun finishShowMenu(newMode: Sort?) {
|
||||
mShowMenu.value = null
|
||||
|
||||
if (newMode != null) {
|
||||
logD("Applying new sort mode")
|
||||
when (currentMenuContext) {
|
||||
DisplayMode.SHOW_ALBUMS -> {
|
||||
settingsManager.detailAlbumSort = newMode
|
||||
refreshAlbumData()
|
||||
}
|
||||
DisplayMode.SHOW_ARTISTS -> {
|
||||
settingsManager.detailArtistSort = newMode
|
||||
refreshArtistData()
|
||||
}
|
||||
DisplayMode.SHOW_GENRES -> {
|
||||
settingsManager.detailGenreSort = newMode
|
||||
refreshGenreData()
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
currentMenuContext = null
|
||||
}
|
||||
|
||||
/** Navigate to an item, whether a song/album/artist */
|
||||
fun navToItem(item: Music) {
|
||||
mNavToItem.value = item
|
||||
|
@ -150,17 +137,7 @@ class DetailViewModel : ViewModel() {
|
|||
val genre = requireNotNull(currentGenre.value)
|
||||
val data = mutableListOf<Item>(genre)
|
||||
|
||||
data.add(
|
||||
ActionHeader(
|
||||
id = -2,
|
||||
string = R.string.lbl_songs,
|
||||
icon = R.drawable.ic_sort,
|
||||
desc = R.string.lbl_sort,
|
||||
onClick = { view ->
|
||||
currentMenuContext = DisplayMode.SHOW_GENRES
|
||||
mShowMenu.value = MenuConfig(view, settingsManager.detailGenreSort)
|
||||
}))
|
||||
|
||||
data.add(SortHeader(-2, R.string.lbl_songs))
|
||||
data.addAll(settingsManager.detailGenreSort.genre(currentGenre.value!!))
|
||||
|
||||
mGenreData.value = data
|
||||
|
@ -171,21 +148,9 @@ class DetailViewModel : ViewModel() {
|
|||
val artist = requireNotNull(currentArtist.value)
|
||||
val data = mutableListOf<Item>(artist)
|
||||
|
||||
data.add(Header(id = -2, string = R.string.lbl_albums))
|
||||
|
||||
data.add(Header(-2, R.string.lbl_albums))
|
||||
data.addAll(Sort.ByYear(false).albums(artist.albums))
|
||||
|
||||
data.add(
|
||||
ActionHeader(
|
||||
id = -3,
|
||||
string = R.string.lbl_songs,
|
||||
icon = R.drawable.ic_sort,
|
||||
desc = R.string.lbl_sort,
|
||||
onClick = { view ->
|
||||
currentMenuContext = DisplayMode.SHOW_ARTISTS
|
||||
mShowMenu.value = MenuConfig(view, settingsManager.detailArtistSort)
|
||||
}))
|
||||
|
||||
data.add(SortHeader(-3, R.string.lbl_songs))
|
||||
data.addAll(settingsManager.detailArtistSort.artist(artist))
|
||||
|
||||
mArtistData.value = data.toList()
|
||||
|
@ -193,21 +158,11 @@ class DetailViewModel : ViewModel() {
|
|||
|
||||
private fun refreshAlbumData() {
|
||||
logD("Refreshing album data")
|
||||
val album = requireNotNull(curAlbum.value)
|
||||
val album = requireNotNull(currentAlbum.value)
|
||||
val data = mutableListOf<Item>(album)
|
||||
|
||||
data.add(
|
||||
ActionHeader(
|
||||
id = -2,
|
||||
string = R.string.lbl_songs,
|
||||
icon = R.drawable.ic_sort,
|
||||
desc = R.string.lbl_sort,
|
||||
onClick = { view ->
|
||||
currentMenuContext = DisplayMode.SHOW_ALBUMS
|
||||
mShowMenu.value = MenuConfig(view, settingsManager.detailAlbumSort)
|
||||
}))
|
||||
|
||||
data.addAll(settingsManager.detailAlbumSort.album(curAlbum.value!!))
|
||||
data.add(SortHeader(id = -2, R.string.lbl_albums))
|
||||
data.addAll(settingsManager.detailAlbumSort.album(currentAlbum.value!!))
|
||||
|
||||
mAlbumData.value = data
|
||||
}
|
||||
|
|
|
@ -18,20 +18,23 @@
|
|||
package org.oxycblt.auxio.detail
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import org.oxycblt.auxio.databinding.FragmentDetailBinding
|
||||
import org.oxycblt.auxio.detail.recycler.DetailItemListener
|
||||
import org.oxycblt.auxio.detail.recycler.GenreDetailAdapter
|
||||
import org.oxycblt.auxio.music.ActionHeader
|
||||
import org.oxycblt.auxio.detail.recycler.SortHeader
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.Genre
|
||||
import org.oxycblt.auxio.music.Header
|
||||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.playback.state.PlaybackMode
|
||||
import org.oxycblt.auxio.ui.ActionMenu
|
||||
import org.oxycblt.auxio.ui.Header
|
||||
import org.oxycblt.auxio.ui.Item
|
||||
import org.oxycblt.auxio.ui.newMenu
|
||||
import org.oxycblt.auxio.util.applySpans
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.util.logW
|
||||
|
||||
|
@ -39,39 +42,56 @@ import org.oxycblt.auxio.util.logW
|
|||
* The [DetailFragment] for a genre.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class GenreDetailFragment : DetailFragment() {
|
||||
class GenreDetailFragment : DetailFragment(), DetailItemListener {
|
||||
private val args: GenreDetailFragmentArgs by navArgs()
|
||||
private val detailAdapter = GenreDetailAdapter(this)
|
||||
|
||||
override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) {
|
||||
detailModel.setGenreId(args.genreId)
|
||||
|
||||
val detailAdapter =
|
||||
GenreDetailAdapter(
|
||||
playbackModel,
|
||||
doOnClick = { song -> playbackModel.playSong(song, PlaybackMode.IN_GENRE) },
|
||||
doOnLongClick = { view, data -> newMenu(view, data, ActionMenu.FLAG_IN_GENRE) })
|
||||
|
||||
setupToolbar(detailModel.currentGenre.value!!)
|
||||
setupRecycler(detailAdapter) { pos ->
|
||||
val item = detailAdapter.currentList[pos]
|
||||
item is Header || item is ActionHeader || item is Genre
|
||||
binding.detailRecycler.apply {
|
||||
adapter = detailAdapter
|
||||
applySpans { pos ->
|
||||
val item = detailAdapter.currentList[pos]
|
||||
item is Header || item is SortHeader || item is Genre
|
||||
}
|
||||
}
|
||||
|
||||
// --- VIEWMODEL SETUP ---
|
||||
|
||||
detailModel.genreData.observe(viewLifecycleOwner, detailAdapter::submitList)
|
||||
detailModel.genreData.observe(viewLifecycleOwner) { list -> detailAdapter.submitList(list) }
|
||||
|
||||
detailModel.navToItem.observe(viewLifecycleOwner, ::handleNavigation)
|
||||
|
||||
playbackModel.song.observe(viewLifecycleOwner) { song -> updateSong(song, detailAdapter) }
|
||||
}
|
||||
|
||||
detailModel.showMenu.observe(viewLifecycleOwner) { config ->
|
||||
if (config != null) {
|
||||
showMenu(config)
|
||||
}
|
||||
override fun onItemClick(item: Item) {
|
||||
when (item) {
|
||||
is Song -> playbackModel.playSong(item, PlaybackMode.IN_GENRE)
|
||||
is Album ->
|
||||
findNavController()
|
||||
.navigate(ArtistDetailFragmentDirections.actionShowAlbum(item.id))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOpenMenu(item: Item, anchor: View) {
|
||||
newMenu(anchor, item)
|
||||
}
|
||||
|
||||
override fun onPlayParent() {
|
||||
playbackModel.playGenre(requireNotNull(detailModel.currentGenre.value), false)
|
||||
}
|
||||
|
||||
override fun onShuffleParent() {
|
||||
playbackModel.playGenre(requireNotNull(detailModel.currentGenre.value), true)
|
||||
}
|
||||
|
||||
override fun onShowSortMenu(anchor: View) {
|
||||
showSortMenu(anchor, detailModel.genreSort, onConfirm = { detailModel.genreSort = it })
|
||||
}
|
||||
|
||||
private fun handleNavigation(item: Music?) {
|
||||
when (item) {
|
||||
// All items will launch new detail fragments.
|
||||
|
|
|
@ -17,82 +17,71 @@
|
|||
|
||||
package org.oxycblt.auxio.detail.recycler
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.content.Context
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.oxycblt.auxio.IntegerTable
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.coil.bindAlbumCover
|
||||
import org.oxycblt.auxio.databinding.ItemAlbumSongBinding
|
||||
import org.oxycblt.auxio.databinding.ItemDetailBinding
|
||||
import org.oxycblt.auxio.detail.DetailViewModel
|
||||
import org.oxycblt.auxio.music.ActionHeader
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Item
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.music.toDuration
|
||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||
import org.oxycblt.auxio.ui.ActionHeaderViewHolder
|
||||
import org.oxycblt.auxio.ui.BaseViewHolder
|
||||
import org.oxycblt.auxio.ui.DiffCallback
|
||||
import org.oxycblt.auxio.ui.BindingViewHolder
|
||||
import org.oxycblt.auxio.ui.Item
|
||||
import org.oxycblt.auxio.ui.ItemDiffCallback
|
||||
import org.oxycblt.auxio.ui.MenuItemListener
|
||||
import org.oxycblt.auxio.util.getPluralSafe
|
||||
import org.oxycblt.auxio.util.inflater
|
||||
import org.oxycblt.auxio.util.textSafe
|
||||
|
||||
/**
|
||||
* An adapter for displaying the details and [Song]s of an [Album]
|
||||
* An adapter for displaying [Album] information and it's children.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class AlbumDetailAdapter(
|
||||
private val playbackModel: PlaybackViewModel,
|
||||
private val detailModel: DetailViewModel,
|
||||
private val doOnClick: (data: Song) -> Unit,
|
||||
private val doOnLongClick: (view: View, data: Song) -> Unit
|
||||
) : ListAdapter<Item, RecyclerView.ViewHolder>(DiffCallback()) {
|
||||
private var currentSong: Song? = null
|
||||
private var currentHolder: Highlightable? = null
|
||||
class AlbumDetailAdapter(listener: AlbumDetailItemListener) :
|
||||
DetailAdapter<AlbumDetailItemListener>(listener, DIFFER) {
|
||||
private var highlightedSong: Song? = null
|
||||
private var highlightedViewHolder: Highlightable? = null
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return when (getItem(position)) {
|
||||
is Album -> IntegerTable.ITEM_TYPE_ALBUM_DETAIL
|
||||
is ActionHeader -> IntegerTable.ITEM_TYPE_ACTION_HEADER
|
||||
is Song -> IntegerTable.ITEM_TYPE_ALBUM_SONG
|
||||
else -> -1
|
||||
}
|
||||
}
|
||||
override fun getCreatorFromItem(item: Item) =
|
||||
super.getCreatorFromItem(item)
|
||||
?: when (item) {
|
||||
is Album -> AlbumDetailViewHolder.CREATOR
|
||||
is Song -> AlbumSongViewHolder.CREATOR
|
||||
else -> null
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return when (viewType) {
|
||||
IntegerTable.ITEM_TYPE_ALBUM_DETAIL ->
|
||||
AlbumDetailViewHolder(ItemDetailBinding.inflate(parent.context.inflater))
|
||||
IntegerTable.ITEM_TYPE_ACTION_HEADER -> ActionHeaderViewHolder.from(parent.context)
|
||||
IntegerTable.ITEM_TYPE_ALBUM_SONG ->
|
||||
AlbumSongViewHolder(ItemAlbumSongBinding.inflate(parent.context.inflater))
|
||||
else -> error("Invalid ViewHolder item type $viewType")
|
||||
}
|
||||
}
|
||||
override fun getCreatorFromViewType(viewType: Int) =
|
||||
super.getCreatorFromViewType(viewType)
|
||||
?: when (viewType) {
|
||||
AlbumDetailViewHolder.CREATOR.viewType -> AlbumDetailViewHolder.CREATOR
|
||||
AlbumSongViewHolder.CREATOR.viewType -> AlbumSongViewHolder.CREATOR
|
||||
else -> null
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
val item = getItem(position)
|
||||
override fun onBind(
|
||||
viewHolder: RecyclerView.ViewHolder,
|
||||
item: Item,
|
||||
listener: AlbumDetailItemListener
|
||||
) {
|
||||
super.onBind(viewHolder, item, listener)
|
||||
|
||||
when (item) {
|
||||
is Album -> (holder as AlbumDetailViewHolder).bind(item)
|
||||
is Song -> (holder as AlbumSongViewHolder).bind(item)
|
||||
is ActionHeader -> (holder as ActionHeaderViewHolder).bind(item)
|
||||
else -> {}
|
||||
is Album -> (viewHolder as AlbumDetailViewHolder).bind(item, listener)
|
||||
is Song -> (viewHolder as AlbumSongViewHolder).bind(item, listener)
|
||||
}
|
||||
}
|
||||
|
||||
if (holder is Highlightable) {
|
||||
if (item.id == currentSong?.id) {
|
||||
// Reset the last ViewHolder before assigning the new, correct one to be highlighted
|
||||
currentHolder?.setHighlighted(false)
|
||||
currentHolder = holder
|
||||
holder.setHighlighted(true)
|
||||
} else {
|
||||
holder.setHighlighted(false)
|
||||
}
|
||||
override fun onHighlightViewHolder(viewHolder: Highlightable, item: Item) {
|
||||
if (item is Song && item.id == highlightedSong?.id) {
|
||||
// Reset the last ViewHolder before assigning the new, correct one to be highlighted
|
||||
highlightedViewHolder?.setHighlighted(false)
|
||||
highlightedViewHolder = viewHolder
|
||||
viewHolder.setHighlighted(true)
|
||||
} else {
|
||||
viewHolder.setHighlighted(false)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -101,89 +90,134 @@ class AlbumDetailAdapter(
|
|||
* @param recycler The recyclerview the highlighting should act on.
|
||||
*/
|
||||
fun highlightSong(song: Song?, recycler: RecyclerView) {
|
||||
if (song == currentSong) return // Already highlighting this ViewHolder
|
||||
|
||||
// Clear the current ViewHolder since it's invalid
|
||||
currentHolder?.setHighlighted(false)
|
||||
currentHolder = null
|
||||
currentSong = song
|
||||
|
||||
if (song != null) {
|
||||
// Use existing data instead of having to re-sort it.
|
||||
val pos = currentList.indexOfFirst { item -> item.id == song.id && item is Song }
|
||||
|
||||
// Check if the ViewHolder for this song is visible, if it is then highlight it.
|
||||
// If the ViewHolder is not visible, then the adapter should take care of it if
|
||||
// it does become visible.
|
||||
recycler.layoutManager?.findViewByPosition(pos)?.let { child ->
|
||||
recycler.getChildViewHolder(child)?.let {
|
||||
currentHolder = it as Highlightable
|
||||
currentHolder?.setHighlighted(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (song == highlightedSong) return
|
||||
highlightedSong = song
|
||||
highlightedViewHolder?.setHighlighted(false)
|
||||
highlightedViewHolder = highlightItem(song, recycler)
|
||||
}
|
||||
|
||||
inner class AlbumDetailViewHolder(private val binding: ItemDetailBinding) :
|
||||
BaseViewHolder<Album>(binding) {
|
||||
|
||||
override fun onBind(data: Album) {
|
||||
binding.detailCover.apply {
|
||||
bindAlbumCover(data)
|
||||
contentDescription = context.getString(R.string.desc_album_cover, data.resolvedName)
|
||||
}
|
||||
|
||||
binding.detailName.textSafe = data.resolvedName
|
||||
|
||||
binding.detailSubhead.apply {
|
||||
textSafe = data.artist.resolvedName
|
||||
setOnClickListener { detailModel.navToItem(data.artist) }
|
||||
}
|
||||
|
||||
binding.detailInfo.apply {
|
||||
text =
|
||||
context.getString(
|
||||
R.string.fmt_three,
|
||||
data.year?.toString() ?: context.getString(R.string.def_date),
|
||||
context.getPluralSafe(R.plurals.fmt_song_count, data.songs.size),
|
||||
data.totalDuration)
|
||||
}
|
||||
|
||||
binding.detailPlayButton.setOnClickListener { playbackModel.playAlbum(data, false) }
|
||||
|
||||
binding.detailShuffleButton.setOnClickListener { playbackModel.playAlbum(data, true) }
|
||||
}
|
||||
}
|
||||
|
||||
inner class AlbumSongViewHolder(
|
||||
private val binding: ItemAlbumSongBinding,
|
||||
) : BaseViewHolder<Song>(binding, doOnClick, doOnLongClick), Highlightable {
|
||||
override fun onBind(data: Song) {
|
||||
// Hide the track number view if the song does not have a track.
|
||||
if (data.track != null) {
|
||||
binding.songTrack.apply {
|
||||
textSafe = context.getString(R.string.fmt_number, data.track)
|
||||
isInvisible = false
|
||||
companion object {
|
||||
private val DIFFER =
|
||||
object : ItemDiffCallback<Item>() {
|
||||
override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
|
||||
return when {
|
||||
oldItem is Album && newItem is Album ->
|
||||
AlbumDetailViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
|
||||
oldItem is SortHeader && newItem is SortHeader ->
|
||||
SortHeaderViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
|
||||
oldItem is Song && newItem is Song ->
|
||||
AlbumSongViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
|
||||
else -> DetailAdapter.DIFFER.areItemsTheSame(oldItem, newItem)
|
||||
}
|
||||
}
|
||||
|
||||
binding.songTrackPlaceholder.isInvisible = true
|
||||
} else {
|
||||
binding.songTrack.apply {
|
||||
textSafe = ""
|
||||
isInvisible = true
|
||||
}
|
||||
|
||||
binding.songTrackPlaceholder.isInvisible = false
|
||||
}
|
||||
|
||||
binding.songName.textSafe = data.resolvedName
|
||||
binding.songDuration.textSafe = data.seconds.toDuration(false)
|
||||
}
|
||||
|
||||
override fun setHighlighted(isHighlighted: Boolean) {
|
||||
binding.songName.isActivated = isHighlighted
|
||||
binding.songTrack.isActivated = isHighlighted
|
||||
binding.songTrackPlaceholder.isActivated = isHighlighted
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface AlbumDetailItemListener : DetailItemListener {
|
||||
fun onNavigateToArtist()
|
||||
}
|
||||
|
||||
private class AlbumDetailViewHolder private constructor(private val binding: ItemDetailBinding) :
|
||||
BindingViewHolder<Album, AlbumDetailItemListener>(binding.root) {
|
||||
|
||||
override fun bind(item: Album, listener: AlbumDetailItemListener) {
|
||||
binding.detailCover.bindAlbumCover(item)
|
||||
binding.detailName.textSafe = item.resolvedName
|
||||
|
||||
binding.detailSubhead.apply {
|
||||
textSafe = item.resolvedArtistName
|
||||
setOnClickListener { listener.onNavigateToArtist() }
|
||||
}
|
||||
|
||||
binding.detailInfo.apply {
|
||||
text =
|
||||
context.getString(
|
||||
R.string.fmt_three,
|
||||
item.year?.toString() ?: context.getString(R.string.def_date),
|
||||
context.getPluralSafe(R.plurals.fmt_song_count, item.songs.size),
|
||||
item.totalDuration)
|
||||
}
|
||||
|
||||
binding.detailPlayButton.setOnClickListener { listener.onPlayParent() }
|
||||
binding.detailShuffleButton.setOnClickListener { listener.onShuffleParent() }
|
||||
}
|
||||
|
||||
companion object {
|
||||
val CREATOR =
|
||||
object : Creator<AlbumDetailViewHolder> {
|
||||
override val viewType: Int
|
||||
get() = IntegerTable.ITEM_TYPE_ALBUM_DETAIL
|
||||
|
||||
override fun create(context: Context) =
|
||||
AlbumDetailViewHolder(ItemDetailBinding.inflate(context.inflater))
|
||||
}
|
||||
|
||||
val DIFFER =
|
||||
object : ItemDiffCallback<Album>() {
|
||||
override fun areItemsTheSame(oldItem: Album, newItem: Album) =
|
||||
oldItem.resolvedName == newItem.resolvedName &&
|
||||
oldItem.resolvedArtistName == newItem.resolvedArtistName &&
|
||||
oldItem.year == newItem.year &&
|
||||
oldItem.songs.size == newItem.songs.size &&
|
||||
oldItem.totalDuration == newItem.totalDuration
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class AlbumSongViewHolder private constructor(private val binding: ItemAlbumSongBinding) :
|
||||
BindingViewHolder<Song, MenuItemListener>(binding.root), Highlightable {
|
||||
override fun bind(item: Song, listener: MenuItemListener) {
|
||||
// Hide the track number view if the song does not have a track.
|
||||
if (item.track != null) {
|
||||
binding.songTrack.apply {
|
||||
textSafe = context.getString(R.string.fmt_number, item.track)
|
||||
isInvisible = false
|
||||
}
|
||||
|
||||
binding.songTrackPlaceholder.isInvisible = true
|
||||
} else {
|
||||
binding.songTrack.apply {
|
||||
textSafe = ""
|
||||
isInvisible = true
|
||||
}
|
||||
|
||||
binding.songTrackPlaceholder.isInvisible = false
|
||||
}
|
||||
|
||||
binding.songName.textSafe = item.resolvedName
|
||||
binding.songDuration.textSafe = item.seconds.toDuration(false)
|
||||
|
||||
binding.root.apply {
|
||||
setOnClickListener { listener.onItemClick(item) }
|
||||
setOnLongClickListener { view ->
|
||||
listener.onOpenMenu(item, view)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun setHighlighted(isHighlighted: Boolean) {
|
||||
binding.songName.isActivated = isHighlighted
|
||||
binding.songTrack.isActivated = isHighlighted
|
||||
binding.songTrackPlaceholder.isActivated = isHighlighted
|
||||
}
|
||||
|
||||
companion object {
|
||||
val CREATOR =
|
||||
object : Creator<AlbumSongViewHolder> {
|
||||
override val viewType: Int
|
||||
get() = IntegerTable.ITEM_TYPE_ALBUM_SONG
|
||||
|
||||
override fun create(context: Context) =
|
||||
AlbumSongViewHolder(ItemAlbumSongBinding.inflate(context.inflater))
|
||||
}
|
||||
|
||||
val DIFFER =
|
||||
object : ItemDiffCallback<Song>() {
|
||||
override fun areItemsTheSame(oldItem: Song, newItem: Song) =
|
||||
oldItem.resolvedName == newItem.resolvedName &&
|
||||
oldItem.duration == newItem.duration
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,9 +17,7 @@
|
|||
|
||||
package org.oxycblt.auxio.detail.recycler
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import android.content.Context
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.oxycblt.auxio.IntegerTable
|
||||
import org.oxycblt.auxio.R
|
||||
|
@ -28,88 +26,76 @@ import org.oxycblt.auxio.coil.bindArtistImage
|
|||
import org.oxycblt.auxio.databinding.ItemDetailBinding
|
||||
import org.oxycblt.auxio.databinding.ItemParentBinding
|
||||
import org.oxycblt.auxio.databinding.ItemSongBinding
|
||||
import org.oxycblt.auxio.music.ActionHeader
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.Header
|
||||
import org.oxycblt.auxio.music.Item
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||
import org.oxycblt.auxio.ui.ActionHeaderViewHolder
|
||||
import org.oxycblt.auxio.ui.BaseViewHolder
|
||||
import org.oxycblt.auxio.ui.DiffCallback
|
||||
import org.oxycblt.auxio.ui.HeaderViewHolder
|
||||
import org.oxycblt.auxio.ui.ArtistViewHolder
|
||||
import org.oxycblt.auxio.ui.BindingViewHolder
|
||||
import org.oxycblt.auxio.ui.Item
|
||||
import org.oxycblt.auxio.ui.ItemDiffCallback
|
||||
import org.oxycblt.auxio.ui.MenuItemListener
|
||||
import org.oxycblt.auxio.util.context
|
||||
import org.oxycblt.auxio.util.getPluralSafe
|
||||
import org.oxycblt.auxio.util.inflater
|
||||
import org.oxycblt.auxio.util.textSafe
|
||||
|
||||
/**
|
||||
* An adapter for displaying the [Album]s and [Song]s of an artist.
|
||||
* An adapter for displaying [Artist] information and it's children. Unlike the other adapters, this
|
||||
* one actually contains both album information and song information.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class ArtistDetailAdapter(
|
||||
private val playbackModel: PlaybackViewModel,
|
||||
private val doOnClick: (data: Album) -> Unit,
|
||||
private val doOnSongClick: (data: Song) -> Unit,
|
||||
private val doOnLongClick: (view: View, data: Item) -> Unit,
|
||||
) : ListAdapter<Item, RecyclerView.ViewHolder>(DiffCallback()) {
|
||||
class ArtistDetailAdapter(listener: DetailItemListener) :
|
||||
DetailAdapter<DetailItemListener>(listener, DIFFER) {
|
||||
private var currentAlbum: Album? = null
|
||||
private var currentAlbumHolder: Highlightable? = null
|
||||
|
||||
private var currentSong: Song? = null
|
||||
private var currentSongHolder: Highlightable? = null
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return when (getItem(position)) {
|
||||
is Artist -> IntegerTable.ITEM_TYPE_ARTIST_DETAIL
|
||||
is Album -> IntegerTable.ITEM_TYPE_ARTIST_ALBUM
|
||||
is Song -> IntegerTable.ITEM_TYPE_ARTIST_SONG
|
||||
is Header -> IntegerTable.ITEM_TYPE_HEADER
|
||||
is ActionHeader -> IntegerTable.ITEM_TYPE_ACTION_HEADER
|
||||
else -> -1
|
||||
}
|
||||
}
|
||||
override fun getCreatorFromItem(item: Item) =
|
||||
super.getCreatorFromItem(item)
|
||||
?: when (item) {
|
||||
is Artist -> ArtistDetailViewHolder.CREATOR
|
||||
is Album -> ArtistAlbumViewHolder.CREATOR
|
||||
is Song -> ArtistSongViewHolder.CREATOR
|
||||
else -> null
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return when (viewType) {
|
||||
IntegerTable.ITEM_TYPE_ARTIST_DETAIL ->
|
||||
ArtistDetailViewHolder(ItemDetailBinding.inflate(parent.context.inflater))
|
||||
IntegerTable.ITEM_TYPE_ARTIST_ALBUM ->
|
||||
ArtistAlbumViewHolder(ItemParentBinding.inflate(parent.context.inflater))
|
||||
IntegerTable.ITEM_TYPE_ARTIST_SONG ->
|
||||
ArtistSongViewHolder(ItemSongBinding.inflate(parent.context.inflater))
|
||||
IntegerTable.ITEM_TYPE_HEADER -> HeaderViewHolder.from(parent.context)
|
||||
IntegerTable.ITEM_TYPE_ACTION_HEADER -> ActionHeaderViewHolder.from(parent.context)
|
||||
else -> error("Invalid ViewHolder item type $viewType")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
val item = getItem(position)
|
||||
override fun getCreatorFromViewType(viewType: Int) =
|
||||
super.getCreatorFromViewType(viewType)
|
||||
?: when (viewType) {
|
||||
ArtistDetailViewHolder.CREATOR.viewType -> ArtistDetailViewHolder.CREATOR
|
||||
ArtistAlbumViewHolder.CREATOR.viewType -> ArtistAlbumViewHolder.CREATOR
|
||||
ArtistSongViewHolder.CREATOR.viewType -> ArtistSongViewHolder.CREATOR
|
||||
else -> null
|
||||
}
|
||||
|
||||
override fun onBind(
|
||||
viewHolder: RecyclerView.ViewHolder,
|
||||
item: Item,
|
||||
listener: DetailItemListener
|
||||
) {
|
||||
super.onBind(viewHolder, item, listener)
|
||||
when (item) {
|
||||
is Artist -> (holder as ArtistDetailViewHolder).bind(item)
|
||||
is Album -> (holder as ArtistAlbumViewHolder).bind(item)
|
||||
is Song -> (holder as ArtistSongViewHolder).bind(item)
|
||||
is Header -> (holder as HeaderViewHolder).bind(item)
|
||||
is ActionHeader -> (holder as ActionHeaderViewHolder).bind(item)
|
||||
is Artist -> (viewHolder as ArtistDetailViewHolder).bind(item, listener)
|
||||
is Album -> (viewHolder as ArtistAlbumViewHolder).bind(item, listener)
|
||||
is Song -> (viewHolder as ArtistSongViewHolder).bind(item, listener)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
if (holder is Highlightable) {
|
||||
// If the item corresponds to a currently playing song/album then highlight it
|
||||
if (item.id == currentAlbum?.id && item is Album) {
|
||||
currentAlbumHolder?.setHighlighted(false)
|
||||
currentAlbumHolder = holder
|
||||
holder.setHighlighted(true)
|
||||
} else if (item.id == currentSong?.id && item is Song) {
|
||||
currentSongHolder?.setHighlighted(false)
|
||||
currentSongHolder = holder
|
||||
holder.setHighlighted(true)
|
||||
} else {
|
||||
holder.setHighlighted(false)
|
||||
}
|
||||
override fun onHighlightViewHolder(viewHolder: Highlightable, item: Item) {
|
||||
// If the item corresponds to a currently playing song/album then highlight it
|
||||
if (item.id == currentAlbum?.id && item is Album) {
|
||||
currentAlbumHolder?.setHighlighted(false)
|
||||
currentAlbumHolder = viewHolder
|
||||
viewHolder.setHighlighted(true)
|
||||
} else if (item.id == currentSong?.id && item is Song) {
|
||||
currentSongHolder?.setHighlighted(false)
|
||||
currentSongHolder = viewHolder
|
||||
viewHolder.setHighlighted(true)
|
||||
} else {
|
||||
viewHolder.setHighlighted(false)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -118,26 +104,10 @@ class ArtistDetailAdapter(
|
|||
* @param recycler The recyclerview the highlighting should act on.
|
||||
*/
|
||||
fun highlightAlbum(album: Album?, recycler: RecyclerView) {
|
||||
if (album == currentAlbum) return // Already highlighting this ViewHolder
|
||||
|
||||
// Album is no longer valid, clear out this ViewHolder.
|
||||
currentAlbumHolder?.setHighlighted(false)
|
||||
currentAlbumHolder = null
|
||||
if (album == currentAlbum) return
|
||||
currentAlbum = album
|
||||
|
||||
if (album != null) {
|
||||
// Use existing data instead of having to re-sort it.
|
||||
val pos = currentList.indexOfFirst { item -> item.id == album.id && item is Album }
|
||||
|
||||
// Check if the ViewHolder if this album is visible, and highlight it if so.
|
||||
recycler.layoutManager?.findViewByPosition(pos)?.let { child ->
|
||||
recycler.getChildViewHolder(child)?.let {
|
||||
currentAlbumHolder = it as Highlightable
|
||||
|
||||
currentAlbumHolder?.setHighlighted(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
currentAlbumHolder?.setHighlighted(false)
|
||||
currentAlbumHolder = highlightItem(album, recycler)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -145,91 +115,144 @@ class ArtistDetailAdapter(
|
|||
* @param recycler The recyclerview the highlighting should act on.
|
||||
*/
|
||||
fun highlightSong(song: Song?, recycler: RecyclerView) {
|
||||
if (song == currentSong) return // Already highlighting this ViewHolder
|
||||
|
||||
// Clear the current ViewHolder since it's invalid
|
||||
currentSongHolder?.setHighlighted(false)
|
||||
currentSongHolder = null
|
||||
if (song == currentSong) return
|
||||
currentSong = song
|
||||
currentSongHolder?.setHighlighted(false)
|
||||
currentSongHolder = highlightItem(song, recycler)
|
||||
}
|
||||
|
||||
if (song != null) {
|
||||
// Use existing data instead of having to re-sort it.
|
||||
// We also have to account for the album count when searching for the ViewHolder.
|
||||
val pos = currentList.indexOfFirst { item -> item.id == song.id && item is Song }
|
||||
|
||||
// Check if the ViewHolder for this song is visible, if it is then highlight it.
|
||||
// If the ViewHolder is not visible, then the adapter should take care of it if
|
||||
// it does become visible.
|
||||
recycler.layoutManager?.findViewByPosition(pos)?.let { child ->
|
||||
recycler.getChildViewHolder(child)?.let {
|
||||
currentSongHolder = it as Highlightable
|
||||
currentSongHolder?.setHighlighted(true)
|
||||
companion object {
|
||||
private val DIFFER =
|
||||
object : ItemDiffCallback<Item>() {
|
||||
override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
|
||||
return when {
|
||||
oldItem is Artist && newItem is Artist ->
|
||||
ArtistDetailViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
|
||||
oldItem is Album && newItem is Album ->
|
||||
ArtistAlbumViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
|
||||
oldItem is Song && newItem is Song ->
|
||||
ArtistSongViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
|
||||
else -> DetailAdapter.DIFFER.areItemsTheSame(oldItem, newItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner class ArtistDetailViewHolder(private val binding: ItemDetailBinding) :
|
||||
BaseViewHolder<Artist>(binding) {
|
||||
|
||||
override fun onBind(data: Artist) {
|
||||
val context = binding.root.context
|
||||
|
||||
binding.detailCover.apply {
|
||||
bindArtistImage(data)
|
||||
contentDescription =
|
||||
context.getString(R.string.desc_artist_image, data.resolvedName)
|
||||
}
|
||||
|
||||
binding.detailName.textSafe = data.resolvedName
|
||||
|
||||
// Get the genre that corresponds to the most songs in this artist, which would be
|
||||
// the most "Prominent" genre.
|
||||
binding.detailSubhead.textSafe =
|
||||
data.songs
|
||||
.groupBy { it.genre.resolvedName }
|
||||
.entries
|
||||
.maxByOrNull { it.value.size }
|
||||
?.key
|
||||
?: context.getString(R.string.def_genre)
|
||||
|
||||
binding.detailInfo.textSafe =
|
||||
binding.context.getString(
|
||||
R.string.fmt_two,
|
||||
binding.context.getPluralSafe(R.plurals.fmt_album_count, data.albums.size),
|
||||
binding.context.getPluralSafe(R.plurals.fmt_song_count, data.songs.size))
|
||||
|
||||
binding.detailPlayButton.setOnClickListener { playbackModel.playArtist(data, false) }
|
||||
|
||||
binding.detailShuffleButton.setOnClickListener { playbackModel.playArtist(data, true) }
|
||||
}
|
||||
}
|
||||
|
||||
inner class ArtistAlbumViewHolder(
|
||||
private val binding: ItemParentBinding,
|
||||
) : BaseViewHolder<Album>(binding, doOnClick, doOnLongClick), Highlightable {
|
||||
override fun onBind(data: Album) {
|
||||
binding.parentImage.bindAlbumCover(data)
|
||||
binding.parentName.textSafe = data.resolvedName
|
||||
binding.parentInfo.textSafe = binding.context.getString(R.string.fmt_number, data.year)
|
||||
}
|
||||
|
||||
override fun setHighlighted(isHighlighted: Boolean) {
|
||||
binding.parentName.isActivated = isHighlighted
|
||||
}
|
||||
}
|
||||
|
||||
inner class ArtistSongViewHolder(
|
||||
private val binding: ItemSongBinding,
|
||||
) : BaseViewHolder<Song>(binding, doOnSongClick, doOnLongClick), Highlightable {
|
||||
override fun onBind(data: Song) {
|
||||
binding.songAlbumCover.bindAlbumCover(data)
|
||||
binding.songName.textSafe = data.resolvedName
|
||||
binding.songInfo.textSafe = data.resolvedAlbumName
|
||||
}
|
||||
|
||||
override fun setHighlighted(isHighlighted: Boolean) {
|
||||
binding.songName.isActivated = isHighlighted
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ArtistDetailViewHolder private constructor(private val binding: ItemDetailBinding) :
|
||||
BindingViewHolder<Artist, DetailItemListener>(binding.root) {
|
||||
|
||||
override fun bind(item: Artist, listener: DetailItemListener) {
|
||||
binding.detailCover.bindArtistImage(item)
|
||||
binding.detailName.textSafe = item.resolvedName
|
||||
|
||||
// Get the genre that corresponds to the most songs in this artist, which would be
|
||||
// the most "Prominent" genre.
|
||||
binding.detailSubhead.textSafe =
|
||||
item.songs.groupBy { it.genre.resolvedName }.entries.maxByOrNull { it.value.size }?.key
|
||||
?: binding.context.getString(R.string.def_genre)
|
||||
|
||||
binding.detailInfo.textSafe =
|
||||
binding.context.getString(
|
||||
R.string.fmt_two,
|
||||
binding.context.getPluralSafe(R.plurals.fmt_album_count, item.albums.size),
|
||||
binding.context.getPluralSafe(R.plurals.fmt_song_count, item.songs.size))
|
||||
|
||||
binding.detailPlayButton.setOnClickListener { listener.onPlayParent() }
|
||||
binding.detailShuffleButton.setOnClickListener { listener.onShuffleParent() }
|
||||
}
|
||||
|
||||
companion object {
|
||||
val CREATOR =
|
||||
object : Creator<ArtistDetailViewHolder> {
|
||||
override val viewType: Int
|
||||
get() = IntegerTable.ITEM_TYPE_ARTIST_DETAIL
|
||||
|
||||
override fun create(context: Context) =
|
||||
ArtistDetailViewHolder(ItemDetailBinding.inflate(context.inflater))
|
||||
}
|
||||
|
||||
val DIFFER = ArtistViewHolder.DIFFER
|
||||
}
|
||||
}
|
||||
|
||||
private class ArtistAlbumViewHolder
|
||||
private constructor(
|
||||
private val binding: ItemParentBinding,
|
||||
) : BindingViewHolder<Album, MenuItemListener>(binding.root), Highlightable {
|
||||
override fun bind(item: Album, listener: MenuItemListener) {
|
||||
binding.parentImage.bindAlbumCover(item)
|
||||
binding.parentName.textSafe = item.resolvedName
|
||||
binding.parentInfo.textSafe = binding.context.getString(R.string.fmt_number, item.year)
|
||||
|
||||
binding.root.apply {
|
||||
setOnClickListener { listener.onItemClick(item) }
|
||||
setOnLongClickListener { view ->
|
||||
listener.onOpenMenu(item, view)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun setHighlighted(isHighlighted: Boolean) {
|
||||
binding.parentName.isActivated = isHighlighted
|
||||
}
|
||||
|
||||
companion object {
|
||||
val CREATOR =
|
||||
object : Creator<ArtistAlbumViewHolder> {
|
||||
override val viewType: Int
|
||||
get() = IntegerTable.ITEM_TYPE_ARTIST_ALBUM
|
||||
|
||||
override fun create(context: Context) =
|
||||
ArtistAlbumViewHolder(ItemParentBinding.inflate(context.inflater))
|
||||
}
|
||||
|
||||
val DIFFER =
|
||||
object : ItemDiffCallback<Album>() {
|
||||
override fun areItemsTheSame(oldItem: Album, newItem: Album) =
|
||||
oldItem.resolvedName == newItem.resolvedName && oldItem.year == newItem.year
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ArtistSongViewHolder
|
||||
private constructor(
|
||||
private val binding: ItemSongBinding,
|
||||
) : BindingViewHolder<Song, MenuItemListener>(binding.root), Highlightable {
|
||||
override fun bind(item: Song, listener: MenuItemListener) {
|
||||
binding.songAlbumCover.bindAlbumCover(item)
|
||||
binding.songName.textSafe = item.resolvedName
|
||||
binding.songInfo.textSafe = item.resolvedAlbumName
|
||||
|
||||
binding.root.apply {
|
||||
setOnClickListener { listener.onItemClick(item) }
|
||||
setOnLongClickListener { view ->
|
||||
listener.onOpenMenu(item, view)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun setHighlighted(isHighlighted: Boolean) {
|
||||
binding.songName.isActivated = isHighlighted
|
||||
}
|
||||
|
||||
companion object {
|
||||
val CREATOR =
|
||||
object : Creator<ArtistSongViewHolder> {
|
||||
override val viewType: Int
|
||||
get() = IntegerTable.ITEM_TYPE_ARTIST_SONG
|
||||
|
||||
override fun create(context: Context) =
|
||||
ArtistSongViewHolder(ItemSongBinding.inflate(context.inflater))
|
||||
}
|
||||
|
||||
val DIFFER =
|
||||
object : ItemDiffCallback<Song>() {
|
||||
override fun areItemsTheSame(oldItem: Song, newItem: Song) =
|
||||
oldItem.resolvedName == newItem.resolvedName &&
|
||||
oldItem.resolvedAlbumName == newItem.resolvedAlbumName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,153 @@
|
|||
/*
|
||||
* Copyright (c) 2022 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.detail.recycler
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.widget.TooltipCompat
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.oxycblt.auxio.IntegerTable
|
||||
import org.oxycblt.auxio.databinding.ItemSortHeaderBinding
|
||||
import org.oxycblt.auxio.ui.BindingViewHolder
|
||||
import org.oxycblt.auxio.ui.Header
|
||||
import org.oxycblt.auxio.ui.Item
|
||||
import org.oxycblt.auxio.ui.ItemDiffCallback
|
||||
import org.oxycblt.auxio.ui.MenuItemListener
|
||||
import org.oxycblt.auxio.ui.MultiAdapter
|
||||
import org.oxycblt.auxio.ui.NewHeaderViewHolder
|
||||
import org.oxycblt.auxio.util.context
|
||||
import org.oxycblt.auxio.util.getViewHolderAt
|
||||
import org.oxycblt.auxio.util.inflater
|
||||
import org.oxycblt.auxio.util.logW
|
||||
import org.oxycblt.auxio.util.textSafe
|
||||
|
||||
abstract class DetailAdapter<L : DetailItemListener>(
|
||||
listener: L,
|
||||
diffCallback: DiffUtil.ItemCallback<Item>
|
||||
) : MultiAdapter<L>(listener, diffCallback) {
|
||||
abstract fun onHighlightViewHolder(viewHolder: Highlightable, item: Item)
|
||||
|
||||
protected inline fun <reified T : Item> highlightItem(
|
||||
newItem: T?,
|
||||
recycler: RecyclerView
|
||||
): Highlightable? {
|
||||
if (newItem == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Use existing data instead of having to re-sort it.
|
||||
// We also have to account for the album count when searching for the ViewHolder.
|
||||
val pos = mCurrentList.indexOfFirst { item -> item.id == newItem.id && item is T }
|
||||
|
||||
// Check if the ViewHolder for this song is visible, if it is then highlight it.
|
||||
// If the ViewHolder is not visible, then the adapter should take care of it if
|
||||
// it does become visible.
|
||||
val viewHolder = recycler.getViewHolderAt(pos)
|
||||
|
||||
return if (viewHolder is Highlightable) {
|
||||
viewHolder.setHighlighted(true)
|
||||
viewHolder
|
||||
} else {
|
||||
logW("ViewHolder intended to highlight was not Highlightable")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun getCreatorFromItem(item: Item) =
|
||||
when (item) {
|
||||
is Header -> NewHeaderViewHolder.CREATOR
|
||||
is SortHeader -> SortHeaderViewHolder.CREATOR
|
||||
else -> null
|
||||
}
|
||||
|
||||
override fun getCreatorFromViewType(viewType: Int) =
|
||||
when (viewType) {
|
||||
NewHeaderViewHolder.CREATOR.viewType -> NewHeaderViewHolder.CREATOR
|
||||
SortHeaderViewHolder.CREATOR.viewType -> SortHeaderViewHolder.CREATOR
|
||||
else -> null
|
||||
}
|
||||
|
||||
override fun onBind(viewHolder: RecyclerView.ViewHolder, item: Item, listener: L) {
|
||||
when (item) {
|
||||
is Header -> (viewHolder as NewHeaderViewHolder).bind(item, Unit)
|
||||
is SortHeader -> (viewHolder as SortHeaderViewHolder).bind(item, listener)
|
||||
}
|
||||
|
||||
if (viewHolder is Highlightable) {
|
||||
onHighlightViewHolder(viewHolder, item)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val DIFFER =
|
||||
object : ItemDiffCallback<Item>() {
|
||||
override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
|
||||
return when {
|
||||
oldItem is Header && newItem is Header ->
|
||||
NewHeaderViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
|
||||
oldItem is SortHeader && newItem is SortHeader ->
|
||||
SortHeaderViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class SortHeader(override val id: Long, @StringRes val string: Int) : Item()
|
||||
|
||||
class SortHeaderViewHolder(private val binding: ItemSortHeaderBinding) :
|
||||
BindingViewHolder<SortHeader, DetailItemListener>(binding.root) {
|
||||
override fun bind(item: SortHeader, listener: DetailItemListener) {
|
||||
binding.headerTitle.textSafe = binding.context.getString(item.string)
|
||||
binding.headerButton.apply {
|
||||
TooltipCompat.setTooltipText(this, contentDescription)
|
||||
setOnClickListener(listener::onShowSortMenu)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val CREATOR =
|
||||
object : Creator<SortHeaderViewHolder> {
|
||||
override val viewType: Int
|
||||
get() = IntegerTable.ITEM_TYPE_SORT_HEADER
|
||||
|
||||
override fun create(context: Context) =
|
||||
SortHeaderViewHolder(ItemSortHeaderBinding.inflate(context.inflater))
|
||||
}
|
||||
|
||||
val DIFFER =
|
||||
object : ItemDiffCallback<SortHeader>() {
|
||||
override fun areItemsTheSame(oldItem: SortHeader, newItem: SortHeader) =
|
||||
oldItem.string == newItem.string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Interface that allows the highlighting of certain ViewHolders */
|
||||
interface Highlightable {
|
||||
fun setHighlighted(isHighlighted: Boolean)
|
||||
}
|
||||
|
||||
interface DetailItemListener : MenuItemListener {
|
||||
fun onPlayParent()
|
||||
fun onShuffleParent()
|
||||
fun onShowSortMenu(anchor: View)
|
||||
}
|
|
@ -17,9 +17,7 @@
|
|||
|
||||
package org.oxycblt.auxio.detail.recycler
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import android.content.Context
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.oxycblt.auxio.IntegerTable
|
||||
import org.oxycblt.auxio.R
|
||||
|
@ -27,70 +25,65 @@ import org.oxycblt.auxio.coil.bindAlbumCover
|
|||
import org.oxycblt.auxio.coil.bindGenreImage
|
||||
import org.oxycblt.auxio.databinding.ItemDetailBinding
|
||||
import org.oxycblt.auxio.databinding.ItemSongBinding
|
||||
import org.oxycblt.auxio.music.ActionHeader
|
||||
import org.oxycblt.auxio.music.Genre
|
||||
import org.oxycblt.auxio.music.Item
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||
import org.oxycblt.auxio.ui.ActionHeaderViewHolder
|
||||
import org.oxycblt.auxio.ui.BaseViewHolder
|
||||
import org.oxycblt.auxio.ui.DiffCallback
|
||||
import org.oxycblt.auxio.ui.BindingViewHolder
|
||||
import org.oxycblt.auxio.ui.Item
|
||||
import org.oxycblt.auxio.ui.ItemDiffCallback
|
||||
import org.oxycblt.auxio.ui.MenuItemListener
|
||||
import org.oxycblt.auxio.ui.SongViewHolder
|
||||
import org.oxycblt.auxio.util.context
|
||||
import org.oxycblt.auxio.util.getPluralSafe
|
||||
import org.oxycblt.auxio.util.inflater
|
||||
import org.oxycblt.auxio.util.textSafe
|
||||
|
||||
/**
|
||||
* An adapter for displaying the [Song]s of a genre.
|
||||
* An adapter for displaying genre information and it's children.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class GenreDetailAdapter(
|
||||
private val playbackModel: PlaybackViewModel,
|
||||
private val doOnClick: (data: Song) -> Unit,
|
||||
private val doOnLongClick: (view: View, data: Song) -> Unit
|
||||
) : ListAdapter<Item, RecyclerView.ViewHolder>(DiffCallback()) {
|
||||
class GenreDetailAdapter(listener: DetailItemListener) :
|
||||
DetailAdapter<DetailItemListener>(listener, DIFFER) {
|
||||
private var currentSong: Song? = null
|
||||
private var currentHolder: Highlightable? = null
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return when (getItem(position)) {
|
||||
is Genre -> IntegerTable.ITEM_TYPE_GENRE_DETAIL
|
||||
is ActionHeader -> IntegerTable.ITEM_TYPE_ACTION_HEADER
|
||||
is Song -> IntegerTable.ITEM_TYPE_GENRE_SONG
|
||||
else -> -1
|
||||
}
|
||||
}
|
||||
override fun getCreatorFromItem(item: Item) =
|
||||
super.getCreatorFromItem(item)
|
||||
?: when (item) {
|
||||
is Genre -> GenreDetailViewHolder.CREATOR
|
||||
is Song -> GenreSongViewHolder.CREATOR
|
||||
else -> null
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return when (viewType) {
|
||||
IntegerTable.ITEM_TYPE_GENRE_DETAIL ->
|
||||
GenreDetailViewHolder(ItemDetailBinding.inflate(parent.context.inflater))
|
||||
IntegerTable.ITEM_TYPE_ACTION_HEADER -> ActionHeaderViewHolder.from(parent.context)
|
||||
IntegerTable.ITEM_TYPE_GENRE_SONG ->
|
||||
GenreSongViewHolder(ItemSongBinding.inflate(parent.context.inflater))
|
||||
else -> error("Bad ViewHolder item type $viewType")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
val item = getItem(position)
|
||||
override fun getCreatorFromViewType(viewType: Int) =
|
||||
super.getCreatorFromViewType(viewType)
|
||||
?: when (viewType) {
|
||||
GenreDetailViewHolder.CREATOR.viewType -> GenreDetailViewHolder.CREATOR
|
||||
GenreSongViewHolder.CREATOR.viewType -> GenreSongViewHolder.CREATOR
|
||||
else -> null
|
||||
}
|
||||
|
||||
override fun onBind(
|
||||
viewHolder: RecyclerView.ViewHolder,
|
||||
item: Item,
|
||||
listener: DetailItemListener
|
||||
) {
|
||||
super.onBind(viewHolder, item, listener)
|
||||
when (item) {
|
||||
is Genre -> (holder as GenreDetailViewHolder).bind(item)
|
||||
is ActionHeader -> (holder as ActionHeaderViewHolder).bind(item)
|
||||
is Song -> (holder as GenreSongViewHolder).bind(item)
|
||||
is Genre -> (viewHolder as GenreDetailViewHolder).bind(item, listener)
|
||||
is Song -> (viewHolder as GenreSongViewHolder).bind(item, listener)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
if (holder is Highlightable) {
|
||||
if (item.id == currentSong?.id) {
|
||||
// Reset the last ViewHolder before assigning the new, correct one to be highlighted
|
||||
currentHolder?.setHighlighted(false)
|
||||
currentHolder = holder
|
||||
holder.setHighlighted(true)
|
||||
} else {
|
||||
holder.setHighlighted(false)
|
||||
}
|
||||
override fun onHighlightViewHolder(viewHolder: Highlightable, item: Item) {
|
||||
// If the item corresponds to a currently playing song/album then highlight it
|
||||
if (item.id == currentSong?.id) {
|
||||
// Reset the last ViewHolder before assigning the new, correct one to be highlighted
|
||||
currentHolder?.setHighlighted(false)
|
||||
currentHolder = viewHolder
|
||||
viewHolder.setHighlighted(true)
|
||||
} else {
|
||||
viewHolder.setHighlighted(false)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -99,64 +92,89 @@ class GenreDetailAdapter(
|
|||
* @param recycler The recyclerview the highlighting should act on.
|
||||
*/
|
||||
fun highlightSong(song: Song?, recycler: RecyclerView) {
|
||||
if (song == currentSong) return // Already highlighting this ViewHolder
|
||||
|
||||
// Clear the current ViewHolder since it's invalid
|
||||
currentHolder?.setHighlighted(false)
|
||||
currentHolder = null
|
||||
if (song == currentSong) return
|
||||
currentSong = song
|
||||
currentHolder?.setHighlighted(false)
|
||||
currentHolder = highlightItem(song, recycler)
|
||||
}
|
||||
|
||||
if (song != null) {
|
||||
// Use existing data instead of having to re-sort it.
|
||||
val pos = currentList.indexOfFirst { item -> item.id == song.id && item is Song }
|
||||
|
||||
// Check if the ViewHolder for this song is visible, if it is then highlight it.
|
||||
// If the ViewHolder is not visible, then the adapter should take care of it if
|
||||
// it does become visible.
|
||||
recycler.layoutManager?.findViewByPosition(pos)?.let { child ->
|
||||
recycler.getChildViewHolder(child)?.let {
|
||||
currentHolder = it as Highlightable
|
||||
currentHolder?.setHighlighted(true)
|
||||
companion object {
|
||||
val DIFFER =
|
||||
object : ItemDiffCallback<Item>() {
|
||||
override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
|
||||
return when {
|
||||
oldItem is Genre && newItem is Genre ->
|
||||
GenreDetailViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
|
||||
oldItem is Song && newItem is Song ->
|
||||
GenreSongViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
|
||||
else -> DetailAdapter.DIFFER.areContentsTheSame(oldItem, newItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner class GenreDetailViewHolder(private val binding: ItemDetailBinding) :
|
||||
BaseViewHolder<Genre>(binding) {
|
||||
override fun onBind(data: Genre) {
|
||||
val context = binding.root.context
|
||||
|
||||
binding.detailCover.apply {
|
||||
bindGenreImage(data)
|
||||
contentDescription = context.getString(R.string.desc_genre_image, data.resolvedName)
|
||||
}
|
||||
|
||||
binding.detailName.textSafe = data.resolvedName
|
||||
binding.detailSubhead.textSafe =
|
||||
binding.context.getPluralSafe(R.plurals.fmt_song_count, data.songs.size)
|
||||
binding.detailInfo.textSafe = data.totalDuration
|
||||
|
||||
binding.detailPlayButton.setOnClickListener { playbackModel.playGenre(data, false) }
|
||||
|
||||
binding.detailShuffleButton.setOnClickListener { playbackModel.playGenre(data, true) }
|
||||
}
|
||||
}
|
||||
|
||||
/** The Shared ViewHolder for a [Song]. Instantiation should be done with [from]. */
|
||||
inner class GenreSongViewHolder
|
||||
constructor(
|
||||
private val binding: ItemSongBinding,
|
||||
) : BaseViewHolder<Song>(binding, doOnClick, doOnLongClick), Highlightable {
|
||||
|
||||
override fun onBind(data: Song) {
|
||||
binding.songAlbumCover.bindAlbumCover(data)
|
||||
binding.songName.textSafe = data.resolvedName
|
||||
binding.songInfo.textSafe = data.resolvedArtistName
|
||||
}
|
||||
|
||||
override fun setHighlighted(isHighlighted: Boolean) {
|
||||
binding.songName.isActivated = isHighlighted
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class GenreDetailViewHolder private constructor(private val binding: ItemDetailBinding) :
|
||||
BindingViewHolder<Genre, DetailItemListener>(binding.root) {
|
||||
override fun bind(item: Genre, listener: DetailItemListener) {
|
||||
binding.detailCover.bindGenreImage(item)
|
||||
binding.detailName.textSafe = item.resolvedName
|
||||
binding.detailSubhead.textSafe =
|
||||
binding.context.getPluralSafe(R.plurals.fmt_song_count, item.songs.size)
|
||||
binding.detailInfo.textSafe = item.totalDuration
|
||||
binding.detailPlayButton.setOnClickListener { listener.onPlayParent() }
|
||||
binding.detailShuffleButton.setOnClickListener { listener.onShuffleParent() }
|
||||
}
|
||||
|
||||
companion object {
|
||||
val CREATOR =
|
||||
object : Creator<GenreDetailViewHolder> {
|
||||
override val viewType: Int
|
||||
get() = IntegerTable.ITEM_TYPE_GENRE_DETAIL
|
||||
|
||||
override fun create(context: Context) =
|
||||
GenreDetailViewHolder(ItemDetailBinding.inflate(context.inflater))
|
||||
}
|
||||
|
||||
val DIFFER =
|
||||
object : ItemDiffCallback<Genre>() {
|
||||
override fun areItemsTheSame(oldItem: Genre, newItem: Genre) =
|
||||
oldItem.resolvedName == newItem.resolvedName &&
|
||||
oldItem.songs.size == newItem.songs.size &&
|
||||
oldItem.totalDuration == newItem.totalDuration
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class GenreSongViewHolder private constructor(private val binding: ItemSongBinding) :
|
||||
BindingViewHolder<Song, MenuItemListener>(binding.root), Highlightable {
|
||||
override fun bind(item: Song, listener: MenuItemListener) {
|
||||
binding.songAlbumCover.bindAlbumCover(item)
|
||||
binding.songName.textSafe = item.resolvedName
|
||||
binding.songInfo.textSafe = item.resolvedArtistName
|
||||
binding.root.apply {
|
||||
setOnClickListener { listener.onItemClick(item) }
|
||||
setOnLongClickListener { view ->
|
||||
listener.onOpenMenu(item, view)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun setHighlighted(isHighlighted: Boolean) {
|
||||
binding.songName.isActivated = isHighlighted
|
||||
}
|
||||
|
||||
companion object {
|
||||
val CREATOR =
|
||||
object : Creator<GenreSongViewHolder> {
|
||||
override val viewType: Int
|
||||
get() = IntegerTable.ITEM_TYPE_GENRE_SONG
|
||||
|
||||
override fun create(context: Context) =
|
||||
GenreSongViewHolder(ItemSongBinding.inflate(context.inflater))
|
||||
}
|
||||
|
||||
val DIFFER = SongViewHolder.DIFFER
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2021 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.detail.recycler
|
||||
|
||||
/** Interface that allows the highlighting of certain ViewHolders */
|
||||
interface Highlightable {
|
||||
fun setHighlighted(isHighlighted: Boolean)
|
||||
}
|
|
@ -136,24 +136,33 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
|||
removeCallbacks(hideThumbRunnable)
|
||||
showScrollbar()
|
||||
showPopup()
|
||||
onDragListener?.onFastScrollStart()
|
||||
} else {
|
||||
postAutoHideScrollbar()
|
||||
hidePopup()
|
||||
onDragListener?.onFastScrollStop()
|
||||
}
|
||||
|
||||
onDragListener?.invoke(value)
|
||||
}
|
||||
|
||||
private val tRect = Rect()
|
||||
|
||||
interface PopupProvider {
|
||||
fun getPopup(pos: Int): String?
|
||||
}
|
||||
|
||||
/** Callback to provide a string to be shown on the popup when an item is passed */
|
||||
var popupProvider: ((Int) -> String)? = null
|
||||
var popupProvider: PopupProvider? = null
|
||||
|
||||
interface OnFastScrollListener {
|
||||
fun onFastScrollStart()
|
||||
fun onFastScrollStop()
|
||||
}
|
||||
|
||||
/**
|
||||
* A listener for when a drag event occurs. The value will be true if a drag has begun, and
|
||||
* false if a drag ended.
|
||||
*/
|
||||
var onDragListener: ((Boolean) -> Unit)? = null
|
||||
var onDragListener: OnFastScrollListener? = null
|
||||
|
||||
init {
|
||||
overlay.add(thumbView)
|
||||
|
@ -186,8 +195,6 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
|||
// --- RECYCLERVIEW EVENT MANAGEMENT ---
|
||||
|
||||
private fun onPreDraw() {
|
||||
// FIXME: Make the way we lay out views less of a hacky mess. Perhaps consider
|
||||
// overlaying views or turning this into a ViewGroup.
|
||||
updateScrollbarState()
|
||||
|
||||
thumbView.layoutDirection = layoutDirection
|
||||
|
@ -207,13 +214,11 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
|||
val firstPos = firstAdapterPos
|
||||
val popupText =
|
||||
if (firstPos != NO_POSITION) {
|
||||
popupProvider?.invoke(firstPos)?.ifEmpty { null }
|
||||
popupProvider?.getPopup(firstPos)?.ifEmpty { null }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
// Lay out the popup view
|
||||
|
||||
popupView.isInvisible = popupText == null
|
||||
|
||||
if (popupText != null) {
|
||||
|
@ -370,6 +375,10 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
|||
}
|
||||
|
||||
private fun scrollTo(offset: Int) {
|
||||
if (childCount == 0) {
|
||||
return
|
||||
}
|
||||
|
||||
stopScroll()
|
||||
|
||||
val trueOffset = offset - paddingTop
|
||||
|
|
|
@ -17,16 +17,18 @@
|
|||
|
||||
package org.oxycblt.auxio.home.list
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
|
||||
import org.oxycblt.auxio.home.HomeFragmentDirections
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.ui.AlbumViewHolder
|
||||
import org.oxycblt.auxio.ui.BindingViewHolder
|
||||
import org.oxycblt.auxio.ui.DisplayMode
|
||||
import org.oxycblt.auxio.ui.Item
|
||||
import org.oxycblt.auxio.ui.MenuItemListener
|
||||
import org.oxycblt.auxio.ui.MonoAdapter
|
||||
import org.oxycblt.auxio.ui.Sort
|
||||
import org.oxycblt.auxio.ui.newMenu
|
||||
import org.oxycblt.auxio.ui.sliceArticle
|
||||
|
@ -35,50 +37,43 @@ import org.oxycblt.auxio.ui.sliceArticle
|
|||
* A [HomeListFragment] for showing a list of [Album]s.
|
||||
* @author
|
||||
*/
|
||||
class AlbumListFragment : HomeListFragment() {
|
||||
override fun onBindingCreated(binding: FragmentHomeListBinding, savedInstanceState: Bundle?) {
|
||||
val homeAdapter =
|
||||
AlbumAdapter(
|
||||
doOnClick = { album ->
|
||||
findNavController().navigate(HomeFragmentDirections.actionShowAlbum(album.id))
|
||||
},
|
||||
::newMenu)
|
||||
class AlbumListFragment : HomeListFragment<Album>() {
|
||||
override val recyclerId: Int = R.id.home_album_list
|
||||
override val homeAdapter = AlbumAdapter(this)
|
||||
override val homeData: LiveData<List<Album>>
|
||||
get() = homeModel.albums
|
||||
|
||||
setupRecycler(R.id.home_album_list, homeAdapter, homeModel.albums)
|
||||
override fun getPopup(pos: Int): String? {
|
||||
val album = homeModel.albums.value!![pos]
|
||||
|
||||
// Change how we display the popup depending on the mode.
|
||||
return when (homeModel.getSortForDisplay(DisplayMode.SHOW_ALBUMS)) {
|
||||
// By Name -> Use Name
|
||||
is Sort.ByName -> album.resolvedName.sliceArticle().first().uppercase()
|
||||
|
||||
// By Artist -> Use Artist Name
|
||||
is Sort.ByArtist -> album.artist.resolvedName.sliceArticle().first().uppercase()
|
||||
|
||||
// Year -> Use Full Year
|
||||
is Sort.ByYear -> album.year?.toString() ?: getString(R.string.def_date)
|
||||
|
||||
// Unsupported sort, error gracefully
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
override val listPopupProvider: (Int) -> String
|
||||
get() = { idx ->
|
||||
val album = homeModel.albums.value!![idx]
|
||||
override fun onItemClick(item: Item) {
|
||||
check(item is Album)
|
||||
findNavController().navigate(HomeFragmentDirections.actionShowAlbum(item.id))
|
||||
}
|
||||
|
||||
// Change how we display the popup depending on the mode.
|
||||
when (homeModel.getSortForDisplay(DisplayMode.SHOW_ALBUMS)) {
|
||||
// By Name -> Use Name
|
||||
is Sort.ByName -> album.resolvedName.sliceArticle().first().uppercase()
|
||||
override fun onOpenMenu(item: Item, anchor: View) {
|
||||
newMenu(anchor, item)
|
||||
}
|
||||
|
||||
// By Artist -> Use Artist Name
|
||||
is Sort.ByArtist -> album.artist.resolvedName.sliceArticle().first().uppercase()
|
||||
|
||||
// Year -> Use Full Year
|
||||
is Sort.ByYear -> album.year?.toString() ?: getString(R.string.def_date)
|
||||
|
||||
// Unsupported sort, error gracefully
|
||||
else -> ""
|
||||
}
|
||||
}
|
||||
|
||||
class AlbumAdapter(
|
||||
private val doOnClick: (data: Album) -> Unit,
|
||||
private val doOnLongClick: (view: View, data: Album) -> Unit,
|
||||
) : HomeAdapter<Album, AlbumViewHolder>() {
|
||||
override fun getItemCount(): Int = data.size
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AlbumViewHolder {
|
||||
return AlbumViewHolder.from(parent.context, doOnClick, doOnLongClick)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: AlbumViewHolder, position: Int) {
|
||||
holder.bind(data[position])
|
||||
}
|
||||
class AlbumAdapter(listener: MenuItemListener) :
|
||||
MonoAdapter<Album, MenuItemListener, AlbumViewHolder>(listener, AlbumViewHolder.DIFFER) {
|
||||
override val creator: BindingViewHolder.Creator<AlbumViewHolder>
|
||||
get() = AlbumViewHolder.CREATOR
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,15 +17,16 @@
|
|||
|
||||
package org.oxycblt.auxio.home.list
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
|
||||
import org.oxycblt.auxio.home.HomeFragmentDirections
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.ui.ArtistViewHolder
|
||||
import org.oxycblt.auxio.ui.Item
|
||||
import org.oxycblt.auxio.ui.MenuItemListener
|
||||
import org.oxycblt.auxio.ui.MonoAdapter
|
||||
import org.oxycblt.auxio.ui.newMenu
|
||||
import org.oxycblt.auxio.ui.sliceArticle
|
||||
|
||||
|
@ -33,35 +34,26 @@ import org.oxycblt.auxio.ui.sliceArticle
|
|||
* A [HomeListFragment] for showing a list of [Artist]s.
|
||||
* @author
|
||||
*/
|
||||
class ArtistListFragment : HomeListFragment() {
|
||||
override fun onBindingCreated(binding: FragmentHomeListBinding, savedInstanceState: Bundle?) {
|
||||
val homeAdapter =
|
||||
ArtistAdapter(
|
||||
doOnClick = { artist ->
|
||||
findNavController().navigate(HomeFragmentDirections.actionShowArtist(artist.id))
|
||||
},
|
||||
::newMenu)
|
||||
class ArtistListFragment : HomeListFragment<Artist>() {
|
||||
override val recyclerId: Int = R.id.home_artist_list
|
||||
override val homeAdapter = ArtistAdapter(this)
|
||||
override val homeData: LiveData<List<Artist>>
|
||||
get() = homeModel.artists
|
||||
|
||||
setupRecycler(R.id.home_artist_list, homeAdapter, homeModel.artists)
|
||||
override fun getPopup(pos: Int) =
|
||||
homeModel.artists.value!![pos].resolvedName.sliceArticle().first().uppercase()
|
||||
|
||||
override fun onItemClick(item: Item) {
|
||||
check(item is Artist)
|
||||
findNavController().navigate(HomeFragmentDirections.actionShowArtist(item.id))
|
||||
}
|
||||
|
||||
override val listPopupProvider: (Int) -> String
|
||||
get() = { idx ->
|
||||
homeModel.artists.value!![idx].resolvedName.sliceArticle().first().uppercase()
|
||||
}
|
||||
override fun onOpenMenu(item: Item, anchor: View) {
|
||||
newMenu(anchor, item)
|
||||
}
|
||||
|
||||
class ArtistAdapter(
|
||||
private val doOnClick: (data: Artist) -> Unit,
|
||||
private val doOnLongClick: (view: View, data: Artist) -> Unit,
|
||||
) : HomeAdapter<Artist, ArtistViewHolder>() {
|
||||
override fun getItemCount(): Int = data.size
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ArtistViewHolder {
|
||||
return ArtistViewHolder.from(parent.context, doOnClick, doOnLongClick)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ArtistViewHolder, position: Int) {
|
||||
holder.bind(data[position])
|
||||
}
|
||||
class ArtistAdapter(listener: MenuItemListener) :
|
||||
MonoAdapter<Artist, MenuItemListener, ArtistViewHolder>(listener, ArtistViewHolder.DIFFER) {
|
||||
override val creator = ArtistViewHolder.CREATOR
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,52 +17,43 @@
|
|||
|
||||
package org.oxycblt.auxio.home.list
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
|
||||
import org.oxycblt.auxio.home.HomeFragmentDirections
|
||||
import org.oxycblt.auxio.music.Genre
|
||||
import org.oxycblt.auxio.ui.GenreViewHolder
|
||||
import org.oxycblt.auxio.ui.Item
|
||||
import org.oxycblt.auxio.ui.MenuItemListener
|
||||
import org.oxycblt.auxio.ui.MonoAdapter
|
||||
import org.oxycblt.auxio.ui.newMenu
|
||||
import org.oxycblt.auxio.ui.sliceArticle
|
||||
import org.oxycblt.auxio.util.context
|
||||
|
||||
/**
|
||||
* A [HomeListFragment] for showing a list of [Genre]s.
|
||||
* @author
|
||||
*/
|
||||
class GenreListFragment : HomeListFragment() {
|
||||
override fun onBindingCreated(binding: FragmentHomeListBinding, savedInstanceState: Bundle?) {
|
||||
val homeAdapter =
|
||||
GenreAdapter(
|
||||
doOnClick = { Genre ->
|
||||
findNavController().navigate(HomeFragmentDirections.actionShowGenre(Genre.id))
|
||||
},
|
||||
::newMenu)
|
||||
class GenreListFragment : HomeListFragment<Genre>() {
|
||||
override val recyclerId = R.id.home_genre_list
|
||||
override val homeAdapter = GenreAdapter(this)
|
||||
override val homeData: LiveData<List<Genre>>
|
||||
get() = homeModel.genres
|
||||
|
||||
setupRecycler(R.id.home_genre_list, homeAdapter, homeModel.genres)
|
||||
override fun getPopup(pos: Int) =
|
||||
homeModel.genres.value!![pos].resolvedName.sliceArticle().first().uppercase()
|
||||
|
||||
override fun onItemClick(item: Item) {
|
||||
check(item is Genre)
|
||||
findNavController().navigate(HomeFragmentDirections.actionShowGenre(item.id))
|
||||
}
|
||||
|
||||
override val listPopupProvider: (Int) -> String
|
||||
get() = { idx ->
|
||||
homeModel.genres.value!![idx].resolvedName.sliceArticle().first().uppercase()
|
||||
}
|
||||
override fun onOpenMenu(item: Item, anchor: View) {
|
||||
newMenu(anchor, item)
|
||||
}
|
||||
|
||||
class GenreAdapter(
|
||||
private val doOnClick: (data: Genre) -> Unit,
|
||||
private val doOnLongClick: (view: View, data: Genre) -> Unit,
|
||||
) : HomeAdapter<Genre, GenreViewHolder>() {
|
||||
override fun getItemCount(): Int = data.size
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GenreViewHolder {
|
||||
return GenreViewHolder.from(parent.context, doOnClick, doOnLongClick)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: GenreViewHolder, position: Int) {
|
||||
holder.bind(data[position])
|
||||
}
|
||||
class GenreAdapter(listener: MenuItemListener) :
|
||||
MonoAdapter<Genre, MenuItemListener, GenreViewHolder>(listener, GenreViewHolder.DIFFER) {
|
||||
override val creator = GenreViewHolder.CREATOR
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,17 +17,19 @@
|
|||
|
||||
package org.oxycblt.auxio.home.list
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import androidx.annotation.IdRes
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
|
||||
import org.oxycblt.auxio.home.HomeViewModel
|
||||
import org.oxycblt.auxio.music.Item
|
||||
import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView
|
||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||
import org.oxycblt.auxio.ui.BindingViewHolder
|
||||
import org.oxycblt.auxio.ui.Item
|
||||
import org.oxycblt.auxio.ui.MenuItemListener
|
||||
import org.oxycblt.auxio.ui.MonoAdapter
|
||||
import org.oxycblt.auxio.ui.ViewBindingFragment
|
||||
import org.oxycblt.auxio.util.applySpans
|
||||
|
||||
|
@ -35,49 +37,52 @@ import org.oxycblt.auxio.util.applySpans
|
|||
* A Base [Fragment] implementing the base features shared across all list fragments in the home UI.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
abstract class HomeListFragment : ViewBindingFragment<FragmentHomeListBinding>() {
|
||||
abstract class HomeListFragment<T : Item> :
|
||||
ViewBindingFragment<FragmentHomeListBinding>(),
|
||||
MenuItemListener,
|
||||
FastScrollRecyclerView.PopupProvider,
|
||||
FastScrollRecyclerView.OnFastScrollListener {
|
||||
/** The popup provider to use for the fast scroller view. */
|
||||
abstract val listPopupProvider: (Int) -> String
|
||||
abstract val recyclerId: Int
|
||||
abstract val homeAdapter:
|
||||
MonoAdapter<T, MenuItemListener, out BindingViewHolder<T, MenuItemListener>>
|
||||
abstract val homeData: LiveData<List<T>>
|
||||
|
||||
protected val homeModel: HomeViewModel by activityViewModels()
|
||||
protected val playbackModel: PlaybackViewModel by activityViewModels()
|
||||
|
||||
protected fun <T : Item, VH : RecyclerView.ViewHolder> setupRecycler(
|
||||
@IdRes uniqueId: Int,
|
||||
homeAdapter: HomeAdapter<T, VH>,
|
||||
homeData: LiveData<List<T>>,
|
||||
) {
|
||||
requireBinding().homeRecycler.apply {
|
||||
id = uniqueId
|
||||
adapter = homeAdapter
|
||||
setHasFixedSize(true)
|
||||
applySpans()
|
||||
|
||||
popupProvider = listPopupProvider
|
||||
onDragListener = homeModel::updateFastScrolling
|
||||
}
|
||||
|
||||
homeData.observe(viewLifecycleOwner, homeAdapter::updateData)
|
||||
}
|
||||
|
||||
override fun onCreateBinding(inflater: LayoutInflater) =
|
||||
FragmentHomeListBinding.inflate(inflater)
|
||||
|
||||
override fun onDestroyBinding(binding: FragmentHomeListBinding) {
|
||||
homeModel.updateFastScrolling(false)
|
||||
}
|
||||
override fun onBindingCreated(binding: FragmentHomeListBinding, savedInstanceState: Bundle?) {
|
||||
binding.homeRecycler.apply {
|
||||
id = recyclerId
|
||||
adapter = homeAdapter
|
||||
applySpans()
|
||||
}
|
||||
|
||||
abstract class HomeAdapter<T : Item, VH : RecyclerView.ViewHolder> :
|
||||
RecyclerView.Adapter<VH>() {
|
||||
protected var data = listOf<T>()
|
||||
binding.homeRecycler.popupProvider = this
|
||||
binding.homeRecycler.onDragListener = this
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun updateData(newData: List<T>) {
|
||||
data = newData
|
||||
|
||||
// notifyDataSetChanged here is okay, as we have no idea how the layout changed when
|
||||
// we re-sort and ListAdapter causes the scroll position to get messed up
|
||||
notifyDataSetChanged()
|
||||
homeData.observe(viewLifecycleOwner) { list ->
|
||||
homeAdapter.submitListHard(list.toMutableList())
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyBinding(binding: FragmentHomeListBinding) {
|
||||
homeModel.updateFastScrolling(false)
|
||||
binding.homeRecycler.apply {
|
||||
adapter = null
|
||||
popupProvider = null
|
||||
onDragListener = null
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFastScrollStart() {
|
||||
homeModel.updateFastScrolling(true)
|
||||
}
|
||||
|
||||
override fun onFastScrollStop() {
|
||||
homeModel.updateFastScrolling(false)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,13 +17,14 @@
|
|||
|
||||
package org.oxycblt.auxio.home.list
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.lifecycle.LiveData
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.ui.DisplayMode
|
||||
import org.oxycblt.auxio.ui.Item
|
||||
import org.oxycblt.auxio.ui.MenuItemListener
|
||||
import org.oxycblt.auxio.ui.MonoAdapter
|
||||
import org.oxycblt.auxio.ui.SongViewHolder
|
||||
import org.oxycblt.auxio.ui.Sort
|
||||
import org.oxycblt.auxio.ui.newMenu
|
||||
|
@ -33,47 +34,44 @@ import org.oxycblt.auxio.ui.sliceArticle
|
|||
* A [HomeListFragment] for showing a list of [Song]s.
|
||||
* @author
|
||||
*/
|
||||
class SongListFragment : HomeListFragment() {
|
||||
override fun onBindingCreated(binding: FragmentHomeListBinding, savedInstanceState: Bundle?) {
|
||||
val homeAdapter = SongsAdapter(doOnClick = playbackModel::playSong, ::newMenu)
|
||||
setupRecycler(R.id.home_song_list, homeAdapter, homeModel.songs)
|
||||
class SongListFragment : HomeListFragment<Song>() {
|
||||
override val recyclerId = R.id.home_song_list
|
||||
override val homeAdapter = SongsAdapter(this)
|
||||
override val homeData: LiveData<List<Song>>
|
||||
get() = homeModel.songs
|
||||
|
||||
override fun getPopup(pos: Int): String {
|
||||
val song = homeModel.songs.value!![pos]
|
||||
|
||||
// Change how we display the popup depending on the mode.
|
||||
// We don't use the more correct resolve(Model)Name here, as sorts are largely
|
||||
// based off the names of the parent objects and not the child objects.
|
||||
return when (homeModel.getSortForDisplay(DisplayMode.SHOW_SONGS)) {
|
||||
// Name -> Use name
|
||||
is Sort.ByName -> song.resolvedName.sliceArticle().first().uppercase()
|
||||
|
||||
// Artist -> Use Artist Name
|
||||
is Sort.ByArtist -> song.album.artist.resolvedName.sliceArticle().first().uppercase()
|
||||
|
||||
// Album -> Use Album Name
|
||||
is Sort.ByAlbum -> song.album.resolvedName.sliceArticle().first().uppercase()
|
||||
|
||||
// Year -> Use Full Year
|
||||
is Sort.ByYear -> song.album.year?.toString() ?: getString(R.string.def_date)
|
||||
}
|
||||
}
|
||||
|
||||
override val listPopupProvider: (Int) -> String
|
||||
get() = { idx ->
|
||||
val song = homeModel.songs.value!![idx]
|
||||
override fun onItemClick(item: Item) {
|
||||
check(item is Song)
|
||||
playbackModel.playSong(item)
|
||||
}
|
||||
|
||||
// Change how we display the popup depending on the mode.
|
||||
// We don't use the more correct resolve(Model)Name here, as sorts are largely
|
||||
// based off the names of the parent objects and not the child objects.
|
||||
when (homeModel.getSortForDisplay(DisplayMode.SHOW_SONGS)) {
|
||||
// Name -> Use name
|
||||
is Sort.ByName -> song.resolvedName.sliceArticle().first().uppercase()
|
||||
override fun onOpenMenu(item: Item, anchor: View) {
|
||||
newMenu(anchor, item)
|
||||
}
|
||||
|
||||
// Artist -> Use Artist Name
|
||||
is Sort.ByArtist ->
|
||||
song.album.artist.resolvedName.sliceArticle().first().uppercase()
|
||||
|
||||
// Album -> Use Album Name
|
||||
is Sort.ByAlbum -> song.album.resolvedName.sliceArticle().first().uppercase()
|
||||
|
||||
// Year -> Use Full Year
|
||||
is Sort.ByYear -> song.album.year?.toString() ?: getString(R.string.def_date)
|
||||
}
|
||||
}
|
||||
|
||||
inner class SongsAdapter(
|
||||
private val doOnClick: (data: Song) -> Unit,
|
||||
private val doOnLongClick: (view: View, data: Song) -> Unit,
|
||||
) : HomeAdapter<Song, SongViewHolder>() {
|
||||
override fun getItemCount(): Int = data.size
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SongViewHolder {
|
||||
return SongViewHolder.from(parent.context, doOnClick, doOnLongClick)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: SongViewHolder, position: Int) {
|
||||
holder.bind(data[position])
|
||||
}
|
||||
inner class SongsAdapter(listener: MenuItemListener) :
|
||||
MonoAdapter<Song, MenuItemListener, SongViewHolder>(listener, SongViewHolder.DIFFER) {
|
||||
override val creator = SongViewHolder.CREATOR
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,18 +20,10 @@ package org.oxycblt.auxio.music
|
|||
import android.content.ContentUris
|
||||
import android.net.Uri
|
||||
import android.provider.MediaStore
|
||||
import android.view.View
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import org.oxycblt.auxio.ui.Item
|
||||
|
||||
// --- MUSIC MODELS ---
|
||||
|
||||
/** The base for all items in Auxio. */
|
||||
sealed class Item {
|
||||
/** A unique ID for this item. ***THIS IS NOT A MEDIASTORE ID!** */
|
||||
abstract val id: Long
|
||||
}
|
||||
|
||||
/** [Item] variant that represents a music item. */
|
||||
sealed class Music : Item() {
|
||||
/** The raw name of this item. */
|
||||
|
@ -245,49 +237,3 @@ data class Genre(
|
|||
val totalDuration: String
|
||||
get() = songs.sumOf { it.seconds }.toDuration(false)
|
||||
}
|
||||
|
||||
/** A data object used solely for the "Header" UI element. */
|
||||
data class Header(
|
||||
override val id: Long,
|
||||
/** The string resource used for the header. */
|
||||
@StringRes val string: Int
|
||||
) : Item()
|
||||
|
||||
/**
|
||||
* A data object used for an action header. Like [Header], but with a button.
|
||||
* @see Header
|
||||
*/
|
||||
data class ActionHeader(
|
||||
override val id: Long,
|
||||
/** The string resource used for the header. */
|
||||
@StringRes val string: Int,
|
||||
/** The icon resource used for the header action. */
|
||||
@DrawableRes val icon: Int,
|
||||
/** The string resource used for the header action's content description. */
|
||||
@StringRes val desc: Int,
|
||||
/** A callback for when this item is clicked. */
|
||||
val onClick: (View) -> Unit,
|
||||
) : Item() {
|
||||
// All lambdas are not equal to each-other, so we override equals/hashCode and exclude them.
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is ActionHeader) return false
|
||||
|
||||
if (id != other.id) return false
|
||||
if (string != other.string) return false
|
||||
if (icon != other.icon) return false
|
||||
if (desc != other.desc) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = id.hashCode()
|
||||
result = 31 * result + string.hashCode()
|
||||
result = 31 * result + icon
|
||||
result = 31 * result + desc
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
|
@ -357,10 +357,8 @@ class MusicLoader {
|
|||
|
||||
while (cursor.moveToNext()) {
|
||||
// Genre names can be a normal name, an ID3v2 constant, or null. Normal names
|
||||
// are
|
||||
// resolved as usual, but null values don't make sense and are often junk
|
||||
// anyway,
|
||||
// so we skip genres that have them.
|
||||
// are resolved as usual, but null values don't make sense and are often junk
|
||||
// anyway, so we skip genres that have them.
|
||||
val id = cursor.getLong(idIndex)
|
||||
val name = cursor.getStringOrNull(nameIndex) ?: continue
|
||||
val resolvedName = name.genreNameCompat ?: name
|
||||
|
|
|
@ -18,137 +18,89 @@
|
|||
package org.oxycblt.auxio.playback.queue
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.recyclerview.widget.AsyncListDiffer
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.shape.MaterialShapeDrawable
|
||||
import org.oxycblt.auxio.IntegerTable
|
||||
import org.oxycblt.auxio.coil.bindAlbumCover
|
||||
import org.oxycblt.auxio.databinding.ItemQueueSongBinding
|
||||
import org.oxycblt.auxio.music.ActionHeader
|
||||
import org.oxycblt.auxio.music.Header
|
||||
import org.oxycblt.auxio.music.Item
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.ui.ActionHeaderViewHolder
|
||||
import org.oxycblt.auxio.ui.BaseViewHolder
|
||||
import org.oxycblt.auxio.ui.DiffCallback
|
||||
import org.oxycblt.auxio.ui.HeaderViewHolder
|
||||
import org.oxycblt.auxio.ui.BindingViewHolder
|
||||
import org.oxycblt.auxio.ui.MonoAdapter
|
||||
import org.oxycblt.auxio.ui.SongViewHolder
|
||||
import org.oxycblt.auxio.util.disableDropShadowCompat
|
||||
import org.oxycblt.auxio.util.inflater
|
||||
import org.oxycblt.auxio.util.logE
|
||||
import org.oxycblt.auxio.util.stateList
|
||||
import org.oxycblt.auxio.util.textSafe
|
||||
|
||||
/**
|
||||
* The single adapter for both the Next Queue and the User Queue.
|
||||
* @param touchHelper The [ItemTouchHelper] ***containing*** [QueueDragCallback] to be used
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class QueueAdapter(private val touchHelper: ItemTouchHelper) :
|
||||
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
private var data = mutableListOf<Item>()
|
||||
private var listDiffer = AsyncListDiffer(this, DiffCallback())
|
||||
class NewQueueAdapter(listener: QueueItemListener) :
|
||||
MonoAdapter<Song, QueueItemListener, QueueSongViewHolder>(
|
||||
listener, QueueSongViewHolder.DIFFER) {
|
||||
override val creator = QueueSongViewHolder.CREATOR
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = data.size
|
||||
interface QueueItemListener {
|
||||
fun onPickUp(viewHolder: RecyclerView.ViewHolder)
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return when (data[position]) {
|
||||
is Song -> IntegerTable.ITEM_TYPE_QUEUE_SONG
|
||||
is Header -> IntegerTable.ITEM_TYPE_HEADER
|
||||
is ActionHeader -> IntegerTable.ITEM_TYPE_ACTION_HEADER
|
||||
else -> -1
|
||||
}
|
||||
}
|
||||
class QueueSongViewHolder
|
||||
private constructor(
|
||||
private val binding: ItemQueueSongBinding,
|
||||
) : BindingViewHolder<Song, QueueItemListener>(binding.root) {
|
||||
val bodyView: View
|
||||
get() = binding.body
|
||||
val backgroundView: View
|
||||
get() = binding.background
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return when (viewType) {
|
||||
IntegerTable.ITEM_TYPE_QUEUE_SONG ->
|
||||
QueueSongViewHolder(ItemQueueSongBinding.inflate(parent.context.inflater))
|
||||
IntegerTable.ITEM_TYPE_HEADER -> HeaderViewHolder.from(parent.context)
|
||||
IntegerTable.ITEM_TYPE_ACTION_HEADER -> ActionHeaderViewHolder.from(parent.context)
|
||||
else -> error("Invalid ViewHolder item type $viewType")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
when (val item = data[position]) {
|
||||
is Song -> (holder as QueueSongViewHolder).bind(item)
|
||||
is Header -> (holder as HeaderViewHolder).bind(item)
|
||||
is ActionHeader -> (holder as ActionHeaderViewHolder).bind(item)
|
||||
else -> logE("Bad data given to QueueAdapter")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit data using [AsyncListDiffer]. **Only use this if you have no idea what changes
|
||||
* occurred to the data**
|
||||
*/
|
||||
fun submitList(newData: MutableList<Item>) {
|
||||
if (data != newData) {
|
||||
data = newData
|
||||
listDiffer.submitList(newData)
|
||||
}
|
||||
}
|
||||
|
||||
/** Move Items. Used since [submitList] will cause QueueAdapter to freak out. */
|
||||
fun moveItems(adapterFrom: Int, adapterTo: Int) {
|
||||
data.add(adapterTo, data.removeAt(adapterFrom))
|
||||
notifyItemMoved(adapterFrom, adapterTo)
|
||||
}
|
||||
|
||||
/** Remove an item. Used since [submitList] will cause QueueAdapter to freak out. */
|
||||
fun removeItem(adapterIndex: Int) {
|
||||
data.removeAt(adapterIndex)
|
||||
notifyItemRemoved(adapterIndex)
|
||||
}
|
||||
|
||||
/** Generic ViewHolder for a queue song */
|
||||
inner class QueueSongViewHolder(
|
||||
private val binding: ItemQueueSongBinding,
|
||||
) : BaseViewHolder<Song>(binding) {
|
||||
val bodyView: View
|
||||
get() = binding.body
|
||||
val backgroundView: View
|
||||
get() = binding.background
|
||||
|
||||
init {
|
||||
binding.body.background =
|
||||
MaterialShapeDrawable.createWithElevationOverlay(binding.root.context).apply {
|
||||
fillColor = (binding.body.background as ColorDrawable).color.stateList
|
||||
}
|
||||
|
||||
binding.root.disableDropShadowCompat()
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onBind(data: Song) {
|
||||
binding.songAlbumCover.bindAlbumCover(data)
|
||||
binding.songName.textSafe = data.resolvedName
|
||||
binding.songInfo.textSafe = data.resolvedArtistName
|
||||
|
||||
binding.background.isInvisible = true
|
||||
|
||||
binding.songName.requestLayout()
|
||||
binding.songInfo.requestLayout()
|
||||
|
||||
// Roll our own drag handlers as the default ones suck
|
||||
binding.songDragHandle.setOnTouchListener { _, motionEvent ->
|
||||
binding.songDragHandle.performClick()
|
||||
if (motionEvent.actionMasked == MotionEvent.ACTION_DOWN) {
|
||||
touchHelper.startDrag(this)
|
||||
true
|
||||
} else false
|
||||
init {
|
||||
binding.body.background =
|
||||
MaterialShapeDrawable.createWithElevationOverlay(binding.root.context).apply {
|
||||
fillColor = (binding.body.background as ColorDrawable).color.stateList
|
||||
}
|
||||
|
||||
binding.body.setOnLongClickListener {
|
||||
touchHelper.startDrag(this)
|
||||
binding.root.disableDropShadowCompat()
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun bind(item: Song, listener: QueueItemListener) {
|
||||
binding.songAlbumCover.bindAlbumCover(item)
|
||||
binding.songName.textSafe = item.resolvedName
|
||||
binding.songInfo.textSafe = item.resolvedArtistName
|
||||
|
||||
binding.background.isInvisible = true
|
||||
|
||||
binding.songName.requestLayout()
|
||||
binding.songInfo.requestLayout()
|
||||
|
||||
// Roll our own drag handlers as the default ones suck
|
||||
binding.songDragHandle.setOnTouchListener { _, motionEvent ->
|
||||
binding.songDragHandle.performClick()
|
||||
if (motionEvent.actionMasked == MotionEvent.ACTION_DOWN) {
|
||||
listener.onPickUp(this)
|
||||
true
|
||||
}
|
||||
} else false
|
||||
}
|
||||
|
||||
binding.body.setOnLongClickListener {
|
||||
listener.onPickUp(this)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val CREATOR =
|
||||
object : Creator<QueueSongViewHolder> {
|
||||
override val viewType: Int
|
||||
get() = IntegerTable.ITEM_TYPE_QUEUE_SONG
|
||||
|
||||
override fun create(context: Context): QueueSongViewHolder =
|
||||
QueueSongViewHolder(ItemQueueSongBinding.inflate(context.inflater))
|
||||
}
|
||||
|
||||
val DIFFER = SongViewHolder.DIFFER
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,8 +38,10 @@ import org.oxycblt.auxio.util.logD
|
|||
* hot garbage. This shouldn't have *too many* UI bugs. I hope.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class QueueDragCallback(private val playbackModel: PlaybackViewModel) : ItemTouchHelper.Callback() {
|
||||
private lateinit var queueAdapter: QueueAdapter
|
||||
class QueueDragCallback(
|
||||
private val playbackModel: PlaybackViewModel,
|
||||
private val queueAdapter: NewQueueAdapter
|
||||
) : ItemTouchHelper.Callback() {
|
||||
private var shouldLift = true
|
||||
|
||||
override fun getMovementFlags(
|
||||
|
@ -83,7 +85,7 @@ class QueueDragCallback(private val playbackModel: PlaybackViewModel) : ItemTouc
|
|||
// themselves when being dragged. Too bad google's implementation of this doesn't even
|
||||
// work! To emulate it on my own, I check if this child is in a drag state and then animate
|
||||
// an elevation change.
|
||||
val holder = viewHolder as QueueAdapter.QueueSongViewHolder
|
||||
val holder = viewHolder as QueueSongViewHolder
|
||||
|
||||
if (shouldLift && isCurrentlyActive && actionState == ItemTouchHelper.ACTION_STATE_DRAG) {
|
||||
logD("Lifting queue item")
|
||||
|
@ -122,7 +124,7 @@ class QueueDragCallback(private val playbackModel: PlaybackViewModel) : ItemTouc
|
|||
|
||||
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
|
||||
// When an elevated item is cleared, we reset the elevation using another animation.
|
||||
val holder = viewHolder as QueueAdapter.QueueSongViewHolder
|
||||
val holder = viewHolder as QueueSongViewHolder
|
||||
|
||||
if (holder.itemView.translationZ != 0f) {
|
||||
logD("Dropping queue item")
|
||||
|
@ -163,14 +165,6 @@ class QueueDragCallback(private val playbackModel: PlaybackViewModel) : ItemTouc
|
|||
|
||||
override fun isLongPressDragEnabled(): Boolean = false
|
||||
|
||||
/**
|
||||
* Add the queue adapter to this callback. Done because there's a circular dependency between
|
||||
* the two objects
|
||||
*/
|
||||
fun addQueueAdapter(adapter: QueueAdapter) {
|
||||
queueAdapter = adapter
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val MINIMUM_INITIAL_DRAG_VELOCITY = 10
|
||||
const val MAXIMUM_INITIAL_DRAG_VELOCITY = 25
|
||||
|
|
|
@ -23,56 +23,68 @@ import androidx.fragment.app.Fragment
|
|||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.oxycblt.auxio.databinding.FragmentQueueBinding
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||
import org.oxycblt.auxio.ui.ViewBindingFragment
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.util.requireAttached
|
||||
|
||||
/**
|
||||
* A [Fragment] that shows the queue and enables editing as well.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class QueueFragment : ViewBindingFragment<FragmentQueueBinding>() {
|
||||
class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), QueueItemListener {
|
||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||
private var lastShuffle: Boolean? = null
|
||||
private var queueAdapter = NewQueueAdapter(this)
|
||||
private var touchHelper: ItemTouchHelper? = null
|
||||
private var callback: QueueDragCallback? = null
|
||||
|
||||
override fun onCreateBinding(inflater: LayoutInflater) = FragmentQueueBinding.inflate(inflater)
|
||||
|
||||
override fun onBindingCreated(binding: FragmentQueueBinding, savedInstanceState: Bundle?) {
|
||||
// TODO: Merge ItemTouchHelper with QueueAdapter
|
||||
val callback = QueueDragCallback(playbackModel)
|
||||
val helper = ItemTouchHelper(callback)
|
||||
val queueAdapter = QueueAdapter(helper)
|
||||
callback.addQueueAdapter(queueAdapter)
|
||||
|
||||
binding.queueToolbar.setNavigationOnClickListener { findNavController().navigateUp() }
|
||||
|
||||
binding.queueRecycler.apply {
|
||||
setHasFixedSize(true)
|
||||
adapter = queueAdapter
|
||||
helper.attachToRecyclerView(this)
|
||||
requireTouchHelper().attachToRecyclerView(this)
|
||||
}
|
||||
|
||||
// --- VIEWMODEL SETUP ----
|
||||
|
||||
lastShuffle = playbackModel.isShuffling.value
|
||||
playbackModel.isShuffling.observe(viewLifecycleOwner) { isShuffling ->
|
||||
// Try to prevent the queue adapter from going spastic during reshuffle events
|
||||
// by just scrolling back to the top.
|
||||
if (isShuffling != lastShuffle) {
|
||||
logD("Reshuffle event, scrolling to top")
|
||||
lastShuffle = isShuffling
|
||||
binding.queueRecycler.scrollToPosition(0)
|
||||
}
|
||||
playbackModel.nextUp.observe(viewLifecycleOwner, ::updateQueue)
|
||||
}
|
||||
|
||||
override fun onDestroyBinding(binding: FragmentQueueBinding) {
|
||||
super.onDestroyBinding(binding)
|
||||
binding.queueRecycler.adapter = null
|
||||
touchHelper = null
|
||||
callback = null
|
||||
}
|
||||
|
||||
override fun onPickUp(viewHolder: RecyclerView.ViewHolder) {
|
||||
requireTouchHelper().startDrag(viewHolder)
|
||||
}
|
||||
|
||||
private fun updateQueue(queue: List<Song>) {
|
||||
if (queue.isEmpty()) {
|
||||
findNavController().navigateUp()
|
||||
return
|
||||
}
|
||||
|
||||
playbackModel.nextUp.observe(viewLifecycleOwner) { queue ->
|
||||
if (queue.isEmpty()) {
|
||||
findNavController().navigateUp()
|
||||
return@observe
|
||||
}
|
||||
queueAdapter.submitList(queue.toMutableList())
|
||||
}
|
||||
|
||||
queueAdapter.submitList(queue.toMutableList())
|
||||
private fun requireTouchHelper(): ItemTouchHelper {
|
||||
requireAttached()
|
||||
val instance = touchHelper
|
||||
if (instance != null) {
|
||||
return instance
|
||||
}
|
||||
val newCallback = QueueDragCallback(playbackModel, queueAdapter)
|
||||
val newInstance = ItemTouchHelper(newCallback)
|
||||
callback = newCallback
|
||||
touchHelper = newInstance
|
||||
return newInstance
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,68 +17,76 @@
|
|||
|
||||
package org.oxycblt.auxio.search
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.oxycblt.auxio.IntegerTable
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.Genre
|
||||
import org.oxycblt.auxio.music.Header
|
||||
import org.oxycblt.auxio.music.Item
|
||||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.ui.AlbumViewHolder
|
||||
import org.oxycblt.auxio.ui.ArtistViewHolder
|
||||
import org.oxycblt.auxio.ui.DiffCallback
|
||||
import org.oxycblt.auxio.ui.GenreViewHolder
|
||||
import org.oxycblt.auxio.ui.HeaderViewHolder
|
||||
import org.oxycblt.auxio.ui.Header
|
||||
import org.oxycblt.auxio.ui.Item
|
||||
import org.oxycblt.auxio.ui.ItemDiffCallback
|
||||
import org.oxycblt.auxio.ui.MenuItemListener
|
||||
import org.oxycblt.auxio.ui.MultiAdapter
|
||||
import org.oxycblt.auxio.ui.NewHeaderViewHolder
|
||||
import org.oxycblt.auxio.ui.SongViewHolder
|
||||
|
||||
/**
|
||||
* A Multi-ViewHolder adapter that displays the results of a search query.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class SearchAdapter(
|
||||
private val doOnClick: (data: Music) -> Unit,
|
||||
private val doOnLongClick: (view: View, data: Music) -> Unit
|
||||
) : ListAdapter<Item, RecyclerView.ViewHolder>(DiffCallback<Item>()) {
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return when (getItem(position)) {
|
||||
is Genre -> IntegerTable.ITEM_TYPE_GENRE
|
||||
is Artist -> IntegerTable.ITEM_TYPE_ARTIST
|
||||
is Album -> IntegerTable.ITEM_TYPE_ALBUM
|
||||
is Song -> IntegerTable.ITEM_TYPE_SONG
|
||||
is Header -> IntegerTable.ITEM_TYPE_HEADER
|
||||
else -> -1
|
||||
class NeoSearchAdapter(listener: MenuItemListener) :
|
||||
MultiAdapter<MenuItemListener>(listener, DIFFER) {
|
||||
override fun getCreatorFromItem(item: Item) =
|
||||
when (item) {
|
||||
is Song -> SongViewHolder.CREATOR
|
||||
is Album -> AlbumViewHolder.CREATOR
|
||||
is Artist -> ArtistViewHolder.CREATOR
|
||||
is Genre -> GenreViewHolder.CREATOR
|
||||
is Header -> NewHeaderViewHolder.CREATOR
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return when (viewType) {
|
||||
IntegerTable.ITEM_TYPE_GENRE ->
|
||||
GenreViewHolder.from(parent.context, doOnClick, doOnLongClick)
|
||||
IntegerTable.ITEM_TYPE_ARTIST ->
|
||||
ArtistViewHolder.from(parent.context, doOnClick, doOnLongClick)
|
||||
IntegerTable.ITEM_TYPE_ALBUM ->
|
||||
AlbumViewHolder.from(parent.context, doOnClick, doOnLongClick)
|
||||
IntegerTable.ITEM_TYPE_SONG ->
|
||||
SongViewHolder.from(parent.context, doOnClick, doOnLongClick)
|
||||
IntegerTable.ITEM_TYPE_HEADER -> HeaderViewHolder.from(parent.context)
|
||||
else -> error("Invalid ViewHolder item type")
|
||||
override fun getCreatorFromViewType(viewType: Int) =
|
||||
when (viewType) {
|
||||
SongViewHolder.CREATOR.viewType -> SongViewHolder.CREATOR
|
||||
AlbumViewHolder.CREATOR.viewType -> AlbumViewHolder.CREATOR
|
||||
ArtistViewHolder.CREATOR.viewType -> ArtistViewHolder.CREATOR
|
||||
GenreViewHolder.CREATOR.viewType -> GenreViewHolder.CREATOR
|
||||
NewHeaderViewHolder.CREATOR.viewType -> NewHeaderViewHolder.CREATOR
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
when (val item = getItem(position)) {
|
||||
is Genre -> (holder as GenreViewHolder).bind(item)
|
||||
is Artist -> (holder as ArtistViewHolder).bind(item)
|
||||
is Album -> (holder as AlbumViewHolder).bind(item)
|
||||
is Song -> (holder as SongViewHolder).bind(item)
|
||||
is Header -> (holder as HeaderViewHolder).bind(item)
|
||||
override fun onBind(
|
||||
viewHolder: RecyclerView.ViewHolder,
|
||||
item: Item,
|
||||
listener: MenuItemListener
|
||||
) {
|
||||
when (item) {
|
||||
is Song -> (viewHolder as SongViewHolder).bind(item, listener)
|
||||
is Album -> (viewHolder as AlbumViewHolder).bind(item, listener)
|
||||
is Artist -> (viewHolder as ArtistViewHolder).bind(item, listener)
|
||||
is Genre -> (viewHolder as GenreViewHolder).bind(item, listener)
|
||||
is Header -> (viewHolder as NewHeaderViewHolder).bind(item, Unit)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val DIFFER =
|
||||
object : ItemDiffCallback<Item>() {
|
||||
override fun areItemsTheSame(oldItem: Item, newItem: Item) =
|
||||
when {
|
||||
oldItem is Song && newItem is Song ->
|
||||
SongViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
|
||||
oldItem is Album && newItem is Album ->
|
||||
AlbumViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
|
||||
oldItem is Artist && newItem is Artist ->
|
||||
ArtistViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
|
||||
oldItem is Genre && newItem is Genre ->
|
||||
GenreViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
|
||||
oldItem is Header && newItem is Header ->
|
||||
NewHeaderViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ package org.oxycblt.auxio.search
|
|||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.core.view.postDelayed
|
||||
|
@ -33,44 +34,42 @@ import org.oxycblt.auxio.detail.DetailViewModel
|
|||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.Genre
|
||||
import org.oxycblt.auxio.music.Header
|
||||
import org.oxycblt.auxio.music.Item
|
||||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.music.MusicParent
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||
import org.oxycblt.auxio.ui.Header
|
||||
import org.oxycblt.auxio.ui.Item
|
||||
import org.oxycblt.auxio.ui.MenuItemListener
|
||||
import org.oxycblt.auxio.ui.ViewBindingFragment
|
||||
import org.oxycblt.auxio.ui.newMenu
|
||||
import org.oxycblt.auxio.util.applySpans
|
||||
import org.oxycblt.auxio.util.getSystemServiceSafe
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.util.requireAttached
|
||||
|
||||
/**
|
||||
* A [Fragment] that allows for the searching of the entire music library.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class SearchFragment : ViewBindingFragment<FragmentSearchBinding>() {
|
||||
class SearchFragment : ViewBindingFragment<FragmentSearchBinding>(), MenuItemListener {
|
||||
// SearchViewModel is only scoped to this Fragment
|
||||
private val searchModel: SearchViewModel by viewModels()
|
||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||
private val detailModel: DetailViewModel by activityViewModels()
|
||||
|
||||
private val searchAdapter = NeoSearchAdapter(this)
|
||||
private var imm: InputMethodManager? = null
|
||||
private var launchedKeyboard = false
|
||||
|
||||
override fun onCreateBinding(inflater: LayoutInflater) = FragmentSearchBinding.inflate(inflater)
|
||||
|
||||
override fun onBindingCreated(binding: FragmentSearchBinding, savedInstanceState: Bundle?) {
|
||||
val imm = requireContext().getSystemServiceSafe(InputMethodManager::class)
|
||||
|
||||
val searchAdapter =
|
||||
SearchAdapter(doOnClick = { item -> onItemSelection(item, imm) }, ::newMenu)
|
||||
|
||||
// --- UI SETUP --
|
||||
|
||||
binding.searchToolbar.apply {
|
||||
menu.findItem(searchModel.filterMode?.itemId ?: R.id.option_filter_all).isChecked = true
|
||||
|
||||
setNavigationOnClickListener {
|
||||
imm.hide()
|
||||
requireImm().hide()
|
||||
findNavController().navigateUp()
|
||||
}
|
||||
|
||||
|
@ -94,7 +93,9 @@ class SearchFragment : ViewBindingFragment<FragmentSearchBinding>() {
|
|||
if (!launchedKeyboard) {
|
||||
// Auto-open the keyboard when this view is shown
|
||||
requestFocus()
|
||||
postDelayed(200) { imm.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT) }
|
||||
postDelayed(200) {
|
||||
requireImm().showSoftInput(this, InputMethodManager.SHOW_IMPLICIT)
|
||||
}
|
||||
|
||||
launchedKeyboard = true
|
||||
}
|
||||
|
@ -107,13 +108,11 @@ class SearchFragment : ViewBindingFragment<FragmentSearchBinding>() {
|
|||
|
||||
// --- VIEWMODEL SETUP ---
|
||||
|
||||
searchModel.searchResults.observe(viewLifecycleOwner) { results ->
|
||||
updateResults(results, searchAdapter)
|
||||
}
|
||||
searchModel.searchResults.observe(viewLifecycleOwner, ::updateResults)
|
||||
|
||||
detailModel.navToItem.observe(viewLifecycleOwner) { item ->
|
||||
handleNavigation(item)
|
||||
imm.hide()
|
||||
requireImm().hide()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -122,10 +121,47 @@ class SearchFragment : ViewBindingFragment<FragmentSearchBinding>() {
|
|||
searchModel.setNavigating(false)
|
||||
}
|
||||
|
||||
private fun updateResults(results: List<Item>, searchAdapter: SearchAdapter) {
|
||||
override fun onDestroyBinding(binding: FragmentSearchBinding) {
|
||||
super.onDestroyBinding(binding)
|
||||
binding.searchRecycler.adapter = null
|
||||
imm = null
|
||||
}
|
||||
|
||||
override fun onItemClick(item: Item) {
|
||||
if (item is Song) {
|
||||
playbackModel.playSong(item)
|
||||
return
|
||||
}
|
||||
|
||||
if (item is MusicParent && !searchModel.isNavigating) {
|
||||
searchModel.setNavigating(true)
|
||||
|
||||
logD("Navigating to the detail fragment for ${item.rawName}")
|
||||
|
||||
findNavController()
|
||||
.navigate(
|
||||
when (item) {
|
||||
is Genre -> SearchFragmentDirections.actionShowGenre(item.id)
|
||||
is Artist -> SearchFragmentDirections.actionShowArtist(item.id)
|
||||
is Album -> SearchFragmentDirections.actionShowAlbum(item.id)
|
||||
})
|
||||
|
||||
requireImm().hide()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOpenMenu(item: Item, anchor: View) {
|
||||
newMenu(anchor, item)
|
||||
}
|
||||
|
||||
private fun updateResults(results: List<Item>) {
|
||||
if (isDetached) {
|
||||
error("Fragment not attached to activity")
|
||||
}
|
||||
|
||||
val binding = requireBinding()
|
||||
|
||||
searchAdapter.submitList(results) {
|
||||
searchAdapter.submitList(results.toMutableList()) {
|
||||
// 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
|
||||
// that doesn't seem possible.
|
||||
|
@ -146,41 +182,18 @@ class SearchFragment : ViewBindingFragment<FragmentSearchBinding>() {
|
|||
})
|
||||
}
|
||||
|
||||
private fun requireImm(): InputMethodManager {
|
||||
requireAttached()
|
||||
val instance = imm
|
||||
if (instance != null) {
|
||||
return instance
|
||||
}
|
||||
val newInstance = requireContext().getSystemServiceSafe(InputMethodManager::class)
|
||||
imm = newInstance
|
||||
return newInstance
|
||||
}
|
||||
|
||||
private fun InputMethodManager.hide() {
|
||||
hideSoftInputFromWindow(requireView().windowToken, InputMethodManager.HIDE_NOT_ALWAYS)
|
||||
}
|
||||
|
||||
/**
|
||||
* Function that handles when an [item] is selected. Handles all datatypes that are selectable.
|
||||
*/
|
||||
private fun onItemSelection(item: Music, imm: InputMethodManager) {
|
||||
if (item is Song) {
|
||||
playbackModel.playSong(item)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (!searchModel.isNavigating) {
|
||||
searchModel.setNavigating(true)
|
||||
|
||||
logD("Navigating to the detail fragment for ${item.rawName}")
|
||||
|
||||
findNavController()
|
||||
.navigate(
|
||||
when (item) {
|
||||
is Genre -> SearchFragmentDirections.actionShowGenre(item.id)
|
||||
is Artist -> SearchFragmentDirections.actionShowArtist(item.id)
|
||||
is Album -> SearchFragmentDirections.actionShowAlbum(item.id)
|
||||
|
||||
// If given model wasn't valid, then reset the navigation status
|
||||
// and abort the navigation.
|
||||
else -> {
|
||||
searchModel.setNavigating(false)
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
imm.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,12 +25,12 @@ import androidx.lifecycle.viewModelScope
|
|||
import java.text.Normalizer
|
||||
import kotlinx.coroutines.launch
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.music.Header
|
||||
import org.oxycblt.auxio.music.Item
|
||||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.music.MusicStore
|
||||
import org.oxycblt.auxio.settings.SettingsManager
|
||||
import org.oxycblt.auxio.ui.DisplayMode
|
||||
import org.oxycblt.auxio.ui.Header
|
||||
import org.oxycblt.auxio.ui.Item
|
||||
import org.oxycblt.auxio.ui.Sort
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
||||
|
|
|
@ -30,7 +30,6 @@ import org.oxycblt.auxio.detail.DetailViewModel
|
|||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.Genre
|
||||
import org.oxycblt.auxio.music.Item
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||
import org.oxycblt.auxio.util.showToast
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2021 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.ui
|
||||
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import org.oxycblt.auxio.music.Item
|
||||
|
||||
/**
|
||||
* A re-usable diff callback for all [Item] implementations. **Use this instead of creating a
|
||||
* DiffCallback for each adapter.**
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class DiffCallback<T : Item> : DiffUtil.ItemCallback<T>() {
|
||||
override fun areItemsTheSame(oldItem: T, newItem: T): Boolean {
|
||||
// Prevent ID collisions from occurring between datatypes.
|
||||
if (oldItem.javaClass != newItem.javaClass) return false
|
||||
|
||||
return oldItem.id == newItem.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: T, newItem: T): Boolean {
|
||||
// FIXME: Not correct, use item displays
|
||||
return oldItem.hashCode() == newItem.hashCode()
|
||||
}
|
||||
}
|
|
@ -38,8 +38,12 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
|||
clipToPadding = false
|
||||
}
|
||||
|
||||
override fun onApplyWindowInsets(insets: WindowInsets): WindowInsets {
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
setHasFixedSize(true)
|
||||
}
|
||||
|
||||
override fun onApplyWindowInsets(insets: WindowInsets): WindowInsets {
|
||||
updatePadding(
|
||||
initialPadding.left,
|
||||
initialPadding.top,
|
||||
|
|
194
app/src/main/java/org/oxycblt/auxio/ui/RecyclerFramework.kt
Normal file
194
app/src/main/java/org/oxycblt/auxio/ui/RecyclerFramework.kt
Normal file
|
@ -0,0 +1,194 @@
|
|||
/*
|
||||
* Copyright (c) 2021 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.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.recyclerview.widget.AsyncListDiffer
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
/**
|
||||
* An adapter enabling both asynchronous list updates and synchronous list updates.
|
||||
*
|
||||
* DiffUtil is a joke. The animations are chaotic and gaudy, it does not preserve the scroll
|
||||
* position of the RecyclerView, it refuses to play along with item movements, and the speed gains
|
||||
* are minimal. We would rather want to use the slower yet more reliable notifyX in nearly all
|
||||
* cases, however DiffUtil does have some use in places such as search, so we still want the ability
|
||||
* to use a differ while also having access to the basic adapter primitives as well. This class
|
||||
* achieves it through some terrible reflection magic, and is more or less the base for all adapters
|
||||
* in the app.
|
||||
*
|
||||
* TODO: Delegate data management to the internal adapters so that we can isolate the horrible hacks
|
||||
* to the specific adapters that use need them.
|
||||
*/
|
||||
abstract class HybridAdapter<T, VH : RecyclerView.ViewHolder>(
|
||||
diffCallback: DiffUtil.ItemCallback<T>
|
||||
) : RecyclerView.Adapter<VH>() {
|
||||
protected var mCurrentList = mutableListOf<T>()
|
||||
val currentList: List<T>
|
||||
get() = mCurrentList
|
||||
|
||||
// Probably okay to leak this here since it's just a callback.
|
||||
@Suppress("LeakingThis") private val differ = AsyncListDiffer(this, diffCallback)
|
||||
|
||||
protected fun getItem(position: Int): T = mCurrentList[position]
|
||||
|
||||
override fun getItemCount(): Int = mCurrentList.size
|
||||
|
||||
fun submitList(newData: List<T>, onDone: () -> Unit = {}) {
|
||||
if (newData != mCurrentList) {
|
||||
mCurrentList = newData.toMutableList()
|
||||
differ.submitList(newData, onDone)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("NotifyDatasetChanged")
|
||||
fun submitListHard(newList: List<T>) {
|
||||
if (newList != mCurrentList) {
|
||||
mCurrentList = newList.toMutableList()
|
||||
differ.rewriteListUnsafe(mCurrentList)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
fun moveItems(from: Int, to: Int) {
|
||||
mCurrentList.add(to, mCurrentList.removeAt(from))
|
||||
differ.rewriteListUnsafe(mCurrentList)
|
||||
notifyItemMoved(from, to)
|
||||
}
|
||||
|
||||
fun removeItem(at: Int) {
|
||||
mCurrentList.removeAt(at)
|
||||
differ.rewriteListUnsafe(mCurrentList)
|
||||
notifyItemRemoved(at)
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewrites the AsyncListDiffer's internal list, cancelling any diffs that are currently in
|
||||
* progress. I cannot describe in words how dangerous this is, but it's also the only thing I
|
||||
* can do to marry the adapter primitives with DiffUtil.
|
||||
*/
|
||||
private fun <T> AsyncListDiffer<T>.rewriteListUnsafe(newList: List<T>) {
|
||||
differMaxGenerationsField.set(this, (differMaxGenerationsField.get(this) as Int).inc())
|
||||
differListField.set(this, newList.toMutableList())
|
||||
differImmutableListField.set(this, newList)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val differListField =
|
||||
AsyncListDiffer::class.java.getDeclaredField("mList").apply { isAccessible = true }
|
||||
|
||||
private val differImmutableListField =
|
||||
AsyncListDiffer::class.java.getDeclaredField("mReadOnlyList").apply {
|
||||
isAccessible = true
|
||||
}
|
||||
|
||||
private val differMaxGenerationsField =
|
||||
AsyncListDiffer::class.java.getDeclaredField("mMaxScheduledGeneration").apply {
|
||||
isAccessible = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract class MonoAdapter<T, L, VH : BindingViewHolder<T, L>>(
|
||||
private val listener: L,
|
||||
diffCallback: DiffUtil.ItemCallback<T>
|
||||
) : HybridAdapter<T, VH>(diffCallback) {
|
||||
protected abstract val creator: BindingViewHolder.Creator<VH>
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
||||
creator.create(parent.context)
|
||||
|
||||
override fun onBindViewHolder(viewHolder: VH, position: Int) {
|
||||
viewHolder.bind(getItem(position), listener)
|
||||
}
|
||||
}
|
||||
|
||||
abstract class MultiAdapter<L>(private val listener: L, diffCallback: DiffUtil.ItemCallback<Item>) :
|
||||
HybridAdapter<Item, RecyclerView.ViewHolder>(diffCallback) {
|
||||
abstract fun getCreatorFromItem(
|
||||
item: Item
|
||||
): BindingViewHolder.Creator<out RecyclerView.ViewHolder>?
|
||||
abstract fun getCreatorFromViewType(
|
||||
viewType: Int
|
||||
): BindingViewHolder.Creator<out RecyclerView.ViewHolder>?
|
||||
abstract fun onBind(viewHolder: RecyclerView.ViewHolder, item: Item, listener: L)
|
||||
|
||||
override fun getItemViewType(position: Int) =
|
||||
requireNotNull(getCreatorFromItem(getItem(position))) {
|
||||
"Unable to get view type for item ${getItem(position)}"
|
||||
}
|
||||
.viewType
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
||||
requireNotNull(getCreatorFromViewType(viewType)) {
|
||||
"Unable to create viewholder for view type $viewType"
|
||||
}
|
||||
.create(parent.context)
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
onBind(holder, getItem(position), listener)
|
||||
}
|
||||
}
|
||||
|
||||
/** The base for all items in Auxio. */
|
||||
abstract class Item {
|
||||
/** A unique ID for this item. ***THIS IS NOT A MEDIASTORE ID!** */
|
||||
abstract val id: Long
|
||||
}
|
||||
|
||||
/** A data object used solely for the "Header" UI element. */
|
||||
data class Header(
|
||||
override val id: Long,
|
||||
/** The string resource used for the header. */
|
||||
@StringRes val string: Int
|
||||
) : Item()
|
||||
|
||||
abstract class ItemDiffCallback<T : Item> : DiffUtil.ItemCallback<T>() {
|
||||
override fun areContentsTheSame(oldItem: T, newItem: T): Boolean {
|
||||
if (oldItem.javaClass != newItem.javaClass) return false
|
||||
return oldItem.id == newItem.id
|
||||
}
|
||||
}
|
||||
|
||||
interface ItemClickListener {
|
||||
fun onItemClick(item: Item)
|
||||
}
|
||||
|
||||
interface MenuItemListener : ItemClickListener {
|
||||
fun onOpenMenu(item: Item, anchor: View)
|
||||
}
|
||||
|
||||
abstract class BindingViewHolder<T, L>(root: View) : RecyclerView.ViewHolder(root) {
|
||||
abstract fun bind(item: T, listener: L)
|
||||
|
||||
init {
|
||||
// Force the layout to *actually* be the screen width
|
||||
root.layoutParams =
|
||||
RecyclerView.LayoutParams(
|
||||
RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT)
|
||||
}
|
||||
|
||||
interface Creator<VH : RecyclerView.ViewHolder> {
|
||||
val viewType: Int
|
||||
fun create(context: Context): VH
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2021 Auxio Project
|
||||
* Copyright (c) 2022 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
|
||||
|
@ -18,99 +18,54 @@
|
|||
package org.oxycblt.auxio.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.TooltipCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import org.oxycblt.auxio.IntegerTable
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.coil.bindAlbumCover
|
||||
import org.oxycblt.auxio.coil.bindArtistImage
|
||||
import org.oxycblt.auxio.coil.bindGenreImage
|
||||
import org.oxycblt.auxio.databinding.ItemActionHeaderBinding
|
||||
import org.oxycblt.auxio.databinding.ItemHeaderBinding
|
||||
import org.oxycblt.auxio.databinding.ItemParentBinding
|
||||
import org.oxycblt.auxio.databinding.ItemSongBinding
|
||||
import org.oxycblt.auxio.music.ActionHeader
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.Genre
|
||||
import org.oxycblt.auxio.music.Header
|
||||
import org.oxycblt.auxio.music.Item
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.util.context
|
||||
import org.oxycblt.auxio.util.getPluralSafe
|
||||
import org.oxycblt.auxio.util.inflater
|
||||
import org.oxycblt.auxio.util.textSafe
|
||||
|
||||
/**
|
||||
* A [RecyclerView.ViewHolder] that streamlines a lot of the common things across all viewholders.
|
||||
* @param T The datatype, inheriting [Item] for this ViewHolder.
|
||||
* @param binding Basic [ViewDataBinding] required to set up click listeners & sizing.
|
||||
* @param doOnClick (Optional) Function that calls on a click.
|
||||
* @param doOnLongClick (Optional) Functions that calls on a long-click.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
abstract class BaseViewHolder<T : Item>(
|
||||
private val binding: ViewBinding,
|
||||
private val doOnClick: ((data: T) -> Unit)? = null,
|
||||
private val doOnLongClick: ((view: View, data: T) -> Unit)? = null
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
init {
|
||||
// Force the layout to *actually* be the screen width
|
||||
binding.root.layoutParams =
|
||||
RecyclerView.LayoutParams(
|
||||
RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT)
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind the viewholder with whatever [Item] instance that has been specified. Will call [onBind]
|
||||
* on the inheriting ViewHolder.
|
||||
* @param data Data that the viewholder should be bound with
|
||||
*/
|
||||
fun bind(data: T) {
|
||||
doOnClick?.let { onClick -> binding.root.setOnClickListener { onClick(data) } }
|
||||
|
||||
doOnLongClick?.let { onLongClick ->
|
||||
binding.root.setOnLongClickListener { view ->
|
||||
onLongClick(view, data)
|
||||
class SongViewHolder private constructor(private val binding: ItemSongBinding) :
|
||||
BindingViewHolder<Song, MenuItemListener>(binding.root) {
|
||||
override fun bind(item: Song, listener: MenuItemListener) {
|
||||
binding.songAlbumCover.bindAlbumCover(item)
|
||||
binding.songName.textSafe = item.resolvedName
|
||||
binding.songInfo.textSafe = item.resolvedArtistName
|
||||
binding.root.apply {
|
||||
setOnClickListener { listener.onItemClick(item) }
|
||||
setOnLongClickListener { view ->
|
||||
listener.onOpenMenu(item, view)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
onBind(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Function that performs binding operations unique to the inheriting viewholder. Add any
|
||||
* specialized code to an override of this instead of [BaseViewHolder] itself.
|
||||
*/
|
||||
protected abstract fun onBind(data: T)
|
||||
}
|
||||
|
||||
/** The Shared ViewHolder for a [Song]. Instantiation should be done with [from]. */
|
||||
class SongViewHolder
|
||||
private constructor(
|
||||
private val binding: ItemSongBinding,
|
||||
doOnClick: (data: Song) -> Unit,
|
||||
doOnLongClick: (view: View, data: Song) -> Unit
|
||||
) : BaseViewHolder<Song>(binding, doOnClick, doOnLongClick) {
|
||||
|
||||
override fun onBind(data: Song) {
|
||||
binding.songAlbumCover.bindAlbumCover(data)
|
||||
binding.songName.textSafe = data.resolvedName
|
||||
binding.songInfo.textSafe = data.resolvedArtistName
|
||||
}
|
||||
|
||||
companion object {
|
||||
/** Create an instance of [SongViewHolder] */
|
||||
fun from(
|
||||
context: Context,
|
||||
doOnClick: (data: Song) -> Unit,
|
||||
doOnLongClick: (view: View, data: Song) -> Unit
|
||||
): SongViewHolder {
|
||||
return SongViewHolder(
|
||||
ItemSongBinding.inflate(context.inflater), doOnClick, doOnLongClick)
|
||||
}
|
||||
val CREATOR =
|
||||
object : Creator<SongViewHolder> {
|
||||
override val viewType: Int
|
||||
get() = IntegerTable.ITEM_TYPE_SONG
|
||||
|
||||
override fun create(context: Context) =
|
||||
SongViewHolder(ItemSongBinding.inflate(context.inflater))
|
||||
}
|
||||
|
||||
val DIFFER =
|
||||
object : ItemDiffCallback<Song>() {
|
||||
override fun areItemsTheSame(oldItem: Song, newItem: Song) =
|
||||
oldItem.resolvedName == newItem.resolvedName &&
|
||||
oldItem.resolvedArtistName == oldItem.resolvedArtistName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -118,122 +73,142 @@ private constructor(
|
|||
class AlbumViewHolder
|
||||
private constructor(
|
||||
private val binding: ItemParentBinding,
|
||||
doOnClick: (data: Album) -> Unit,
|
||||
doOnLongClick: (view: View, data: Album) -> Unit
|
||||
) : BaseViewHolder<Album>(binding, doOnClick, doOnLongClick) {
|
||||
) : BindingViewHolder<Album, MenuItemListener>(binding.root) {
|
||||
|
||||
override fun onBind(data: Album) {
|
||||
binding.parentImage.bindAlbumCover(data)
|
||||
binding.parentName.textSafe = data.resolvedName
|
||||
binding.parentInfo.textSafe = data.resolvedArtistName
|
||||
override fun bind(item: Album, listener: MenuItemListener) {
|
||||
binding.parentImage.bindAlbumCover(item)
|
||||
binding.parentName.textSafe = item.resolvedName
|
||||
binding.parentInfo.textSafe = item.resolvedArtistName
|
||||
binding.root.apply {
|
||||
setOnClickListener { listener.onItemClick(item) }
|
||||
setOnLongClickListener { view ->
|
||||
listener.onOpenMenu(item, view)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
/** Create an instance of [AlbumViewHolder] */
|
||||
fun from(
|
||||
context: Context,
|
||||
doOnClick: (data: Album) -> Unit,
|
||||
doOnLongClick: (view: View, data: Album) -> Unit
|
||||
): AlbumViewHolder {
|
||||
return AlbumViewHolder(
|
||||
ItemParentBinding.inflate(context.inflater), doOnClick, doOnLongClick)
|
||||
}
|
||||
val CREATOR =
|
||||
object : Creator<AlbumViewHolder> {
|
||||
override val viewType: Int
|
||||
get() = IntegerTable.ITEM_TYPE_ALBUM
|
||||
|
||||
override fun create(context: Context) =
|
||||
AlbumViewHolder(ItemParentBinding.inflate(context.inflater))
|
||||
}
|
||||
|
||||
val DIFFER =
|
||||
object : ItemDiffCallback<Album>() {
|
||||
override fun areItemsTheSame(oldItem: Album, newItem: Album) =
|
||||
oldItem.resolvedName == newItem.resolvedName &&
|
||||
oldItem.resolvedArtistName == newItem.resolvedArtistName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** The Shared ViewHolder for a [Artist]. Instantiation should be done with [from]. */
|
||||
class ArtistViewHolder
|
||||
private constructor(
|
||||
private val binding: ItemParentBinding,
|
||||
doOnClick: (Artist) -> Unit,
|
||||
doOnLongClick: (view: View, data: Artist) -> Unit
|
||||
) : BaseViewHolder<Artist>(binding, doOnClick, doOnLongClick) {
|
||||
/** The Shared ViewHolder for a [Artist]. */
|
||||
class ArtistViewHolder private constructor(private val binding: ItemParentBinding) :
|
||||
BindingViewHolder<Artist, MenuItemListener>(binding.root) {
|
||||
|
||||
override fun onBind(data: Artist) {
|
||||
binding.parentImage.bindArtistImage(data)
|
||||
binding.parentName.textSafe = data.resolvedName
|
||||
override fun bind(item: Artist, listener: MenuItemListener) {
|
||||
binding.parentImage.bindArtistImage(item)
|
||||
binding.parentName.textSafe = item.resolvedName
|
||||
binding.parentInfo.textSafe =
|
||||
binding.context.getString(
|
||||
R.string.fmt_two,
|
||||
binding.context.getPluralSafe(R.plurals.fmt_album_count, data.albums.size),
|
||||
binding.context.getPluralSafe(R.plurals.fmt_song_count, data.songs.size))
|
||||
binding.context.getPluralSafe(R.plurals.fmt_album_count, item.albums.size),
|
||||
binding.context.getPluralSafe(R.plurals.fmt_song_count, item.songs.size))
|
||||
binding.root.apply {
|
||||
setOnClickListener { listener.onItemClick(item) }
|
||||
setOnLongClickListener { view ->
|
||||
listener.onOpenMenu(item, view)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
/** Create an instance of [ArtistViewHolder] */
|
||||
fun from(
|
||||
context: Context,
|
||||
doOnClick: (Artist) -> Unit,
|
||||
doOnLongClick: (view: View, data: Artist) -> Unit
|
||||
): ArtistViewHolder {
|
||||
return ArtistViewHolder(
|
||||
ItemParentBinding.inflate(context.inflater), doOnClick, doOnLongClick)
|
||||
}
|
||||
val CREATOR =
|
||||
object : Creator<ArtistViewHolder> {
|
||||
override val viewType: Int
|
||||
get() = IntegerTable.ITEM_TYPE_ARTIST
|
||||
|
||||
override fun create(context: Context) =
|
||||
ArtistViewHolder(ItemParentBinding.inflate(context.inflater))
|
||||
}
|
||||
|
||||
val DIFFER =
|
||||
object : ItemDiffCallback<Artist>() {
|
||||
override fun areItemsTheSame(oldItem: Artist, newItem: Artist) =
|
||||
oldItem.resolvedName == newItem.resolvedName &&
|
||||
oldItem.albums.size == newItem.albums.size &&
|
||||
newItem.songs.size == newItem.songs.size
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** The Shared ViewHolder for a [Genre]. Instantiation should be done with [from]. */
|
||||
/** The Shared ViewHolder for a [Genre]. */
|
||||
class GenreViewHolder
|
||||
private constructor(
|
||||
private val binding: ItemParentBinding,
|
||||
doOnClick: (Genre) -> Unit,
|
||||
doOnLongClick: (view: View, data: Genre) -> Unit
|
||||
) : BaseViewHolder<Genre>(binding, doOnClick, doOnLongClick) {
|
||||
) : BindingViewHolder<Genre, MenuItemListener>(binding.root) {
|
||||
|
||||
override fun onBind(data: Genre) {
|
||||
binding.parentImage.bindGenreImage(data)
|
||||
binding.parentName.textSafe = data.resolvedName
|
||||
override fun bind(item: Genre, listener: MenuItemListener) {
|
||||
binding.parentImage.bindGenreImage(item)
|
||||
binding.parentName.textSafe = item.resolvedName
|
||||
binding.parentInfo.textSafe =
|
||||
binding.context.getPluralSafe(R.plurals.fmt_song_count, data.songs.size)
|
||||
binding.context.getPluralSafe(R.plurals.fmt_song_count, item.songs.size)
|
||||
binding.root.apply {
|
||||
setOnClickListener { listener.onItemClick(item) }
|
||||
setOnLongClickListener { view ->
|
||||
listener.onOpenMenu(item, view)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
/** Create an instance of [GenreViewHolder] */
|
||||
fun from(
|
||||
context: Context,
|
||||
doOnClick: (Genre) -> Unit,
|
||||
doOnLongClick: (view: View, data: Genre) -> Unit
|
||||
): GenreViewHolder {
|
||||
return GenreViewHolder(
|
||||
ItemParentBinding.inflate(context.inflater), doOnClick, doOnLongClick)
|
||||
}
|
||||
val CREATOR =
|
||||
object : Creator<GenreViewHolder> {
|
||||
override val viewType: Int
|
||||
get() = IntegerTable.ITEM_TYPE_GENRE
|
||||
|
||||
override fun create(context: Context) =
|
||||
GenreViewHolder(ItemParentBinding.inflate(context.inflater))
|
||||
}
|
||||
|
||||
val DIFFER =
|
||||
object : ItemDiffCallback<Genre>() {
|
||||
override fun areItemsTheSame(oldItem: Genre, newItem: Genre): Boolean =
|
||||
oldItem.resolvedName == newItem.resolvedName &&
|
||||
oldItem.songs.size == newItem.songs.size
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** The Shared ViewHolder for a [Header]. Instantiation should be done with [from] */
|
||||
class HeaderViewHolder private constructor(private val binding: ItemHeaderBinding) :
|
||||
BaseViewHolder<Header>(binding) {
|
||||
class NewHeaderViewHolder private constructor(private val binding: ItemHeaderBinding) :
|
||||
BindingViewHolder<Header, Unit>(binding.root) {
|
||||
|
||||
override fun onBind(data: Header) {
|
||||
binding.title.textSafe = binding.context.getString(data.string)
|
||||
override fun bind(item: Header, listener: Unit) {
|
||||
binding.title.textSafe = binding.context.getString(item.string)
|
||||
}
|
||||
|
||||
companion object {
|
||||
/** Create an instance of [HeaderViewHolder] */
|
||||
fun from(context: Context): HeaderViewHolder {
|
||||
return HeaderViewHolder(ItemHeaderBinding.inflate(context.inflater))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** The Shared ViewHolder for an [ActionHeader]. Instantiation should be done with [from] */
|
||||
class ActionHeaderViewHolder private constructor(private val binding: ItemActionHeaderBinding) :
|
||||
BaseViewHolder<ActionHeader>(binding) {
|
||||
|
||||
override fun onBind(data: ActionHeader) {
|
||||
binding.headerTitle.textSafe = binding.context.getString(data.string)
|
||||
binding.headerButton.apply {
|
||||
setImageResource(data.icon)
|
||||
contentDescription = context.getString(data.desc)
|
||||
TooltipCompat.setTooltipText(this, contentDescription)
|
||||
setOnClickListener(data.onClick)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
/** Create an instance of [ActionHeaderViewHolder] */
|
||||
fun from(context: Context): ActionHeaderViewHolder {
|
||||
return ActionHeaderViewHolder(ItemActionHeaderBinding.inflate(context.inflater))
|
||||
}
|
||||
val CREATOR =
|
||||
object : Creator<NewHeaderViewHolder> {
|
||||
override val viewType: Int
|
||||
get() = IntegerTable.ITEM_TYPE_HEADER
|
||||
|
||||
override fun create(context: Context) =
|
||||
NewHeaderViewHolder(ItemHeaderBinding.inflate(context.inflater))
|
||||
}
|
||||
|
||||
val DIFFER =
|
||||
object : ItemDiffCallback<Header>() {
|
||||
override fun areItemsTheSame(oldItem: Header, newItem: Header): Boolean =
|
||||
oldItem.string == newItem.string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ package org.oxycblt.auxio.util
|
|||
import android.database.Cursor
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.os.Looper
|
||||
import androidx.fragment.app.Fragment
|
||||
|
||||
/**
|
||||
* Shortcut for querying all items in a database and running [block] with the cursor returned. Will
|
||||
|
@ -34,3 +35,9 @@ fun assertBackgroundThread() {
|
|||
"This operation must be ran on a background thread"
|
||||
}
|
||||
}
|
||||
|
||||
fun Fragment.requireAttached() {
|
||||
if (isDetached) {
|
||||
error("Fragment is detached from activity")
|
||||
}
|
||||
}
|
|
@ -123,6 +123,13 @@ fun RecyclerView.applySpans(shouldBeFullWidth: ((Int) -> Boolean)? = null) {
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun RecyclerView.getViewHolderAt(pos: Int): RecyclerView.ViewHolder? {
|
||||
return layoutManager?.run {
|
||||
findViewByPosition(pos)?.let { child -> getChildViewHolder(child) }
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns whether a recyclerview can scroll. */
|
||||
fun RecyclerView.canScroll(): Boolean = computeVerticalScrollRange() > height
|
||||
|
||||
|
|
|
@ -26,11 +26,12 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:maxLines="1"
|
||||
android:minWidth="@dimen/size_track_number"
|
||||
android:minWidth="@dimen/size_btn_small"
|
||||
android:minHeight="@dimen/size_btn_small"
|
||||
android:textAlignment="center"
|
||||
android:textAppearance="@style/TextAppearance.Auxio.BodyLarge"
|
||||
android:textColor="@color/sel_accented_secondary"
|
||||
android:textSize="@dimen/text_size_ext_title_mid_large"
|
||||
android:textSize="@dimen/text_size_track_number"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/song_name"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
|
|
|
@ -29,8 +29,8 @@
|
|||
app:layout_constraintBottom_toTopOf="@id/header_divider"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:ignore="ContentDescription"
|
||||
tools:src="@drawable/ic_sort" />
|
||||
android:contentDescription="@string/lbl_sort"
|
||||
android:src="@drawable/ic_sort" />
|
||||
|
||||
<com.google.android.material.divider.MaterialDivider
|
||||
android:id="@+id/header_divider"
|
|
@ -26,6 +26,7 @@
|
|||
|
||||
<dimen name="text_size_ext_label_larger">16sp</dimen>
|
||||
<dimen name="text_size_ext_title_mid_large">18sp</dimen>
|
||||
<dimen name="text_size_track_number">22sp</dimen>
|
||||
|
||||
<!-- Misc -->
|
||||
<dimen name="elevation_small">2dp</dimen>
|
||||
|
|
Loading…
Reference in a new issue