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 ]
|
- Updated translations [Konstantin Tutsch -> German, cccClyde -> Chinese ]
|
||||||
- Switched to spotless and ktfmt instead of ktlint
|
- Switched to spotless and ktfmt instead of ktlint
|
||||||
- Migrated constants to centralized table
|
- Migrated constants to centralized table
|
||||||
|
- Removed databinding [Greatly reduces compile times]
|
||||||
- A bunch of internal view implementation improvements
|
- A bunch of internal view implementation improvements
|
||||||
|
|
||||||
## v2.2.2
|
## v2.2.2
|
||||||
|
|
|
@ -28,7 +28,7 @@ import org.oxycblt.auxio.coil.GenreImageFetcher
|
||||||
import org.oxycblt.auxio.coil.MusicKeyer
|
import org.oxycblt.auxio.coil.MusicKeyer
|
||||||
import org.oxycblt.auxio.settings.SettingsManager
|
import org.oxycblt.auxio.settings.SettingsManager
|
||||||
|
|
||||||
/** TODO: Rework RecyclerView management and item dragging */
|
/** TODO: Rework null-safety/usage of requireNotNull */
|
||||||
@Suppress("UNUSED")
|
@Suppress("UNUSED")
|
||||||
class AuxioApp : Application(), ImageLoaderFactory {
|
class AuxioApp : Application(), ImageLoaderFactory {
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
|
|
|
@ -29,8 +29,8 @@ object IntegerTable {
|
||||||
const val ITEM_TYPE_GENRE = 0xA003
|
const val ITEM_TYPE_GENRE = 0xA003
|
||||||
/** HeaderViewHolder */
|
/** HeaderViewHolder */
|
||||||
const val ITEM_TYPE_HEADER = 0xA004
|
const val ITEM_TYPE_HEADER = 0xA004
|
||||||
/** ActionHeaderViewHolder */
|
/** SortHeaderViewHolder */
|
||||||
const val ITEM_TYPE_ACTION_HEADER = 0xA005
|
const val ITEM_TYPE_SORT_HEADER = 0xA005
|
||||||
|
|
||||||
/** AlbumDetailViewHolder */
|
/** AlbumDetailViewHolder */
|
||||||
const val ITEM_TYPE_ALBUM_DETAIL = 0xA006
|
const val ITEM_TYPE_ALBUM_DETAIL = 0xA006
|
||||||
|
|
|
@ -19,6 +19,7 @@ package org.oxycblt.auxio.detail
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
import androidx.core.view.children
|
import androidx.core.view.children
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.navigation.fragment.navArgs
|
import androidx.navigation.fragment.navArgs
|
||||||
|
@ -26,15 +27,17 @@ import androidx.recyclerview.widget.LinearSmoothScroller
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.FragmentDetailBinding
|
import org.oxycblt.auxio.databinding.FragmentDetailBinding
|
||||||
import org.oxycblt.auxio.detail.recycler.AlbumDetailAdapter
|
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.Album
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Header
|
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.playback.state.PlaybackMode
|
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.ui.newMenu
|
||||||
|
import org.oxycblt.auxio.util.applySpans
|
||||||
import org.oxycblt.auxio.util.canScroll
|
import org.oxycblt.auxio.util.canScroll
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.logW
|
import org.oxycblt.auxio.util.logW
|
||||||
|
@ -44,28 +47,22 @@ import org.oxycblt.auxio.util.showToast
|
||||||
* The [DetailFragment] for an album.
|
* The [DetailFragment] for an album.
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class AlbumDetailFragment : DetailFragment() {
|
class AlbumDetailFragment : DetailFragment(), AlbumDetailItemListener {
|
||||||
private val args: AlbumDetailFragmentArgs by navArgs()
|
private val args: AlbumDetailFragmentArgs by navArgs()
|
||||||
|
private val detailAdapter = AlbumDetailAdapter(this)
|
||||||
|
|
||||||
override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) {
|
override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) {
|
||||||
detailModel.setAlbumId(args.albumId)
|
detailModel.setAlbumId(args.albumId)
|
||||||
|
|
||||||
val detailAdapter =
|
setupToolbar(detailModel.currentAlbum.value!!, R.menu.menu_album_detail) { itemId ->
|
||||||
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 ->
|
|
||||||
when (itemId) {
|
when (itemId) {
|
||||||
R.id.action_play_next -> {
|
R.id.action_play_next -> {
|
||||||
playbackModel.playNext(detailModel.curAlbum.value!!)
|
playbackModel.playNext(detailModel.currentAlbum.value!!)
|
||||||
requireContext().showToast(R.string.lbl_queue_added)
|
requireContext().showToast(R.string.lbl_queue_added)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
R.id.action_queue_add -> {
|
R.id.action_queue_add -> {
|
||||||
playbackModel.addToQueue(detailModel.curAlbum.value!!)
|
playbackModel.addToQueue(detailModel.currentAlbum.value!!)
|
||||||
requireContext().showToast(R.string.lbl_queue_added)
|
requireContext().showToast(R.string.lbl_queue_added)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
@ -73,20 +70,17 @@ class AlbumDetailFragment : DetailFragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setupRecycler(detailAdapter) { pos ->
|
requireBinding().detailRecycler.apply {
|
||||||
|
adapter = detailAdapter
|
||||||
|
applySpans { pos ->
|
||||||
val item = detailAdapter.currentList[pos]
|
val item = detailAdapter.currentList[pos]
|
||||||
item is Header || item is ActionHeader || item is Album
|
item is Header || item is SortHeader || item is Album
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- VIEWMODEL SETUP ---
|
// -- VIEWMODEL SETUP ---
|
||||||
|
|
||||||
detailModel.albumData.observe(viewLifecycleOwner, detailAdapter::submitList)
|
detailModel.albumData.observe(viewLifecycleOwner) { list -> detailAdapter.submitList(list) }
|
||||||
|
|
||||||
detailModel.showMenu.observe(viewLifecycleOwner) { config ->
|
|
||||||
if (config != null) {
|
|
||||||
showMenu(config) { id -> id == R.id.option_sort_asc }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
detailModel.navToItem.observe(viewLifecycleOwner) { item ->
|
detailModel.navToItem.observe(viewLifecycleOwner) { item ->
|
||||||
handleNavigation(item, detailAdapter)
|
handleNavigation(item, detailAdapter)
|
||||||
|
@ -96,13 +90,46 @@ class AlbumDetailFragment : DetailFragment() {
|
||||||
playbackModel.song.observe(viewLifecycleOwner) { song -> updateSong(song, detailAdapter) }
|
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) {
|
private fun handleNavigation(item: Music?, adapter: AlbumDetailAdapter) {
|
||||||
val binding = requireBinding()
|
val binding = requireBinding()
|
||||||
when (item) {
|
when (item) {
|
||||||
// Songs should be scrolled to if the album matches, or a new detail
|
// Songs should be scrolled to if the album matches, or a new detail
|
||||||
// fragment should be launched otherwise.
|
// fragment should be launched otherwise.
|
||||||
is Song -> {
|
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")
|
logD("Navigating to a song in this album")
|
||||||
scrollToItem(item.id, adapter)
|
scrollToItem(item.id, adapter)
|
||||||
detailModel.finishNavToItem()
|
detailModel.finishNavToItem()
|
||||||
|
@ -116,7 +143,7 @@ class AlbumDetailFragment : DetailFragment() {
|
||||||
// If the album matches, no need to do anything. Otherwise launch a new
|
// If the album matches, no need to do anything. Otherwise launch a new
|
||||||
// detail fragment.
|
// detail fragment.
|
||||||
is Album -> {
|
is Album -> {
|
||||||
if (detailModel.curAlbum.value!!.id == item.id) {
|
if (detailModel.currentAlbum.value!!.id == item.id) {
|
||||||
logD("Navigating to the top of this album")
|
logD("Navigating to the top of this album")
|
||||||
binding.detailRecycler.scrollToPosition(0)
|
binding.detailRecycler.scrollToPosition(0)
|
||||||
detailModel.finishNavToItem()
|
detailModel.finishNavToItem()
|
||||||
|
@ -169,7 +196,7 @@ class AlbumDetailFragment : DetailFragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (playbackModel.playbackMode.value == PlaybackMode.IN_ALBUM &&
|
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)
|
adapter.highlightSong(song, binding.detailRecycler)
|
||||||
} else {
|
} else {
|
||||||
// Clear the ViewHolders if the mode isn't ALL_SONGS
|
// Clear the ViewHolders if the mode isn't ALL_SONGS
|
||||||
|
|
|
@ -18,21 +18,24 @@
|
||||||
package org.oxycblt.auxio.detail
|
package org.oxycblt.auxio.detail
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.navigation.fragment.navArgs
|
import androidx.navigation.fragment.navArgs
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.FragmentDetailBinding
|
import org.oxycblt.auxio.databinding.FragmentDetailBinding
|
||||||
import org.oxycblt.auxio.detail.recycler.ArtistDetailAdapter
|
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.Album
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Header
|
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.playback.state.PlaybackMode
|
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.ui.newMenu
|
||||||
|
import org.oxycblt.auxio.util.applySpans
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.logW
|
import org.oxycblt.auxio.util.logW
|
||||||
|
|
||||||
|
@ -40,40 +43,27 @@ import org.oxycblt.auxio.util.logW
|
||||||
* The [DetailFragment] for an artist.
|
* The [DetailFragment] for an artist.
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class ArtistDetailFragment : DetailFragment() {
|
class ArtistDetailFragment : DetailFragment(), DetailItemListener {
|
||||||
private val args: ArtistDetailFragmentArgs by navArgs()
|
private val args: ArtistDetailFragmentArgs by navArgs()
|
||||||
|
private val detailAdapter = ArtistDetailAdapter(this)
|
||||||
|
|
||||||
override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) {
|
override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) {
|
||||||
detailModel.setArtistId(args.artistId)
|
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!!)
|
setupToolbar(detailModel.currentArtist.value!!)
|
||||||
setupRecycler(detailAdapter) { pos ->
|
requireBinding().detailRecycler.apply {
|
||||||
|
adapter = detailAdapter
|
||||||
|
applySpans { pos ->
|
||||||
// If the item is an ActionHeader we need to also make the item full-width
|
// If the item is an ActionHeader we need to also make the item full-width
|
||||||
val item = detailAdapter.currentList[pos]
|
val item = detailAdapter.currentList[pos]
|
||||||
item is Header || item is ActionHeader || item is Artist
|
item is Header || item is SortHeader || item is Artist
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- VIEWMODEL SETUP ---
|
// --- VIEWMODEL SETUP ---
|
||||||
|
|
||||||
detailModel.artistData.observe(viewLifecycleOwner, detailAdapter::submitList)
|
detailModel.artistData.observe(viewLifecycleOwner) { list ->
|
||||||
|
detailAdapter.submitList(list)
|
||||||
detailModel.showMenu.observe(viewLifecycleOwner) { config ->
|
|
||||||
if (config != null) {
|
|
||||||
showMenu(config) { id -> id != R.id.option_sort_artist }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
detailModel.navToItem.observe(viewLifecycleOwner, ::handleNavigation)
|
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?) {
|
private fun handleNavigation(item: Music?) {
|
||||||
val binding = requireBinding()
|
val binding = requireBinding()
|
||||||
|
|
||||||
|
|
|
@ -18,19 +18,19 @@
|
||||||
package org.oxycblt.auxio.detail
|
package org.oxycblt.auxio.detail
|
||||||
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
import androidx.annotation.MenuRes
|
import androidx.annotation.MenuRes
|
||||||
import androidx.appcompat.widget.PopupMenu
|
import androidx.appcompat.widget.PopupMenu
|
||||||
import androidx.core.view.children
|
import androidx.core.view.children
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.FragmentDetailBinding
|
import org.oxycblt.auxio.databinding.FragmentDetailBinding
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
|
import org.oxycblt.auxio.ui.Sort
|
||||||
import org.oxycblt.auxio.ui.ViewBindingFragment
|
import org.oxycblt.auxio.ui.ViewBindingFragment
|
||||||
import org.oxycblt.auxio.util.applySpans
|
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -49,10 +49,9 @@ abstract class DetailFragment : ViewBindingFragment<FragmentDetailBinding>() {
|
||||||
detailModel.setNavigating(false)
|
detailModel.setNavigating(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStop() {
|
override fun onDestroyBinding(binding: FragmentDetailBinding) {
|
||||||
super.onStop()
|
super.onDestroyBinding(binding)
|
||||||
// Cancel all pending menus when this fragment stops to prevent bugs/crashes
|
binding.detailRecycler.adapter = null
|
||||||
detailModel.finishShowMenu(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]
|
* Shortcut method for spinning up the sorting [PopupMenu]
|
||||||
* @param config The initial configuration to apply to the menu. This is provided by
|
* @param anchor The view to anchor the sort menu to
|
||||||
* [DetailViewModel.showMenu].
|
* @param sort The initial sort
|
||||||
|
* @param onConfirm What to do when the sort is confirmed
|
||||||
* @param showItem Which menu items to keep
|
* @param showItem Which menu items to keep
|
||||||
*/
|
*/
|
||||||
protected fun showMenu(
|
protected fun showSortMenu(
|
||||||
config: DetailViewModel.MenuConfig,
|
anchor: View,
|
||||||
showItem: ((Int) -> Boolean)? = null
|
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)
|
inflate(R.menu.menu_detail_sort)
|
||||||
|
|
||||||
setOnMenuItemClickListener { item ->
|
setOnMenuItemClickListener { item ->
|
||||||
if (item.itemId == R.id.option_sort_asc) {
|
if (item.itemId == R.id.option_sort_asc) {
|
||||||
item.isChecked = !item.isChecked
|
item.isChecked = !item.isChecked
|
||||||
detailModel.finishShowMenu(config.sortMode.ascending(item.isChecked))
|
onConfirm(sort.ascending(item.isChecked))
|
||||||
} else {
|
} else {
|
||||||
item.isChecked = true
|
item.isChecked = true
|
||||||
detailModel.finishShowMenu(config.sortMode.assignId(item.itemId))
|
onConfirm(requireNotNull(sort.assignId(item.itemId)))
|
||||||
}
|
}
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
setOnDismissListener { detailModel.finishShowMenu(null) }
|
|
||||||
|
|
||||||
if (showItem != null) {
|
if (showItem != null) {
|
||||||
for (item in menu.children) {
|
for (item in menu.children) {
|
||||||
item.isVisible = showItem(item.itemId)
|
item.isVisible = showItem(item.itemId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
menu.findItem(config.sortMode.itemId).isChecked = true
|
menu.findItem(sort.itemId).isChecked = true
|
||||||
menu.findItem(R.id.option_sort_asc).isChecked = config.sortMode.isAscending
|
menu.findItem(R.id.option_sort_asc).isChecked = sort.isAscending
|
||||||
|
|
||||||
show()
|
show()
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,21 +17,19 @@
|
||||||
|
|
||||||
package org.oxycblt.auxio.detail
|
package org.oxycblt.auxio.detail
|
||||||
|
|
||||||
import android.view.View
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import org.oxycblt.auxio.R
|
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.Album
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Genre
|
import org.oxycblt.auxio.music.Genre
|
||||||
import org.oxycblt.auxio.music.Header
|
|
||||||
import org.oxycblt.auxio.music.Item
|
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.MusicStore
|
import org.oxycblt.auxio.music.MusicStore
|
||||||
import org.oxycblt.auxio.settings.SettingsManager
|
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.ui.Sort
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
|
@ -44,14 +42,23 @@ import org.oxycblt.auxio.util.logD
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class DetailViewModel : ViewModel() {
|
class DetailViewModel : ViewModel() {
|
||||||
|
private val settingsManager = SettingsManager.getInstance()
|
||||||
|
|
||||||
private val mCurrentAlbum = MutableLiveData<Album?>()
|
private val mCurrentAlbum = MutableLiveData<Album?>()
|
||||||
val curAlbum: LiveData<Album?>
|
val currentAlbum: LiveData<Album?>
|
||||||
get() = mCurrentAlbum
|
get() = mCurrentAlbum
|
||||||
|
|
||||||
private val mAlbumData = MutableLiveData(listOf<Item>())
|
private val mAlbumData = MutableLiveData(listOf<Item>())
|
||||||
val albumData: LiveData<List<Item>>
|
val albumData: LiveData<List<Item>>
|
||||||
get() = mAlbumData
|
get() = mAlbumData
|
||||||
|
|
||||||
|
var albumSort: Sort
|
||||||
|
get() = settingsManager.detailAlbumSort
|
||||||
|
set(value) {
|
||||||
|
settingsManager.detailAlbumSort = value
|
||||||
|
refreshAlbumData()
|
||||||
|
}
|
||||||
|
|
||||||
private val mCurrentArtist = MutableLiveData<Artist?>()
|
private val mCurrentArtist = MutableLiveData<Artist?>()
|
||||||
val currentArtist: LiveData<Artist?>
|
val currentArtist: LiveData<Artist?>
|
||||||
get() = mCurrentArtist
|
get() = mCurrentArtist
|
||||||
|
@ -59,6 +66,13 @@ class DetailViewModel : ViewModel() {
|
||||||
private val mArtistData = MutableLiveData(listOf<Item>())
|
private val mArtistData = MutableLiveData(listOf<Item>())
|
||||||
val artistData: LiveData<List<Item>> = mArtistData
|
val artistData: LiveData<List<Item>> = mArtistData
|
||||||
|
|
||||||
|
var artistSort: Sort
|
||||||
|
get() = settingsManager.detailArtistSort
|
||||||
|
set(value) {
|
||||||
|
settingsManager.detailArtistSort = value
|
||||||
|
refreshArtistData()
|
||||||
|
}
|
||||||
|
|
||||||
private val mCurrentGenre = MutableLiveData<Genre?>()
|
private val mCurrentGenre = MutableLiveData<Genre?>()
|
||||||
val currentGenre: LiveData<Genre?>
|
val currentGenre: LiveData<Genre?>
|
||||||
get() = mCurrentGenre
|
get() = mCurrentGenre
|
||||||
|
@ -66,10 +80,12 @@ class DetailViewModel : ViewModel() {
|
||||||
private val mGenreData = MutableLiveData(listOf<Item>())
|
private val mGenreData = MutableLiveData(listOf<Item>())
|
||||||
val genreData: LiveData<List<Item>> = mGenreData
|
val genreData: LiveData<List<Item>> = mGenreData
|
||||||
|
|
||||||
data class MenuConfig(val anchor: View, val sortMode: Sort)
|
var genreSort: Sort
|
||||||
|
get() = settingsManager.detailGenreSort
|
||||||
private val mShowMenu = MutableLiveData<MenuConfig?>(null)
|
set(value) {
|
||||||
val showMenu: LiveData<MenuConfig?> = mShowMenu
|
settingsManager.detailGenreSort = value
|
||||||
|
refreshGenreData()
|
||||||
|
}
|
||||||
|
|
||||||
private val mNavToItem = MutableLiveData<Music?>()
|
private val mNavToItem = MutableLiveData<Music?>()
|
||||||
|
|
||||||
|
@ -80,9 +96,6 @@ class DetailViewModel : ViewModel() {
|
||||||
var isNavigating = false
|
var isNavigating = false
|
||||||
private set
|
private set
|
||||||
|
|
||||||
private var currentMenuContext: DisplayMode? = null
|
|
||||||
private val settingsManager = SettingsManager.getInstance()
|
|
||||||
|
|
||||||
fun setAlbumId(id: Long) {
|
fun setAlbumId(id: Long) {
|
||||||
if (mCurrentAlbum.value?.id == id) return
|
if (mCurrentAlbum.value?.id == id) return
|
||||||
val musicStore = MusicStore.requireInstance()
|
val musicStore = MusicStore.requireInstance()
|
||||||
|
@ -104,32 +117,6 @@ class DetailViewModel : ViewModel() {
|
||||||
refreshGenreData()
|
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 */
|
/** Navigate to an item, whether a song/album/artist */
|
||||||
fun navToItem(item: Music) {
|
fun navToItem(item: Music) {
|
||||||
mNavToItem.value = item
|
mNavToItem.value = item
|
||||||
|
@ -150,17 +137,7 @@ class DetailViewModel : ViewModel() {
|
||||||
val genre = requireNotNull(currentGenre.value)
|
val genre = requireNotNull(currentGenre.value)
|
||||||
val data = mutableListOf<Item>(genre)
|
val data = mutableListOf<Item>(genre)
|
||||||
|
|
||||||
data.add(
|
data.add(SortHeader(-2, R.string.lbl_songs))
|
||||||
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.addAll(settingsManager.detailGenreSort.genre(currentGenre.value!!))
|
data.addAll(settingsManager.detailGenreSort.genre(currentGenre.value!!))
|
||||||
|
|
||||||
mGenreData.value = data
|
mGenreData.value = data
|
||||||
|
@ -171,21 +148,9 @@ class DetailViewModel : ViewModel() {
|
||||||
val artist = requireNotNull(currentArtist.value)
|
val artist = requireNotNull(currentArtist.value)
|
||||||
val data = mutableListOf<Item>(artist)
|
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.addAll(Sort.ByYear(false).albums(artist.albums))
|
||||||
|
data.add(SortHeader(-3, R.string.lbl_songs))
|
||||||
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.addAll(settingsManager.detailArtistSort.artist(artist))
|
data.addAll(settingsManager.detailArtistSort.artist(artist))
|
||||||
|
|
||||||
mArtistData.value = data.toList()
|
mArtistData.value = data.toList()
|
||||||
|
@ -193,21 +158,11 @@ class DetailViewModel : ViewModel() {
|
||||||
|
|
||||||
private fun refreshAlbumData() {
|
private fun refreshAlbumData() {
|
||||||
logD("Refreshing album data")
|
logD("Refreshing album data")
|
||||||
val album = requireNotNull(curAlbum.value)
|
val album = requireNotNull(currentAlbum.value)
|
||||||
val data = mutableListOf<Item>(album)
|
val data = mutableListOf<Item>(album)
|
||||||
|
|
||||||
data.add(
|
data.add(SortHeader(id = -2, R.string.lbl_albums))
|
||||||
ActionHeader(
|
data.addAll(settingsManager.detailAlbumSort.album(currentAlbum.value!!))
|
||||||
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!!))
|
|
||||||
|
|
||||||
mAlbumData.value = data
|
mAlbumData.value = data
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,20 +18,23 @@
|
||||||
package org.oxycblt.auxio.detail
|
package org.oxycblt.auxio.detail
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.navigation.fragment.navArgs
|
import androidx.navigation.fragment.navArgs
|
||||||
import org.oxycblt.auxio.databinding.FragmentDetailBinding
|
import org.oxycblt.auxio.databinding.FragmentDetailBinding
|
||||||
|
import org.oxycblt.auxio.detail.recycler.DetailItemListener
|
||||||
import org.oxycblt.auxio.detail.recycler.GenreDetailAdapter
|
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.Album
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Genre
|
import org.oxycblt.auxio.music.Genre
|
||||||
import org.oxycblt.auxio.music.Header
|
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.playback.state.PlaybackMode
|
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.ui.newMenu
|
||||||
|
import org.oxycblt.auxio.util.applySpans
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.logW
|
import org.oxycblt.auxio.util.logW
|
||||||
|
|
||||||
|
@ -39,37 +42,54 @@ import org.oxycblt.auxio.util.logW
|
||||||
* The [DetailFragment] for a genre.
|
* The [DetailFragment] for a genre.
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class GenreDetailFragment : DetailFragment() {
|
class GenreDetailFragment : DetailFragment(), DetailItemListener {
|
||||||
private val args: GenreDetailFragmentArgs by navArgs()
|
private val args: GenreDetailFragmentArgs by navArgs()
|
||||||
|
private val detailAdapter = GenreDetailAdapter(this)
|
||||||
|
|
||||||
override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) {
|
override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) {
|
||||||
detailModel.setGenreId(args.genreId)
|
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!!)
|
setupToolbar(detailModel.currentGenre.value!!)
|
||||||
setupRecycler(detailAdapter) { pos ->
|
binding.detailRecycler.apply {
|
||||||
|
adapter = detailAdapter
|
||||||
|
applySpans { pos ->
|
||||||
val item = detailAdapter.currentList[pos]
|
val item = detailAdapter.currentList[pos]
|
||||||
item is Header || item is ActionHeader || item is Genre
|
item is Header || item is SortHeader || item is Genre
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- VIEWMODEL SETUP ---
|
// --- VIEWMODEL SETUP ---
|
||||||
|
|
||||||
detailModel.genreData.observe(viewLifecycleOwner, detailAdapter::submitList)
|
detailModel.genreData.observe(viewLifecycleOwner) { list -> detailAdapter.submitList(list) }
|
||||||
|
|
||||||
detailModel.navToItem.observe(viewLifecycleOwner, ::handleNavigation)
|
detailModel.navToItem.observe(viewLifecycleOwner, ::handleNavigation)
|
||||||
|
|
||||||
playbackModel.song.observe(viewLifecycleOwner) { song -> updateSong(song, detailAdapter) }
|
playbackModel.song.observe(viewLifecycleOwner) { song -> updateSong(song, detailAdapter) }
|
||||||
|
}
|
||||||
|
|
||||||
detailModel.showMenu.observe(viewLifecycleOwner) { config ->
|
override fun onItemClick(item: Item) {
|
||||||
if (config != null) {
|
when (item) {
|
||||||
showMenu(config)
|
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?) {
|
private fun handleNavigation(item: Music?) {
|
||||||
|
|
|
@ -17,82 +17,71 @@
|
||||||
|
|
||||||
package org.oxycblt.auxio.detail.recycler
|
package org.oxycblt.auxio.detail.recycler
|
||||||
|
|
||||||
import android.view.View
|
import android.content.Context
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.core.view.isInvisible
|
import androidx.core.view.isInvisible
|
||||||
import androidx.recyclerview.widget.ListAdapter
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import org.oxycblt.auxio.IntegerTable
|
import org.oxycblt.auxio.IntegerTable
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.coil.bindAlbumCover
|
import org.oxycblt.auxio.coil.bindAlbumCover
|
||||||
import org.oxycblt.auxio.databinding.ItemAlbumSongBinding
|
import org.oxycblt.auxio.databinding.ItemAlbumSongBinding
|
||||||
import org.oxycblt.auxio.databinding.ItemDetailBinding
|
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.Album
|
||||||
import org.oxycblt.auxio.music.Item
|
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.music.toDuration
|
import org.oxycblt.auxio.music.toDuration
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
import org.oxycblt.auxio.ui.BindingViewHolder
|
||||||
import org.oxycblt.auxio.ui.ActionHeaderViewHolder
|
import org.oxycblt.auxio.ui.Item
|
||||||
import org.oxycblt.auxio.ui.BaseViewHolder
|
import org.oxycblt.auxio.ui.ItemDiffCallback
|
||||||
import org.oxycblt.auxio.ui.DiffCallback
|
import org.oxycblt.auxio.ui.MenuItemListener
|
||||||
import org.oxycblt.auxio.util.getPluralSafe
|
import org.oxycblt.auxio.util.getPluralSafe
|
||||||
import org.oxycblt.auxio.util.inflater
|
import org.oxycblt.auxio.util.inflater
|
||||||
import org.oxycblt.auxio.util.textSafe
|
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
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class AlbumDetailAdapter(
|
class AlbumDetailAdapter(listener: AlbumDetailItemListener) :
|
||||||
private val playbackModel: PlaybackViewModel,
|
DetailAdapter<AlbumDetailItemListener>(listener, DIFFER) {
|
||||||
private val detailModel: DetailViewModel,
|
private var highlightedSong: Song? = null
|
||||||
private val doOnClick: (data: Song) -> Unit,
|
private var highlightedViewHolder: Highlightable? = null
|
||||||
private val doOnLongClick: (view: View, data: Song) -> Unit
|
|
||||||
) : ListAdapter<Item, RecyclerView.ViewHolder>(DiffCallback()) {
|
|
||||||
private var currentSong: Song? = null
|
|
||||||
private var currentHolder: Highlightable? = null
|
|
||||||
|
|
||||||
override fun getItemViewType(position: Int): Int {
|
override fun getCreatorFromItem(item: Item) =
|
||||||
return when (getItem(position)) {
|
super.getCreatorFromItem(item)
|
||||||
is Album -> IntegerTable.ITEM_TYPE_ALBUM_DETAIL
|
?: when (item) {
|
||||||
is ActionHeader -> IntegerTable.ITEM_TYPE_ACTION_HEADER
|
is Album -> AlbumDetailViewHolder.CREATOR
|
||||||
is Song -> IntegerTable.ITEM_TYPE_ALBUM_SONG
|
is Song -> AlbumSongViewHolder.CREATOR
|
||||||
else -> -1
|
else -> null
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
override fun getCreatorFromViewType(viewType: Int) =
|
||||||
return when (viewType) {
|
super.getCreatorFromViewType(viewType)
|
||||||
IntegerTable.ITEM_TYPE_ALBUM_DETAIL ->
|
?: when (viewType) {
|
||||||
AlbumDetailViewHolder(ItemDetailBinding.inflate(parent.context.inflater))
|
AlbumDetailViewHolder.CREATOR.viewType -> AlbumDetailViewHolder.CREATOR
|
||||||
IntegerTable.ITEM_TYPE_ACTION_HEADER -> ActionHeaderViewHolder.from(parent.context)
|
AlbumSongViewHolder.CREATOR.viewType -> AlbumSongViewHolder.CREATOR
|
||||||
IntegerTable.ITEM_TYPE_ALBUM_SONG ->
|
else -> null
|
||||||
AlbumSongViewHolder(ItemAlbumSongBinding.inflate(parent.context.inflater))
|
|
||||||
else -> error("Invalid ViewHolder item type $viewType")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
override fun onBind(
|
||||||
val item = getItem(position)
|
viewHolder: RecyclerView.ViewHolder,
|
||||||
|
item: Item,
|
||||||
|
listener: AlbumDetailItemListener
|
||||||
|
) {
|
||||||
|
super.onBind(viewHolder, item, listener)
|
||||||
|
|
||||||
when (item) {
|
when (item) {
|
||||||
is Album -> (holder as AlbumDetailViewHolder).bind(item)
|
is Album -> (viewHolder as AlbumDetailViewHolder).bind(item, listener)
|
||||||
is Song -> (holder as AlbumSongViewHolder).bind(item)
|
is Song -> (viewHolder as AlbumSongViewHolder).bind(item, listener)
|
||||||
is ActionHeader -> (holder as ActionHeaderViewHolder).bind(item)
|
}
|
||||||
else -> {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (holder is Highlightable) {
|
override fun onHighlightViewHolder(viewHolder: Highlightable, item: Item) {
|
||||||
if (item.id == currentSong?.id) {
|
if (item is Song && item.id == highlightedSong?.id) {
|
||||||
// Reset the last ViewHolder before assigning the new, correct one to be highlighted
|
// Reset the last ViewHolder before assigning the new, correct one to be highlighted
|
||||||
currentHolder?.setHighlighted(false)
|
highlightedViewHolder?.setHighlighted(false)
|
||||||
currentHolder = holder
|
highlightedViewHolder = viewHolder
|
||||||
holder.setHighlighted(true)
|
viewHolder.setHighlighted(true)
|
||||||
} else {
|
} else {
|
||||||
holder.setHighlighted(false)
|
viewHolder.setHighlighted(false)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,68 +90,88 @@ class AlbumDetailAdapter(
|
||||||
* @param recycler The recyclerview the highlighting should act on.
|
* @param recycler The recyclerview the highlighting should act on.
|
||||||
*/
|
*/
|
||||||
fun highlightSong(song: Song?, recycler: RecyclerView) {
|
fun highlightSong(song: Song?, recycler: RecyclerView) {
|
||||||
if (song == currentSong) return // Already highlighting this ViewHolder
|
if (song == highlightedSong) return
|
||||||
|
highlightedSong = song
|
||||||
|
highlightedViewHolder?.setHighlighted(false)
|
||||||
|
highlightedViewHolder = highlightItem(song, recycler)
|
||||||
|
}
|
||||||
|
|
||||||
// Clear the current ViewHolder since it's invalid
|
companion object {
|
||||||
currentHolder?.setHighlighted(false)
|
private val DIFFER =
|
||||||
currentHolder = null
|
object : ItemDiffCallback<Item>() {
|
||||||
currentSong = song
|
override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
|
||||||
|
return when {
|
||||||
if (song != null) {
|
oldItem is Album && newItem is Album ->
|
||||||
// Use existing data instead of having to re-sort it.
|
AlbumDetailViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
|
||||||
val pos = currentList.indexOfFirst { item -> item.id == song.id && item is Song }
|
oldItem is SortHeader && newItem is SortHeader ->
|
||||||
|
SortHeaderViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
|
||||||
// Check if the ViewHolder for this song is visible, if it is then highlight it.
|
oldItem is Song && newItem is Song ->
|
||||||
// If the ViewHolder is not visible, then the adapter should take care of it if
|
AlbumSongViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
|
||||||
// it does become visible.
|
else -> DetailAdapter.DIFFER.areItemsTheSame(oldItem, newItem)
|
||||||
recycler.layoutManager?.findViewByPosition(pos)?.let { child ->
|
}
|
||||||
recycler.getChildViewHolder(child)?.let {
|
|
||||||
currentHolder = it as Highlightable
|
|
||||||
currentHolder?.setHighlighted(true)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class AlbumDetailViewHolder(private val binding: ItemDetailBinding) :
|
interface AlbumDetailItemListener : DetailItemListener {
|
||||||
BaseViewHolder<Album>(binding) {
|
fun onNavigateToArtist()
|
||||||
|
|
||||||
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
|
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 {
|
binding.detailSubhead.apply {
|
||||||
textSafe = data.artist.resolvedName
|
textSafe = item.resolvedArtistName
|
||||||
setOnClickListener { detailModel.navToItem(data.artist) }
|
setOnClickListener { listener.onNavigateToArtist() }
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.detailInfo.apply {
|
binding.detailInfo.apply {
|
||||||
text =
|
text =
|
||||||
context.getString(
|
context.getString(
|
||||||
R.string.fmt_three,
|
R.string.fmt_three,
|
||||||
data.year?.toString() ?: context.getString(R.string.def_date),
|
item.year?.toString() ?: context.getString(R.string.def_date),
|
||||||
context.getPluralSafe(R.plurals.fmt_song_count, data.songs.size),
|
context.getPluralSafe(R.plurals.fmt_song_count, item.songs.size),
|
||||||
data.totalDuration)
|
item.totalDuration)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.detailPlayButton.setOnClickListener { playbackModel.playAlbum(data, false) }
|
binding.detailPlayButton.setOnClickListener { listener.onPlayParent() }
|
||||||
|
binding.detailShuffleButton.setOnClickListener { listener.onShuffleParent() }
|
||||||
|
}
|
||||||
|
|
||||||
binding.detailShuffleButton.setOnClickListener { playbackModel.playAlbum(data, true) }
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class AlbumSongViewHolder(
|
private class AlbumSongViewHolder private constructor(private val binding: ItemAlbumSongBinding) :
|
||||||
private val binding: ItemAlbumSongBinding,
|
BindingViewHolder<Song, MenuItemListener>(binding.root), Highlightable {
|
||||||
) : BaseViewHolder<Song>(binding, doOnClick, doOnLongClick), Highlightable {
|
override fun bind(item: Song, listener: MenuItemListener) {
|
||||||
override fun onBind(data: Song) {
|
|
||||||
// Hide the track number view if the song does not have a track.
|
// Hide the track number view if the song does not have a track.
|
||||||
if (data.track != null) {
|
if (item.track != null) {
|
||||||
binding.songTrack.apply {
|
binding.songTrack.apply {
|
||||||
textSafe = context.getString(R.string.fmt_number, data.track)
|
textSafe = context.getString(R.string.fmt_number, item.track)
|
||||||
isInvisible = false
|
isInvisible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -176,8 +185,16 @@ class AlbumDetailAdapter(
|
||||||
binding.songTrackPlaceholder.isInvisible = false
|
binding.songTrackPlaceholder.isInvisible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.songName.textSafe = data.resolvedName
|
binding.songName.textSafe = item.resolvedName
|
||||||
binding.songDuration.textSafe = data.seconds.toDuration(false)
|
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) {
|
override fun setHighlighted(isHighlighted: Boolean) {
|
||||||
|
@ -185,5 +202,22 @@ class AlbumDetailAdapter(
|
||||||
binding.songTrack.isActivated = isHighlighted
|
binding.songTrack.isActivated = isHighlighted
|
||||||
binding.songTrackPlaceholder.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
|
package org.oxycblt.auxio.detail.recycler
|
||||||
|
|
||||||
import android.view.View
|
import android.content.Context
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.recyclerview.widget.ListAdapter
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import org.oxycblt.auxio.IntegerTable
|
import org.oxycblt.auxio.IntegerTable
|
||||||
import org.oxycblt.auxio.R
|
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.ItemDetailBinding
|
||||||
import org.oxycblt.auxio.databinding.ItemParentBinding
|
import org.oxycblt.auxio.databinding.ItemParentBinding
|
||||||
import org.oxycblt.auxio.databinding.ItemSongBinding
|
import org.oxycblt.auxio.databinding.ItemSongBinding
|
||||||
import org.oxycblt.auxio.music.ActionHeader
|
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Header
|
|
||||||
import org.oxycblt.auxio.music.Item
|
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
import org.oxycblt.auxio.ui.ArtistViewHolder
|
||||||
import org.oxycblt.auxio.ui.ActionHeaderViewHolder
|
import org.oxycblt.auxio.ui.BindingViewHolder
|
||||||
import org.oxycblt.auxio.ui.BaseViewHolder
|
import org.oxycblt.auxio.ui.Item
|
||||||
import org.oxycblt.auxio.ui.DiffCallback
|
import org.oxycblt.auxio.ui.ItemDiffCallback
|
||||||
import org.oxycblt.auxio.ui.HeaderViewHolder
|
import org.oxycblt.auxio.ui.MenuItemListener
|
||||||
import org.oxycblt.auxio.util.context
|
import org.oxycblt.auxio.util.context
|
||||||
import org.oxycblt.auxio.util.getPluralSafe
|
import org.oxycblt.auxio.util.getPluralSafe
|
||||||
import org.oxycblt.auxio.util.inflater
|
import org.oxycblt.auxio.util.inflater
|
||||||
import org.oxycblt.auxio.util.textSafe
|
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
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class ArtistDetailAdapter(
|
class ArtistDetailAdapter(listener: DetailItemListener) :
|
||||||
private val playbackModel: PlaybackViewModel,
|
DetailAdapter<DetailItemListener>(listener, DIFFER) {
|
||||||
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()) {
|
|
||||||
private var currentAlbum: Album? = null
|
private var currentAlbum: Album? = null
|
||||||
private var currentAlbumHolder: Highlightable? = null
|
private var currentAlbumHolder: Highlightable? = null
|
||||||
|
|
||||||
private var currentSong: Song? = null
|
private var currentSong: Song? = null
|
||||||
private var currentSongHolder: Highlightable? = null
|
private var currentSongHolder: Highlightable? = null
|
||||||
|
|
||||||
override fun getItemViewType(position: Int): Int {
|
override fun getCreatorFromItem(item: Item) =
|
||||||
return when (getItem(position)) {
|
super.getCreatorFromItem(item)
|
||||||
is Artist -> IntegerTable.ITEM_TYPE_ARTIST_DETAIL
|
?: when (item) {
|
||||||
is Album -> IntegerTable.ITEM_TYPE_ARTIST_ALBUM
|
is Artist -> ArtistDetailViewHolder.CREATOR
|
||||||
is Song -> IntegerTable.ITEM_TYPE_ARTIST_SONG
|
is Album -> ArtistAlbumViewHolder.CREATOR
|
||||||
is Header -> IntegerTable.ITEM_TYPE_HEADER
|
is Song -> ArtistSongViewHolder.CREATOR
|
||||||
is ActionHeader -> IntegerTable.ITEM_TYPE_ACTION_HEADER
|
else -> null
|
||||||
else -> -1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
override fun getCreatorFromViewType(viewType: Int) =
|
||||||
return when (viewType) {
|
super.getCreatorFromViewType(viewType)
|
||||||
IntegerTable.ITEM_TYPE_ARTIST_DETAIL ->
|
?: when (viewType) {
|
||||||
ArtistDetailViewHolder(ItemDetailBinding.inflate(parent.context.inflater))
|
ArtistDetailViewHolder.CREATOR.viewType -> ArtistDetailViewHolder.CREATOR
|
||||||
IntegerTable.ITEM_TYPE_ARTIST_ALBUM ->
|
ArtistAlbumViewHolder.CREATOR.viewType -> ArtistAlbumViewHolder.CREATOR
|
||||||
ArtistAlbumViewHolder(ItemParentBinding.inflate(parent.context.inflater))
|
ArtistSongViewHolder.CREATOR.viewType -> ArtistSongViewHolder.CREATOR
|
||||||
IntegerTable.ITEM_TYPE_ARTIST_SONG ->
|
else -> null
|
||||||
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) {
|
override fun onBind(
|
||||||
val item = getItem(position)
|
viewHolder: RecyclerView.ViewHolder,
|
||||||
|
item: Item,
|
||||||
|
listener: DetailItemListener
|
||||||
|
) {
|
||||||
|
super.onBind(viewHolder, item, listener)
|
||||||
when (item) {
|
when (item) {
|
||||||
is Artist -> (holder as ArtistDetailViewHolder).bind(item)
|
is Artist -> (viewHolder as ArtistDetailViewHolder).bind(item, listener)
|
||||||
is Album -> (holder as ArtistAlbumViewHolder).bind(item)
|
is Album -> (viewHolder as ArtistAlbumViewHolder).bind(item, listener)
|
||||||
is Song -> (holder as ArtistSongViewHolder).bind(item)
|
is Song -> (viewHolder as ArtistSongViewHolder).bind(item, listener)
|
||||||
is Header -> (holder as HeaderViewHolder).bind(item)
|
|
||||||
is ActionHeader -> (holder as ActionHeaderViewHolder).bind(item)
|
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (holder is Highlightable) {
|
override fun onHighlightViewHolder(viewHolder: Highlightable, item: Item) {
|
||||||
// If the item corresponds to a currently playing song/album then highlight it
|
// If the item corresponds to a currently playing song/album then highlight it
|
||||||
if (item.id == currentAlbum?.id && item is Album) {
|
if (item.id == currentAlbum?.id && item is Album) {
|
||||||
currentAlbumHolder?.setHighlighted(false)
|
currentAlbumHolder?.setHighlighted(false)
|
||||||
currentAlbumHolder = holder
|
currentAlbumHolder = viewHolder
|
||||||
holder.setHighlighted(true)
|
viewHolder.setHighlighted(true)
|
||||||
} else if (item.id == currentSong?.id && item is Song) {
|
} else if (item.id == currentSong?.id && item is Song) {
|
||||||
currentSongHolder?.setHighlighted(false)
|
currentSongHolder?.setHighlighted(false)
|
||||||
currentSongHolder = holder
|
currentSongHolder = viewHolder
|
||||||
holder.setHighlighted(true)
|
viewHolder.setHighlighted(true)
|
||||||
} else {
|
} else {
|
||||||
holder.setHighlighted(false)
|
viewHolder.setHighlighted(false)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,26 +104,10 @@ class ArtistDetailAdapter(
|
||||||
* @param recycler The recyclerview the highlighting should act on.
|
* @param recycler The recyclerview the highlighting should act on.
|
||||||
*/
|
*/
|
||||||
fun highlightAlbum(album: Album?, recycler: RecyclerView) {
|
fun highlightAlbum(album: Album?, recycler: RecyclerView) {
|
||||||
if (album == currentAlbum) return // Already highlighting this ViewHolder
|
if (album == currentAlbum) return
|
||||||
|
|
||||||
// Album is no longer valid, clear out this ViewHolder.
|
|
||||||
currentAlbumHolder?.setHighlighted(false)
|
|
||||||
currentAlbumHolder = null
|
|
||||||
currentAlbum = album
|
currentAlbum = album
|
||||||
|
currentAlbumHolder?.setHighlighted(false)
|
||||||
if (album != null) {
|
currentAlbumHolder = highlightItem(album, recycler)
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -145,91 +115,144 @@ class ArtistDetailAdapter(
|
||||||
* @param recycler The recyclerview the highlighting should act on.
|
* @param recycler The recyclerview the highlighting should act on.
|
||||||
*/
|
*/
|
||||||
fun highlightSong(song: Song?, recycler: RecyclerView) {
|
fun highlightSong(song: Song?, recycler: RecyclerView) {
|
||||||
if (song == currentSong) return // Already highlighting this ViewHolder
|
if (song == currentSong) return
|
||||||
|
|
||||||
// Clear the current ViewHolder since it's invalid
|
|
||||||
currentSongHolder?.setHighlighted(false)
|
|
||||||
currentSongHolder = null
|
|
||||||
currentSong = song
|
currentSong = song
|
||||||
|
currentSongHolder?.setHighlighted(false)
|
||||||
|
currentSongHolder = highlightItem(song, recycler)
|
||||||
|
}
|
||||||
|
|
||||||
if (song != null) {
|
companion object {
|
||||||
// Use existing data instead of having to re-sort it.
|
private val DIFFER =
|
||||||
// We also have to account for the album count when searching for the ViewHolder.
|
object : ItemDiffCallback<Item>() {
|
||||||
val pos = currentList.indexOfFirst { item -> item.id == song.id && item is Song }
|
override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
|
||||||
|
return when {
|
||||||
// Check if the ViewHolder for this song is visible, if it is then highlight it.
|
oldItem is Artist && newItem is Artist ->
|
||||||
// If the ViewHolder is not visible, then the adapter should take care of it if
|
ArtistDetailViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
|
||||||
// it does become visible.
|
oldItem is Album && newItem is Album ->
|
||||||
recycler.layoutManager?.findViewByPosition(pos)?.let { child ->
|
ArtistAlbumViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
|
||||||
recycler.getChildViewHolder(child)?.let {
|
oldItem is Song && newItem is Song ->
|
||||||
currentSongHolder = it as Highlightable
|
ArtistSongViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
|
||||||
currentSongHolder?.setHighlighted(true)
|
else -> DetailAdapter.DIFFER.areItemsTheSame(oldItem, newItem)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class ArtistDetailViewHolder(private val binding: ItemDetailBinding) :
|
private class ArtistDetailViewHolder private constructor(private val binding: ItemDetailBinding) :
|
||||||
BaseViewHolder<Artist>(binding) {
|
BindingViewHolder<Artist, DetailItemListener>(binding.root) {
|
||||||
|
|
||||||
override fun onBind(data: Artist) {
|
override fun bind(item: Artist, listener: DetailItemListener) {
|
||||||
val context = binding.root.context
|
binding.detailCover.bindArtistImage(item)
|
||||||
|
binding.detailName.textSafe = item.resolvedName
|
||||||
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
|
// Get the genre that corresponds to the most songs in this artist, which would be
|
||||||
// the most "Prominent" genre.
|
// the most "Prominent" genre.
|
||||||
binding.detailSubhead.textSafe =
|
binding.detailSubhead.textSafe =
|
||||||
data.songs
|
item.songs.groupBy { it.genre.resolvedName }.entries.maxByOrNull { it.value.size }?.key
|
||||||
.groupBy { it.genre.resolvedName }
|
?: binding.context.getString(R.string.def_genre)
|
||||||
.entries
|
|
||||||
.maxByOrNull { it.value.size }
|
|
||||||
?.key
|
|
||||||
?: context.getString(R.string.def_genre)
|
|
||||||
|
|
||||||
binding.detailInfo.textSafe =
|
binding.detailInfo.textSafe =
|
||||||
binding.context.getString(
|
binding.context.getString(
|
||||||
R.string.fmt_two,
|
R.string.fmt_two,
|
||||||
binding.context.getPluralSafe(R.plurals.fmt_album_count, data.albums.size),
|
binding.context.getPluralSafe(R.plurals.fmt_album_count, item.albums.size),
|
||||||
binding.context.getPluralSafe(R.plurals.fmt_song_count, data.songs.size))
|
binding.context.getPluralSafe(R.plurals.fmt_song_count, item.songs.size))
|
||||||
|
|
||||||
binding.detailPlayButton.setOnClickListener { playbackModel.playArtist(data, false) }
|
binding.detailPlayButton.setOnClickListener { listener.onPlayParent() }
|
||||||
|
binding.detailShuffleButton.setOnClickListener { listener.onShuffleParent() }
|
||||||
|
}
|
||||||
|
|
||||||
binding.detailShuffleButton.setOnClickListener { playbackModel.playArtist(data, true) }
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class ArtistAlbumViewHolder(
|
private class ArtistAlbumViewHolder
|
||||||
|
private constructor(
|
||||||
private val binding: ItemParentBinding,
|
private val binding: ItemParentBinding,
|
||||||
) : BaseViewHolder<Album>(binding, doOnClick, doOnLongClick), Highlightable {
|
) : BindingViewHolder<Album, MenuItemListener>(binding.root), Highlightable {
|
||||||
override fun onBind(data: Album) {
|
override fun bind(item: Album, listener: MenuItemListener) {
|
||||||
binding.parentImage.bindAlbumCover(data)
|
binding.parentImage.bindAlbumCover(item)
|
||||||
binding.parentName.textSafe = data.resolvedName
|
binding.parentName.textSafe = item.resolvedName
|
||||||
binding.parentInfo.textSafe = binding.context.getString(R.string.fmt_number, data.year)
|
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) {
|
override fun setHighlighted(isHighlighted: Boolean) {
|
||||||
binding.parentName.isActivated = isHighlighted
|
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))
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class ArtistSongViewHolder(
|
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,
|
private val binding: ItemSongBinding,
|
||||||
) : BaseViewHolder<Song>(binding, doOnSongClick, doOnLongClick), Highlightable {
|
) : BindingViewHolder<Song, MenuItemListener>(binding.root), Highlightable {
|
||||||
override fun onBind(data: Song) {
|
override fun bind(item: Song, listener: MenuItemListener) {
|
||||||
binding.songAlbumCover.bindAlbumCover(data)
|
binding.songAlbumCover.bindAlbumCover(item)
|
||||||
binding.songName.textSafe = data.resolvedName
|
binding.songName.textSafe = item.resolvedName
|
||||||
binding.songInfo.textSafe = data.resolvedAlbumName
|
binding.songInfo.textSafe = item.resolvedAlbumName
|
||||||
|
|
||||||
|
binding.root.apply {
|
||||||
|
setOnClickListener { listener.onItemClick(item) }
|
||||||
|
setOnLongClickListener { view ->
|
||||||
|
listener.onOpenMenu(item, view)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setHighlighted(isHighlighted: Boolean) {
|
override fun setHighlighted(isHighlighted: Boolean) {
|
||||||
binding.songName.isActivated = isHighlighted
|
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
|
package org.oxycblt.auxio.detail.recycler
|
||||||
|
|
||||||
import android.view.View
|
import android.content.Context
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.recyclerview.widget.ListAdapter
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import org.oxycblt.auxio.IntegerTable
|
import org.oxycblt.auxio.IntegerTable
|
||||||
import org.oxycblt.auxio.R
|
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.coil.bindGenreImage
|
||||||
import org.oxycblt.auxio.databinding.ItemDetailBinding
|
import org.oxycblt.auxio.databinding.ItemDetailBinding
|
||||||
import org.oxycblt.auxio.databinding.ItemSongBinding
|
import org.oxycblt.auxio.databinding.ItemSongBinding
|
||||||
import org.oxycblt.auxio.music.ActionHeader
|
|
||||||
import org.oxycblt.auxio.music.Genre
|
import org.oxycblt.auxio.music.Genre
|
||||||
import org.oxycblt.auxio.music.Item
|
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
import org.oxycblt.auxio.ui.BindingViewHolder
|
||||||
import org.oxycblt.auxio.ui.ActionHeaderViewHolder
|
import org.oxycblt.auxio.ui.Item
|
||||||
import org.oxycblt.auxio.ui.BaseViewHolder
|
import org.oxycblt.auxio.ui.ItemDiffCallback
|
||||||
import org.oxycblt.auxio.ui.DiffCallback
|
import org.oxycblt.auxio.ui.MenuItemListener
|
||||||
|
import org.oxycblt.auxio.ui.SongViewHolder
|
||||||
import org.oxycblt.auxio.util.context
|
import org.oxycblt.auxio.util.context
|
||||||
import org.oxycblt.auxio.util.getPluralSafe
|
import org.oxycblt.auxio.util.getPluralSafe
|
||||||
import org.oxycblt.auxio.util.inflater
|
import org.oxycblt.auxio.util.inflater
|
||||||
import org.oxycblt.auxio.util.textSafe
|
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
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class GenreDetailAdapter(
|
class GenreDetailAdapter(listener: DetailItemListener) :
|
||||||
private val playbackModel: PlaybackViewModel,
|
DetailAdapter<DetailItemListener>(listener, DIFFER) {
|
||||||
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 currentSong: Song? = null
|
||||||
private var currentHolder: Highlightable? = null
|
private var currentHolder: Highlightable? = null
|
||||||
|
|
||||||
override fun getItemViewType(position: Int): Int {
|
override fun getCreatorFromItem(item: Item) =
|
||||||
return when (getItem(position)) {
|
super.getCreatorFromItem(item)
|
||||||
is Genre -> IntegerTable.ITEM_TYPE_GENRE_DETAIL
|
?: when (item) {
|
||||||
is ActionHeader -> IntegerTable.ITEM_TYPE_ACTION_HEADER
|
is Genre -> GenreDetailViewHolder.CREATOR
|
||||||
is Song -> IntegerTable.ITEM_TYPE_GENRE_SONG
|
is Song -> GenreSongViewHolder.CREATOR
|
||||||
else -> -1
|
else -> null
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
override fun getCreatorFromViewType(viewType: Int) =
|
||||||
return when (viewType) {
|
super.getCreatorFromViewType(viewType)
|
||||||
IntegerTable.ITEM_TYPE_GENRE_DETAIL ->
|
?: when (viewType) {
|
||||||
GenreDetailViewHolder(ItemDetailBinding.inflate(parent.context.inflater))
|
GenreDetailViewHolder.CREATOR.viewType -> GenreDetailViewHolder.CREATOR
|
||||||
IntegerTable.ITEM_TYPE_ACTION_HEADER -> ActionHeaderViewHolder.from(parent.context)
|
GenreSongViewHolder.CREATOR.viewType -> GenreSongViewHolder.CREATOR
|
||||||
IntegerTable.ITEM_TYPE_GENRE_SONG ->
|
else -> null
|
||||||
GenreSongViewHolder(ItemSongBinding.inflate(parent.context.inflater))
|
|
||||||
else -> error("Bad ViewHolder item type $viewType")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
override fun onBind(
|
||||||
val item = getItem(position)
|
viewHolder: RecyclerView.ViewHolder,
|
||||||
|
item: Item,
|
||||||
|
listener: DetailItemListener
|
||||||
|
) {
|
||||||
|
super.onBind(viewHolder, item, listener)
|
||||||
when (item) {
|
when (item) {
|
||||||
is Genre -> (holder as GenreDetailViewHolder).bind(item)
|
is Genre -> (viewHolder as GenreDetailViewHolder).bind(item, listener)
|
||||||
is ActionHeader -> (holder as ActionHeaderViewHolder).bind(item)
|
is Song -> (viewHolder as GenreSongViewHolder).bind(item, listener)
|
||||||
is Song -> (holder as GenreSongViewHolder).bind(item)
|
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (holder is Highlightable) {
|
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) {
|
if (item.id == currentSong?.id) {
|
||||||
// Reset the last ViewHolder before assigning the new, correct one to be highlighted
|
// Reset the last ViewHolder before assigning the new, correct one to be highlighted
|
||||||
currentHolder?.setHighlighted(false)
|
currentHolder?.setHighlighted(false)
|
||||||
currentHolder = holder
|
currentHolder = viewHolder
|
||||||
holder.setHighlighted(true)
|
viewHolder.setHighlighted(true)
|
||||||
} else {
|
} else {
|
||||||
holder.setHighlighted(false)
|
viewHolder.setHighlighted(false)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,64 +92,89 @@ class GenreDetailAdapter(
|
||||||
* @param recycler The recyclerview the highlighting should act on.
|
* @param recycler The recyclerview the highlighting should act on.
|
||||||
*/
|
*/
|
||||||
fun highlightSong(song: Song?, recycler: RecyclerView) {
|
fun highlightSong(song: Song?, recycler: RecyclerView) {
|
||||||
if (song == currentSong) return // Already highlighting this ViewHolder
|
if (song == currentSong) return
|
||||||
|
|
||||||
// Clear the current ViewHolder since it's invalid
|
|
||||||
currentHolder?.setHighlighted(false)
|
|
||||||
currentHolder = null
|
|
||||||
currentSong = song
|
currentSong = song
|
||||||
|
currentHolder?.setHighlighted(false)
|
||||||
|
currentHolder = highlightItem(song, recycler)
|
||||||
|
}
|
||||||
|
|
||||||
if (song != null) {
|
companion object {
|
||||||
// Use existing data instead of having to re-sort it.
|
val DIFFER =
|
||||||
val pos = currentList.indexOfFirst { item -> item.id == song.id && item is Song }
|
object : ItemDiffCallback<Item>() {
|
||||||
|
override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
|
||||||
// Check if the ViewHolder for this song is visible, if it is then highlight it.
|
return when {
|
||||||
// If the ViewHolder is not visible, then the adapter should take care of it if
|
oldItem is Genre && newItem is Genre ->
|
||||||
// it does become visible.
|
GenreDetailViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
|
||||||
recycler.layoutManager?.findViewByPosition(pos)?.let { child ->
|
oldItem is Song && newItem is Song ->
|
||||||
recycler.getChildViewHolder(child)?.let {
|
GenreSongViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
|
||||||
currentHolder = it as Highlightable
|
else -> DetailAdapter.DIFFER.areContentsTheSame(oldItem, newItem)
|
||||||
currentHolder?.setHighlighted(true)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class GenreDetailViewHolder(private val binding: ItemDetailBinding) :
|
private class GenreDetailViewHolder private constructor(private val binding: ItemDetailBinding) :
|
||||||
BaseViewHolder<Genre>(binding) {
|
BindingViewHolder<Genre, DetailItemListener>(binding.root) {
|
||||||
override fun onBind(data: Genre) {
|
override fun bind(item: Genre, listener: DetailItemListener) {
|
||||||
val context = binding.root.context
|
binding.detailCover.bindGenreImage(item)
|
||||||
|
binding.detailName.textSafe = item.resolvedName
|
||||||
binding.detailCover.apply {
|
|
||||||
bindGenreImage(data)
|
|
||||||
contentDescription = context.getString(R.string.desc_genre_image, data.resolvedName)
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.detailName.textSafe = data.resolvedName
|
|
||||||
binding.detailSubhead.textSafe =
|
binding.detailSubhead.textSafe =
|
||||||
binding.context.getPluralSafe(R.plurals.fmt_song_count, data.songs.size)
|
binding.context.getPluralSafe(R.plurals.fmt_song_count, item.songs.size)
|
||||||
binding.detailInfo.textSafe = data.totalDuration
|
binding.detailInfo.textSafe = item.totalDuration
|
||||||
|
binding.detailPlayButton.setOnClickListener { listener.onPlayParent() }
|
||||||
|
binding.detailShuffleButton.setOnClickListener { listener.onShuffleParent() }
|
||||||
|
}
|
||||||
|
|
||||||
binding.detailPlayButton.setOnClickListener { playbackModel.playGenre(data, false) }
|
companion object {
|
||||||
|
val CREATOR =
|
||||||
|
object : Creator<GenreDetailViewHolder> {
|
||||||
|
override val viewType: Int
|
||||||
|
get() = IntegerTable.ITEM_TYPE_GENRE_DETAIL
|
||||||
|
|
||||||
binding.detailShuffleButton.setOnClickListener { playbackModel.playGenre(data, true) }
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The Shared ViewHolder for a [Song]. Instantiation should be done with [from]. */
|
class GenreSongViewHolder private constructor(private val binding: ItemSongBinding) :
|
||||||
inner class GenreSongViewHolder
|
BindingViewHolder<Song, MenuItemListener>(binding.root), Highlightable {
|
||||||
constructor(
|
override fun bind(item: Song, listener: MenuItemListener) {
|
||||||
private val binding: ItemSongBinding,
|
binding.songAlbumCover.bindAlbumCover(item)
|
||||||
) : BaseViewHolder<Song>(binding, doOnClick, doOnLongClick), Highlightable {
|
binding.songName.textSafe = item.resolvedName
|
||||||
|
binding.songInfo.textSafe = item.resolvedArtistName
|
||||||
override fun onBind(data: Song) {
|
binding.root.apply {
|
||||||
binding.songAlbumCover.bindAlbumCover(data)
|
setOnClickListener { listener.onItemClick(item) }
|
||||||
binding.songName.textSafe = data.resolvedName
|
setOnLongClickListener { view ->
|
||||||
binding.songInfo.textSafe = data.resolvedArtistName
|
listener.onOpenMenu(item, view)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setHighlighted(isHighlighted: Boolean) {
|
override fun setHighlighted(isHighlighted: Boolean) {
|
||||||
binding.songName.isActivated = isHighlighted
|
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)
|
removeCallbacks(hideThumbRunnable)
|
||||||
showScrollbar()
|
showScrollbar()
|
||||||
showPopup()
|
showPopup()
|
||||||
|
onDragListener?.onFastScrollStart()
|
||||||
} else {
|
} else {
|
||||||
postAutoHideScrollbar()
|
postAutoHideScrollbar()
|
||||||
hidePopup()
|
hidePopup()
|
||||||
|
onDragListener?.onFastScrollStop()
|
||||||
}
|
}
|
||||||
|
|
||||||
onDragListener?.invoke(value)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val tRect = Rect()
|
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 */
|
/** 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
|
* A listener for when a drag event occurs. The value will be true if a drag has begun, and
|
||||||
* false if a drag ended.
|
* false if a drag ended.
|
||||||
*/
|
*/
|
||||||
var onDragListener: ((Boolean) -> Unit)? = null
|
var onDragListener: OnFastScrollListener? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
overlay.add(thumbView)
|
overlay.add(thumbView)
|
||||||
|
@ -186,8 +195,6 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
// --- RECYCLERVIEW EVENT MANAGEMENT ---
|
// --- RECYCLERVIEW EVENT MANAGEMENT ---
|
||||||
|
|
||||||
private fun onPreDraw() {
|
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()
|
updateScrollbarState()
|
||||||
|
|
||||||
thumbView.layoutDirection = layoutDirection
|
thumbView.layoutDirection = layoutDirection
|
||||||
|
@ -207,13 +214,11 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
val firstPos = firstAdapterPos
|
val firstPos = firstAdapterPos
|
||||||
val popupText =
|
val popupText =
|
||||||
if (firstPos != NO_POSITION) {
|
if (firstPos != NO_POSITION) {
|
||||||
popupProvider?.invoke(firstPos)?.ifEmpty { null }
|
popupProvider?.getPopup(firstPos)?.ifEmpty { null }
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lay out the popup view
|
|
||||||
|
|
||||||
popupView.isInvisible = popupText == null
|
popupView.isInvisible = popupText == null
|
||||||
|
|
||||||
if (popupText != null) {
|
if (popupText != null) {
|
||||||
|
@ -370,6 +375,10 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun scrollTo(offset: Int) {
|
private fun scrollTo(offset: Int) {
|
||||||
|
if (childCount == 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
stopScroll()
|
stopScroll()
|
||||||
|
|
||||||
val trueOffset = offset - paddingTop
|
val trueOffset = offset - paddingTop
|
||||||
|
|
|
@ -17,16 +17,18 @@
|
||||||
|
|
||||||
package org.oxycblt.auxio.home.list
|
package org.oxycblt.auxio.home.list
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
|
|
||||||
import org.oxycblt.auxio.home.HomeFragmentDirections
|
import org.oxycblt.auxio.home.HomeFragmentDirections
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.ui.AlbumViewHolder
|
import org.oxycblt.auxio.ui.AlbumViewHolder
|
||||||
|
import org.oxycblt.auxio.ui.BindingViewHolder
|
||||||
import org.oxycblt.auxio.ui.DisplayMode
|
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.Sort
|
||||||
import org.oxycblt.auxio.ui.newMenu
|
import org.oxycblt.auxio.ui.newMenu
|
||||||
import org.oxycblt.auxio.ui.sliceArticle
|
import org.oxycblt.auxio.ui.sliceArticle
|
||||||
|
@ -35,24 +37,17 @@ import org.oxycblt.auxio.ui.sliceArticle
|
||||||
* A [HomeListFragment] for showing a list of [Album]s.
|
* A [HomeListFragment] for showing a list of [Album]s.
|
||||||
* @author
|
* @author
|
||||||
*/
|
*/
|
||||||
class AlbumListFragment : HomeListFragment() {
|
class AlbumListFragment : HomeListFragment<Album>() {
|
||||||
override fun onBindingCreated(binding: FragmentHomeListBinding, savedInstanceState: Bundle?) {
|
override val recyclerId: Int = R.id.home_album_list
|
||||||
val homeAdapter =
|
override val homeAdapter = AlbumAdapter(this)
|
||||||
AlbumAdapter(
|
override val homeData: LiveData<List<Album>>
|
||||||
doOnClick = { album ->
|
get() = homeModel.albums
|
||||||
findNavController().navigate(HomeFragmentDirections.actionShowAlbum(album.id))
|
|
||||||
},
|
|
||||||
::newMenu)
|
|
||||||
|
|
||||||
setupRecycler(R.id.home_album_list, homeAdapter, homeModel.albums)
|
override fun getPopup(pos: Int): String? {
|
||||||
}
|
val album = homeModel.albums.value!![pos]
|
||||||
|
|
||||||
override val listPopupProvider: (Int) -> String
|
|
||||||
get() = { idx ->
|
|
||||||
val album = homeModel.albums.value!![idx]
|
|
||||||
|
|
||||||
// Change how we display the popup depending on the mode.
|
// Change how we display the popup depending on the mode.
|
||||||
when (homeModel.getSortForDisplay(DisplayMode.SHOW_ALBUMS)) {
|
return when (homeModel.getSortForDisplay(DisplayMode.SHOW_ALBUMS)) {
|
||||||
// By Name -> Use Name
|
// By Name -> Use Name
|
||||||
is Sort.ByName -> album.resolvedName.sliceArticle().first().uppercase()
|
is Sort.ByName -> album.resolvedName.sliceArticle().first().uppercase()
|
||||||
|
|
||||||
|
@ -63,22 +58,22 @@ class AlbumListFragment : HomeListFragment() {
|
||||||
is Sort.ByYear -> album.year?.toString() ?: getString(R.string.def_date)
|
is Sort.ByYear -> album.year?.toString() ?: getString(R.string.def_date)
|
||||||
|
|
||||||
// Unsupported sort, error gracefully
|
// Unsupported sort, error gracefully
|
||||||
else -> ""
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AlbumAdapter(
|
override fun onItemClick(item: Item) {
|
||||||
private val doOnClick: (data: Album) -> Unit,
|
check(item is Album)
|
||||||
private val doOnLongClick: (view: View, data: Album) -> Unit,
|
findNavController().navigate(HomeFragmentDirections.actionShowAlbum(item.id))
|
||||||
) : 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) {
|
override fun onOpenMenu(item: Item, anchor: View) {
|
||||||
holder.bind(data[position])
|
newMenu(anchor, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
package org.oxycblt.auxio.home.list
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
|
|
||||||
import org.oxycblt.auxio.home.HomeFragmentDirections
|
import org.oxycblt.auxio.home.HomeFragmentDirections
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.ui.ArtistViewHolder
|
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.newMenu
|
||||||
import org.oxycblt.auxio.ui.sliceArticle
|
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.
|
* A [HomeListFragment] for showing a list of [Artist]s.
|
||||||
* @author
|
* @author
|
||||||
*/
|
*/
|
||||||
class ArtistListFragment : HomeListFragment() {
|
class ArtistListFragment : HomeListFragment<Artist>() {
|
||||||
override fun onBindingCreated(binding: FragmentHomeListBinding, savedInstanceState: Bundle?) {
|
override val recyclerId: Int = R.id.home_artist_list
|
||||||
val homeAdapter =
|
override val homeAdapter = ArtistAdapter(this)
|
||||||
ArtistAdapter(
|
override val homeData: LiveData<List<Artist>>
|
||||||
doOnClick = { artist ->
|
get() = homeModel.artists
|
||||||
findNavController().navigate(HomeFragmentDirections.actionShowArtist(artist.id))
|
|
||||||
},
|
|
||||||
::newMenu)
|
|
||||||
|
|
||||||
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
|
override fun onOpenMenu(item: Item, anchor: View) {
|
||||||
get() = { idx ->
|
newMenu(anchor, item)
|
||||||
homeModel.artists.value!![idx].resolvedName.sliceArticle().first().uppercase()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class ArtistAdapter(
|
class ArtistAdapter(listener: MenuItemListener) :
|
||||||
private val doOnClick: (data: Artist) -> Unit,
|
MonoAdapter<Artist, MenuItemListener, ArtistViewHolder>(listener, ArtistViewHolder.DIFFER) {
|
||||||
private val doOnLongClick: (view: View, data: Artist) -> Unit,
|
override val creator = ArtistViewHolder.CREATOR
|
||||||
) : 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])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,52 +17,43 @@
|
||||||
|
|
||||||
package org.oxycblt.auxio.home.list
|
package org.oxycblt.auxio.home.list
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
|
|
||||||
import org.oxycblt.auxio.home.HomeFragmentDirections
|
import org.oxycblt.auxio.home.HomeFragmentDirections
|
||||||
import org.oxycblt.auxio.music.Genre
|
import org.oxycblt.auxio.music.Genre
|
||||||
import org.oxycblt.auxio.ui.GenreViewHolder
|
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.newMenu
|
||||||
import org.oxycblt.auxio.ui.sliceArticle
|
import org.oxycblt.auxio.ui.sliceArticle
|
||||||
import org.oxycblt.auxio.util.context
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [HomeListFragment] for showing a list of [Genre]s.
|
* A [HomeListFragment] for showing a list of [Genre]s.
|
||||||
* @author
|
* @author
|
||||||
*/
|
*/
|
||||||
class GenreListFragment : HomeListFragment() {
|
class GenreListFragment : HomeListFragment<Genre>() {
|
||||||
override fun onBindingCreated(binding: FragmentHomeListBinding, savedInstanceState: Bundle?) {
|
override val recyclerId = R.id.home_genre_list
|
||||||
val homeAdapter =
|
override val homeAdapter = GenreAdapter(this)
|
||||||
GenreAdapter(
|
override val homeData: LiveData<List<Genre>>
|
||||||
doOnClick = { Genre ->
|
get() = homeModel.genres
|
||||||
findNavController().navigate(HomeFragmentDirections.actionShowGenre(Genre.id))
|
|
||||||
},
|
|
||||||
::newMenu)
|
|
||||||
|
|
||||||
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
|
override fun onOpenMenu(item: Item, anchor: View) {
|
||||||
get() = { idx ->
|
newMenu(anchor, item)
|
||||||
homeModel.genres.value!![idx].resolvedName.sliceArticle().first().uppercase()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class GenreAdapter(
|
class GenreAdapter(listener: MenuItemListener) :
|
||||||
private val doOnClick: (data: Genre) -> Unit,
|
MonoAdapter<Genre, MenuItemListener, GenreViewHolder>(listener, GenreViewHolder.DIFFER) {
|
||||||
private val doOnLongClick: (view: View, data: Genre) -> Unit,
|
override val creator = GenreViewHolder.CREATOR
|
||||||
) : 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])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,17 +17,19 @@
|
||||||
|
|
||||||
package org.oxycblt.auxio.home.list
|
package org.oxycblt.auxio.home.list
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import androidx.annotation.IdRes
|
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
|
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
|
||||||
import org.oxycblt.auxio.home.HomeViewModel
|
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.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.ui.ViewBindingFragment
|
||||||
import org.oxycblt.auxio.util.applySpans
|
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.
|
* A Base [Fragment] implementing the base features shared across all list fragments in the home UI.
|
||||||
* @author OxygenCobalt
|
* @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. */
|
/** 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 homeModel: HomeViewModel by activityViewModels()
|
||||||
protected val playbackModel: PlaybackViewModel 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) =
|
override fun onCreateBinding(inflater: LayoutInflater) =
|
||||||
FragmentHomeListBinding.inflate(inflater)
|
FragmentHomeListBinding.inflate(inflater)
|
||||||
|
|
||||||
|
override fun onBindingCreated(binding: FragmentHomeListBinding, savedInstanceState: Bundle?) {
|
||||||
|
binding.homeRecycler.apply {
|
||||||
|
id = recyclerId
|
||||||
|
adapter = homeAdapter
|
||||||
|
applySpans()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.homeRecycler.popupProvider = this
|
||||||
|
binding.homeRecycler.onDragListener = this
|
||||||
|
|
||||||
|
homeData.observe(viewLifecycleOwner) { list ->
|
||||||
|
homeAdapter.submitListHard(list.toMutableList())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onDestroyBinding(binding: FragmentHomeListBinding) {
|
override fun onDestroyBinding(binding: FragmentHomeListBinding) {
|
||||||
homeModel.updateFastScrolling(false)
|
homeModel.updateFastScrolling(false)
|
||||||
|
binding.homeRecycler.apply {
|
||||||
|
adapter = null
|
||||||
|
popupProvider = null
|
||||||
|
onDragListener = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class HomeAdapter<T : Item, VH : RecyclerView.ViewHolder> :
|
override fun onFastScrollStart() {
|
||||||
RecyclerView.Adapter<VH>() {
|
homeModel.updateFastScrolling(true)
|
||||||
protected var data = listOf<T>()
|
}
|
||||||
|
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
override fun onFastScrollStop() {
|
||||||
fun updateData(newData: List<T>) {
|
homeModel.updateFastScrolling(false)
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,13 +17,14 @@
|
||||||
|
|
||||||
package org.oxycblt.auxio.home.list
|
package org.oxycblt.auxio.home.list
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import androidx.lifecycle.LiveData
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
|
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.ui.DisplayMode
|
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.SongViewHolder
|
||||||
import org.oxycblt.auxio.ui.Sort
|
import org.oxycblt.auxio.ui.Sort
|
||||||
import org.oxycblt.auxio.ui.newMenu
|
import org.oxycblt.auxio.ui.newMenu
|
||||||
|
@ -33,26 +34,24 @@ import org.oxycblt.auxio.ui.sliceArticle
|
||||||
* A [HomeListFragment] for showing a list of [Song]s.
|
* A [HomeListFragment] for showing a list of [Song]s.
|
||||||
* @author
|
* @author
|
||||||
*/
|
*/
|
||||||
class SongListFragment : HomeListFragment() {
|
class SongListFragment : HomeListFragment<Song>() {
|
||||||
override fun onBindingCreated(binding: FragmentHomeListBinding, savedInstanceState: Bundle?) {
|
override val recyclerId = R.id.home_song_list
|
||||||
val homeAdapter = SongsAdapter(doOnClick = playbackModel::playSong, ::newMenu)
|
override val homeAdapter = SongsAdapter(this)
|
||||||
setupRecycler(R.id.home_song_list, homeAdapter, homeModel.songs)
|
override val homeData: LiveData<List<Song>>
|
||||||
}
|
get() = homeModel.songs
|
||||||
|
|
||||||
override val listPopupProvider: (Int) -> String
|
override fun getPopup(pos: Int): String {
|
||||||
get() = { idx ->
|
val song = homeModel.songs.value!![pos]
|
||||||
val song = homeModel.songs.value!![idx]
|
|
||||||
|
|
||||||
// Change how we display the popup depending on the mode.
|
// 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
|
// 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.
|
// based off the names of the parent objects and not the child objects.
|
||||||
when (homeModel.getSortForDisplay(DisplayMode.SHOW_SONGS)) {
|
return when (homeModel.getSortForDisplay(DisplayMode.SHOW_SONGS)) {
|
||||||
// Name -> Use name
|
// Name -> Use name
|
||||||
is Sort.ByName -> song.resolvedName.sliceArticle().first().uppercase()
|
is Sort.ByName -> song.resolvedName.sliceArticle().first().uppercase()
|
||||||
|
|
||||||
// Artist -> Use Artist Name
|
// Artist -> Use Artist Name
|
||||||
is Sort.ByArtist ->
|
is Sort.ByArtist -> song.album.artist.resolvedName.sliceArticle().first().uppercase()
|
||||||
song.album.artist.resolvedName.sliceArticle().first().uppercase()
|
|
||||||
|
|
||||||
// Album -> Use Album Name
|
// Album -> Use Album Name
|
||||||
is Sort.ByAlbum -> song.album.resolvedName.sliceArticle().first().uppercase()
|
is Sort.ByAlbum -> song.album.resolvedName.sliceArticle().first().uppercase()
|
||||||
|
@ -62,18 +61,17 @@ class SongListFragment : HomeListFragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class SongsAdapter(
|
override fun onItemClick(item: Item) {
|
||||||
private val doOnClick: (data: Song) -> Unit,
|
check(item is Song)
|
||||||
private val doOnLongClick: (view: View, data: Song) -> Unit,
|
playbackModel.playSong(item)
|
||||||
) : 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) {
|
override fun onOpenMenu(item: Item, anchor: View) {
|
||||||
holder.bind(data[position])
|
newMenu(anchor, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.content.ContentUris
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import android.view.View
|
import org.oxycblt.auxio.ui.Item
|
||||||
import androidx.annotation.DrawableRes
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
|
|
||||||
// --- MUSIC MODELS ---
|
// --- 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. */
|
/** [Item] variant that represents a music item. */
|
||||||
sealed class Music : Item() {
|
sealed class Music : Item() {
|
||||||
/** The raw name of this item. */
|
/** The raw name of this item. */
|
||||||
|
@ -245,49 +237,3 @@ data class Genre(
|
||||||
val totalDuration: String
|
val totalDuration: String
|
||||||
get() = songs.sumOf { it.seconds }.toDuration(false)
|
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()) {
|
while (cursor.moveToNext()) {
|
||||||
// Genre names can be a normal name, an ID3v2 constant, or null. Normal names
|
// Genre names can be a normal name, an ID3v2 constant, or null. Normal names
|
||||||
// are
|
// are resolved as usual, but null values don't make sense and are often junk
|
||||||
// resolved as usual, but null values don't make sense and are often junk
|
// anyway, so we skip genres that have them.
|
||||||
// anyway,
|
|
||||||
// so we skip genres that have them.
|
|
||||||
val id = cursor.getLong(idIndex)
|
val id = cursor.getLong(idIndex)
|
||||||
val name = cursor.getStringOrNull(nameIndex) ?: continue
|
val name = cursor.getStringOrNull(nameIndex) ?: continue
|
||||||
val resolvedName = name.genreNameCompat ?: name
|
val resolvedName = name.genreNameCompat ?: name
|
||||||
|
|
|
@ -18,99 +18,39 @@
|
||||||
package org.oxycblt.auxio.playback.queue
|
package org.oxycblt.auxio.playback.queue
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
import android.graphics.drawable.ColorDrawable
|
import android.graphics.drawable.ColorDrawable
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.core.view.isInvisible
|
import androidx.core.view.isInvisible
|
||||||
import androidx.recyclerview.widget.AsyncListDiffer
|
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.google.android.material.shape.MaterialShapeDrawable
|
import com.google.android.material.shape.MaterialShapeDrawable
|
||||||
import org.oxycblt.auxio.IntegerTable
|
import org.oxycblt.auxio.IntegerTable
|
||||||
import org.oxycblt.auxio.coil.bindAlbumCover
|
import org.oxycblt.auxio.coil.bindAlbumCover
|
||||||
import org.oxycblt.auxio.databinding.ItemQueueSongBinding
|
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.music.Song
|
||||||
import org.oxycblt.auxio.ui.ActionHeaderViewHolder
|
import org.oxycblt.auxio.ui.BindingViewHolder
|
||||||
import org.oxycblt.auxio.ui.BaseViewHolder
|
import org.oxycblt.auxio.ui.MonoAdapter
|
||||||
import org.oxycblt.auxio.ui.DiffCallback
|
import org.oxycblt.auxio.ui.SongViewHolder
|
||||||
import org.oxycblt.auxio.ui.HeaderViewHolder
|
|
||||||
import org.oxycblt.auxio.util.disableDropShadowCompat
|
import org.oxycblt.auxio.util.disableDropShadowCompat
|
||||||
import org.oxycblt.auxio.util.inflater
|
import org.oxycblt.auxio.util.inflater
|
||||||
import org.oxycblt.auxio.util.logE
|
|
||||||
import org.oxycblt.auxio.util.stateList
|
import org.oxycblt.auxio.util.stateList
|
||||||
import org.oxycblt.auxio.util.textSafe
|
import org.oxycblt.auxio.util.textSafe
|
||||||
|
|
||||||
/**
|
class NewQueueAdapter(listener: QueueItemListener) :
|
||||||
* The single adapter for both the Next Queue and the User Queue.
|
MonoAdapter<Song, QueueItemListener, QueueSongViewHolder>(
|
||||||
* @param touchHelper The [ItemTouchHelper] ***containing*** [QueueDragCallback] to be used
|
listener, QueueSongViewHolder.DIFFER) {
|
||||||
* @author OxygenCobalt
|
override val creator = QueueSongViewHolder.CREATOR
|
||||||
*/
|
|
||||||
class QueueAdapter(private val touchHelper: ItemTouchHelper) :
|
|
||||||
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
|
||||||
private var data = mutableListOf<Item>()
|
|
||||||
private var listDiffer = AsyncListDiffer(this, DiffCallback())
|
|
||||||
|
|
||||||
override fun getItemCount(): Int = data.size
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
interface QueueItemListener {
|
||||||
return when (viewType) {
|
fun onPickUp(viewHolder: RecyclerView.ViewHolder)
|
||||||
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) {
|
class QueueSongViewHolder
|
||||||
when (val item = data[position]) {
|
private constructor(
|
||||||
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,
|
private val binding: ItemQueueSongBinding,
|
||||||
) : BaseViewHolder<Song>(binding) {
|
) : BindingViewHolder<Song, QueueItemListener>(binding.root) {
|
||||||
val bodyView: View
|
val bodyView: View
|
||||||
get() = binding.body
|
get() = binding.body
|
||||||
val backgroundView: View
|
val backgroundView: View
|
||||||
|
@ -126,10 +66,10 @@ class QueueAdapter(private val touchHelper: ItemTouchHelper) :
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
override fun onBind(data: Song) {
|
override fun bind(item: Song, listener: QueueItemListener) {
|
||||||
binding.songAlbumCover.bindAlbumCover(data)
|
binding.songAlbumCover.bindAlbumCover(item)
|
||||||
binding.songName.textSafe = data.resolvedName
|
binding.songName.textSafe = item.resolvedName
|
||||||
binding.songInfo.textSafe = data.resolvedArtistName
|
binding.songInfo.textSafe = item.resolvedArtistName
|
||||||
|
|
||||||
binding.background.isInvisible = true
|
binding.background.isInvisible = true
|
||||||
|
|
||||||
|
@ -140,15 +80,27 @@ class QueueAdapter(private val touchHelper: ItemTouchHelper) :
|
||||||
binding.songDragHandle.setOnTouchListener { _, motionEvent ->
|
binding.songDragHandle.setOnTouchListener { _, motionEvent ->
|
||||||
binding.songDragHandle.performClick()
|
binding.songDragHandle.performClick()
|
||||||
if (motionEvent.actionMasked == MotionEvent.ACTION_DOWN) {
|
if (motionEvent.actionMasked == MotionEvent.ACTION_DOWN) {
|
||||||
touchHelper.startDrag(this)
|
listener.onPickUp(this)
|
||||||
true
|
true
|
||||||
} else false
|
} else false
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.body.setOnLongClickListener {
|
binding.body.setOnLongClickListener {
|
||||||
touchHelper.startDrag(this)
|
listener.onPickUp(this)
|
||||||
true
|
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.
|
* hot garbage. This shouldn't have *too many* UI bugs. I hope.
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class QueueDragCallback(private val playbackModel: PlaybackViewModel) : ItemTouchHelper.Callback() {
|
class QueueDragCallback(
|
||||||
private lateinit var queueAdapter: QueueAdapter
|
private val playbackModel: PlaybackViewModel,
|
||||||
|
private val queueAdapter: NewQueueAdapter
|
||||||
|
) : ItemTouchHelper.Callback() {
|
||||||
private var shouldLift = true
|
private var shouldLift = true
|
||||||
|
|
||||||
override fun getMovementFlags(
|
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
|
// 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
|
// work! To emulate it on my own, I check if this child is in a drag state and then animate
|
||||||
// an elevation change.
|
// an elevation change.
|
||||||
val holder = viewHolder as QueueAdapter.QueueSongViewHolder
|
val holder = viewHolder as QueueSongViewHolder
|
||||||
|
|
||||||
if (shouldLift && isCurrentlyActive && actionState == ItemTouchHelper.ACTION_STATE_DRAG) {
|
if (shouldLift && isCurrentlyActive && actionState == ItemTouchHelper.ACTION_STATE_DRAG) {
|
||||||
logD("Lifting queue item")
|
logD("Lifting queue item")
|
||||||
|
@ -122,7 +124,7 @@ class QueueDragCallback(private val playbackModel: PlaybackViewModel) : ItemTouc
|
||||||
|
|
||||||
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
|
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
|
||||||
// When an elevated item is cleared, we reset the elevation using another animation.
|
// 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) {
|
if (holder.itemView.translationZ != 0f) {
|
||||||
logD("Dropping queue item")
|
logD("Dropping queue item")
|
||||||
|
@ -163,14 +165,6 @@ class QueueDragCallback(private val playbackModel: PlaybackViewModel) : ItemTouc
|
||||||
|
|
||||||
override fun isLongPressDragEnabled(): Boolean = false
|
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 {
|
companion object {
|
||||||
const val MINIMUM_INITIAL_DRAG_VELOCITY = 10
|
const val MINIMUM_INITIAL_DRAG_VELOCITY = 10
|
||||||
const val MAXIMUM_INITIAL_DRAG_VELOCITY = 25
|
const val MAXIMUM_INITIAL_DRAG_VELOCITY = 25
|
||||||
|
|
|
@ -23,56 +23,68 @@ import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import org.oxycblt.auxio.databinding.FragmentQueueBinding
|
import org.oxycblt.auxio.databinding.FragmentQueueBinding
|
||||||
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.ui.ViewBindingFragment
|
import org.oxycblt.auxio.ui.ViewBindingFragment
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.requireAttached
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [Fragment] that shows the queue and enables editing as well.
|
* A [Fragment] that shows the queue and enables editing as well.
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class QueueFragment : ViewBindingFragment<FragmentQueueBinding>() {
|
class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), QueueItemListener {
|
||||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
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 onCreateBinding(inflater: LayoutInflater) = FragmentQueueBinding.inflate(inflater)
|
||||||
|
|
||||||
override fun onBindingCreated(binding: FragmentQueueBinding, savedInstanceState: Bundle?) {
|
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.queueToolbar.setNavigationOnClickListener { findNavController().navigateUp() }
|
||||||
|
|
||||||
binding.queueRecycler.apply {
|
binding.queueRecycler.apply {
|
||||||
setHasFixedSize(true)
|
|
||||||
adapter = queueAdapter
|
adapter = queueAdapter
|
||||||
helper.attachToRecyclerView(this)
|
requireTouchHelper().attachToRecyclerView(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- VIEWMODEL SETUP ----
|
// --- VIEWMODEL SETUP ----
|
||||||
|
|
||||||
lastShuffle = playbackModel.isShuffling.value
|
playbackModel.nextUp.observe(viewLifecycleOwner, ::updateQueue)
|
||||||
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) { queue ->
|
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()) {
|
if (queue.isEmpty()) {
|
||||||
findNavController().navigateUp()
|
findNavController().navigateUp()
|
||||||
return@observe
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
package org.oxycblt.auxio.search
|
||||||
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.recyclerview.widget.ListAdapter
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import org.oxycblt.auxio.IntegerTable
|
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Genre
|
import org.oxycblt.auxio.music.Genre
|
||||||
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.music.Song
|
||||||
import org.oxycblt.auxio.ui.AlbumViewHolder
|
import org.oxycblt.auxio.ui.AlbumViewHolder
|
||||||
import org.oxycblt.auxio.ui.ArtistViewHolder
|
import org.oxycblt.auxio.ui.ArtistViewHolder
|
||||||
import org.oxycblt.auxio.ui.DiffCallback
|
|
||||||
import org.oxycblt.auxio.ui.GenreViewHolder
|
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
|
import org.oxycblt.auxio.ui.SongViewHolder
|
||||||
|
|
||||||
/**
|
class NeoSearchAdapter(listener: MenuItemListener) :
|
||||||
* A Multi-ViewHolder adapter that displays the results of a search query.
|
MultiAdapter<MenuItemListener>(listener, DIFFER) {
|
||||||
* @author OxygenCobalt
|
override fun getCreatorFromItem(item: Item) =
|
||||||
*/
|
when (item) {
|
||||||
class SearchAdapter(
|
is Song -> SongViewHolder.CREATOR
|
||||||
private val doOnClick: (data: Music) -> Unit,
|
is Album -> AlbumViewHolder.CREATOR
|
||||||
private val doOnLongClick: (view: View, data: Music) -> Unit
|
is Artist -> ArtistViewHolder.CREATOR
|
||||||
) : ListAdapter<Item, RecyclerView.ViewHolder>(DiffCallback<Item>()) {
|
is Genre -> GenreViewHolder.CREATOR
|
||||||
|
is Header -> NewHeaderViewHolder.CREATOR
|
||||||
override fun getItemViewType(position: Int): Int {
|
else -> null
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
override fun getCreatorFromViewType(viewType: Int) =
|
||||||
return when (viewType) {
|
when (viewType) {
|
||||||
IntegerTable.ITEM_TYPE_GENRE ->
|
SongViewHolder.CREATOR.viewType -> SongViewHolder.CREATOR
|
||||||
GenreViewHolder.from(parent.context, doOnClick, doOnLongClick)
|
AlbumViewHolder.CREATOR.viewType -> AlbumViewHolder.CREATOR
|
||||||
IntegerTable.ITEM_TYPE_ARTIST ->
|
ArtistViewHolder.CREATOR.viewType -> ArtistViewHolder.CREATOR
|
||||||
ArtistViewHolder.from(parent.context, doOnClick, doOnLongClick)
|
GenreViewHolder.CREATOR.viewType -> GenreViewHolder.CREATOR
|
||||||
IntegerTable.ITEM_TYPE_ALBUM ->
|
NewHeaderViewHolder.CREATOR.viewType -> NewHeaderViewHolder.CREATOR
|
||||||
AlbumViewHolder.from(parent.context, doOnClick, doOnLongClick)
|
else -> null
|
||||||
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 onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
override fun onBind(
|
||||||
when (val item = getItem(position)) {
|
viewHolder: RecyclerView.ViewHolder,
|
||||||
is Genre -> (holder as GenreViewHolder).bind(item)
|
item: Item,
|
||||||
is Artist -> (holder as ArtistViewHolder).bind(item)
|
listener: MenuItemListener
|
||||||
is Album -> (holder as AlbumViewHolder).bind(item)
|
) {
|
||||||
is Song -> (holder as SongViewHolder).bind(item)
|
when (item) {
|
||||||
is Header -> (holder as HeaderViewHolder).bind(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 -> {}
|
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.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
import androidx.core.view.isInvisible
|
import androidx.core.view.isInvisible
|
||||||
import androidx.core.view.postDelayed
|
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.Album
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Genre
|
import org.oxycblt.auxio.music.Genre
|
||||||
import org.oxycblt.auxio.music.Header
|
|
||||||
import org.oxycblt.auxio.music.Item
|
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
|
import org.oxycblt.auxio.ui.Header
|
||||||
|
import org.oxycblt.auxio.ui.Item
|
||||||
|
import org.oxycblt.auxio.ui.MenuItemListener
|
||||||
import org.oxycblt.auxio.ui.ViewBindingFragment
|
import org.oxycblt.auxio.ui.ViewBindingFragment
|
||||||
import org.oxycblt.auxio.ui.newMenu
|
import org.oxycblt.auxio.ui.newMenu
|
||||||
import org.oxycblt.auxio.util.applySpans
|
import org.oxycblt.auxio.util.applySpans
|
||||||
import org.oxycblt.auxio.util.getSystemServiceSafe
|
import org.oxycblt.auxio.util.getSystemServiceSafe
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
import org.oxycblt.auxio.util.requireAttached
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [Fragment] that allows for the searching of the entire music library.
|
* A [Fragment] that allows for the searching of the entire music library.
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class SearchFragment : ViewBindingFragment<FragmentSearchBinding>() {
|
class SearchFragment : ViewBindingFragment<FragmentSearchBinding>(), MenuItemListener {
|
||||||
// SearchViewModel is only scoped to this Fragment
|
// SearchViewModel is only scoped to this Fragment
|
||||||
private val searchModel: SearchViewModel by viewModels()
|
private val searchModel: SearchViewModel by viewModels()
|
||||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
private val detailModel: DetailViewModel by activityViewModels()
|
private val detailModel: DetailViewModel by activityViewModels()
|
||||||
|
|
||||||
|
private val searchAdapter = NeoSearchAdapter(this)
|
||||||
|
private var imm: InputMethodManager? = null
|
||||||
private var launchedKeyboard = false
|
private var launchedKeyboard = false
|
||||||
|
|
||||||
override fun onCreateBinding(inflater: LayoutInflater) = FragmentSearchBinding.inflate(inflater)
|
override fun onCreateBinding(inflater: LayoutInflater) = FragmentSearchBinding.inflate(inflater)
|
||||||
|
|
||||||
override fun onBindingCreated(binding: FragmentSearchBinding, savedInstanceState: Bundle?) {
|
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 {
|
binding.searchToolbar.apply {
|
||||||
menu.findItem(searchModel.filterMode?.itemId ?: R.id.option_filter_all).isChecked = true
|
menu.findItem(searchModel.filterMode?.itemId ?: R.id.option_filter_all).isChecked = true
|
||||||
|
|
||||||
setNavigationOnClickListener {
|
setNavigationOnClickListener {
|
||||||
imm.hide()
|
requireImm().hide()
|
||||||
findNavController().navigateUp()
|
findNavController().navigateUp()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,7 +93,9 @@ class SearchFragment : ViewBindingFragment<FragmentSearchBinding>() {
|
||||||
if (!launchedKeyboard) {
|
if (!launchedKeyboard) {
|
||||||
// Auto-open the keyboard when this view is shown
|
// Auto-open the keyboard when this view is shown
|
||||||
requestFocus()
|
requestFocus()
|
||||||
postDelayed(200) { imm.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT) }
|
postDelayed(200) {
|
||||||
|
requireImm().showSoftInput(this, InputMethodManager.SHOW_IMPLICIT)
|
||||||
|
}
|
||||||
|
|
||||||
launchedKeyboard = true
|
launchedKeyboard = true
|
||||||
}
|
}
|
||||||
|
@ -107,13 +108,11 @@ class SearchFragment : ViewBindingFragment<FragmentSearchBinding>() {
|
||||||
|
|
||||||
// --- VIEWMODEL SETUP ---
|
// --- VIEWMODEL SETUP ---
|
||||||
|
|
||||||
searchModel.searchResults.observe(viewLifecycleOwner) { results ->
|
searchModel.searchResults.observe(viewLifecycleOwner, ::updateResults)
|
||||||
updateResults(results, searchAdapter)
|
|
||||||
}
|
|
||||||
|
|
||||||
detailModel.navToItem.observe(viewLifecycleOwner) { item ->
|
detailModel.navToItem.observe(viewLifecycleOwner) { item ->
|
||||||
handleNavigation(item)
|
handleNavigation(item)
|
||||||
imm.hide()
|
requireImm().hide()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,10 +121,47 @@ class SearchFragment : ViewBindingFragment<FragmentSearchBinding>() {
|
||||||
searchModel.setNavigating(false)
|
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()
|
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
|
// I would make it so that the position is only scrolled back to the top when
|
||||||
// the query actually changes instead of once every re-creation event, but sadly
|
// the query actually changes instead of once every re-creation event, but sadly
|
||||||
// that doesn't seem possible.
|
// that doesn't seem possible.
|
||||||
|
@ -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() {
|
private fun InputMethodManager.hide() {
|
||||||
hideSoftInputFromWindow(requireView().windowToken, InputMethodManager.HIDE_NOT_ALWAYS)
|
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 java.text.Normalizer
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.oxycblt.auxio.R
|
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.Music
|
||||||
import org.oxycblt.auxio.music.MusicStore
|
import org.oxycblt.auxio.music.MusicStore
|
||||||
import org.oxycblt.auxio.settings.SettingsManager
|
import org.oxycblt.auxio.settings.SettingsManager
|
||||||
import org.oxycblt.auxio.ui.DisplayMode
|
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.ui.Sort
|
||||||
import org.oxycblt.auxio.util.logD
|
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.Album
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Genre
|
import org.oxycblt.auxio.music.Genre
|
||||||
import org.oxycblt.auxio.music.Item
|
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.util.showToast
|
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
|
clipToPadding = false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onApplyWindowInsets(insets: WindowInsets): WindowInsets {
|
override fun onAttachedToWindow() {
|
||||||
|
super.onAttachedToWindow()
|
||||||
|
setHasFixedSize(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onApplyWindowInsets(insets: WindowInsets): WindowInsets {
|
||||||
updatePadding(
|
updatePadding(
|
||||||
initialPadding.left,
|
initialPadding.left,
|
||||||
initialPadding.top,
|
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
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -18,98 +18,53 @@
|
||||||
package org.oxycblt.auxio.ui
|
package org.oxycblt.auxio.ui
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.view.View
|
import org.oxycblt.auxio.IntegerTable
|
||||||
import androidx.appcompat.widget.TooltipCompat
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import androidx.viewbinding.ViewBinding
|
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.coil.bindAlbumCover
|
import org.oxycblt.auxio.coil.bindAlbumCover
|
||||||
import org.oxycblt.auxio.coil.bindArtistImage
|
import org.oxycblt.auxio.coil.bindArtistImage
|
||||||
import org.oxycblt.auxio.coil.bindGenreImage
|
import org.oxycblt.auxio.coil.bindGenreImage
|
||||||
import org.oxycblt.auxio.databinding.ItemActionHeaderBinding
|
|
||||||
import org.oxycblt.auxio.databinding.ItemHeaderBinding
|
import org.oxycblt.auxio.databinding.ItemHeaderBinding
|
||||||
import org.oxycblt.auxio.databinding.ItemParentBinding
|
import org.oxycblt.auxio.databinding.ItemParentBinding
|
||||||
import org.oxycblt.auxio.databinding.ItemSongBinding
|
import org.oxycblt.auxio.databinding.ItemSongBinding
|
||||||
import org.oxycblt.auxio.music.ActionHeader
|
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Genre
|
import org.oxycblt.auxio.music.Genre
|
||||||
import org.oxycblt.auxio.music.Header
|
|
||||||
import org.oxycblt.auxio.music.Item
|
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.util.context
|
import org.oxycblt.auxio.util.context
|
||||||
import org.oxycblt.auxio.util.getPluralSafe
|
import org.oxycblt.auxio.util.getPluralSafe
|
||||||
import org.oxycblt.auxio.util.inflater
|
import org.oxycblt.auxio.util.inflater
|
||||||
import org.oxycblt.auxio.util.textSafe
|
import org.oxycblt.auxio.util.textSafe
|
||||||
|
|
||||||
/**
|
class SongViewHolder private constructor(private val binding: ItemSongBinding) :
|
||||||
* A [RecyclerView.ViewHolder] that streamlines a lot of the common things across all viewholders.
|
BindingViewHolder<Song, MenuItemListener>(binding.root) {
|
||||||
* @param T The datatype, inheriting [Item] for this ViewHolder.
|
override fun bind(item: Song, listener: MenuItemListener) {
|
||||||
* @param binding Basic [ViewDataBinding] required to set up click listeners & sizing.
|
binding.songAlbumCover.bindAlbumCover(item)
|
||||||
* @param doOnClick (Optional) Function that calls on a click.
|
binding.songName.textSafe = item.resolvedName
|
||||||
* @param doOnLongClick (Optional) Functions that calls on a long-click.
|
binding.songInfo.textSafe = item.resolvedArtistName
|
||||||
* @author OxygenCobalt
|
binding.root.apply {
|
||||||
*/
|
setOnClickListener { listener.onItemClick(item) }
|
||||||
abstract class BaseViewHolder<T : Item>(
|
setOnLongClickListener { view ->
|
||||||
private val binding: ViewBinding,
|
listener.onOpenMenu(item, view)
|
||||||
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)
|
|
||||||
true
|
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 {
|
companion object {
|
||||||
/** Create an instance of [SongViewHolder] */
|
val CREATOR =
|
||||||
fun from(
|
object : Creator<SongViewHolder> {
|
||||||
context: Context,
|
override val viewType: Int
|
||||||
doOnClick: (data: Song) -> Unit,
|
get() = IntegerTable.ITEM_TYPE_SONG
|
||||||
doOnLongClick: (view: View, data: Song) -> Unit
|
|
||||||
): SongViewHolder {
|
override fun create(context: Context) =
|
||||||
return SongViewHolder(
|
SongViewHolder(ItemSongBinding.inflate(context.inflater))
|
||||||
ItemSongBinding.inflate(context.inflater), doOnClick, doOnLongClick)
|
}
|
||||||
|
|
||||||
|
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
|
class AlbumViewHolder
|
||||||
private constructor(
|
private constructor(
|
||||||
private val binding: ItemParentBinding,
|
private val binding: ItemParentBinding,
|
||||||
doOnClick: (data: Album) -> Unit,
|
) : BindingViewHolder<Album, MenuItemListener>(binding.root) {
|
||||||
doOnLongClick: (view: View, data: Album) -> Unit
|
|
||||||
) : BaseViewHolder<Album>(binding, doOnClick, doOnLongClick) {
|
|
||||||
|
|
||||||
override fun onBind(data: Album) {
|
override fun bind(item: Album, listener: MenuItemListener) {
|
||||||
binding.parentImage.bindAlbumCover(data)
|
binding.parentImage.bindAlbumCover(item)
|
||||||
binding.parentName.textSafe = data.resolvedName
|
binding.parentName.textSafe = item.resolvedName
|
||||||
binding.parentInfo.textSafe = data.resolvedArtistName
|
binding.parentInfo.textSafe = item.resolvedArtistName
|
||||||
|
binding.root.apply {
|
||||||
|
setOnClickListener { listener.onItemClick(item) }
|
||||||
|
setOnLongClickListener { view ->
|
||||||
|
listener.onOpenMenu(item, view)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/** Create an instance of [AlbumViewHolder] */
|
val CREATOR =
|
||||||
fun from(
|
object : Creator<AlbumViewHolder> {
|
||||||
context: Context,
|
override val viewType: Int
|
||||||
doOnClick: (data: Album) -> Unit,
|
get() = IntegerTable.ITEM_TYPE_ALBUM
|
||||||
doOnLongClick: (view: View, data: Album) -> Unit
|
|
||||||
): AlbumViewHolder {
|
override fun create(context: Context) =
|
||||||
return AlbumViewHolder(
|
AlbumViewHolder(ItemParentBinding.inflate(context.inflater))
|
||||||
ItemParentBinding.inflate(context.inflater), doOnClick, doOnLongClick)
|
}
|
||||||
|
|
||||||
|
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]. */
|
/** The Shared ViewHolder for a [Artist]. */
|
||||||
class ArtistViewHolder
|
class ArtistViewHolder private constructor(private val binding: ItemParentBinding) :
|
||||||
private constructor(
|
BindingViewHolder<Artist, MenuItemListener>(binding.root) {
|
||||||
private val binding: ItemParentBinding,
|
|
||||||
doOnClick: (Artist) -> Unit,
|
|
||||||
doOnLongClick: (view: View, data: Artist) -> Unit
|
|
||||||
) : BaseViewHolder<Artist>(binding, doOnClick, doOnLongClick) {
|
|
||||||
|
|
||||||
override fun onBind(data: Artist) {
|
override fun bind(item: Artist, listener: MenuItemListener) {
|
||||||
binding.parentImage.bindArtistImage(data)
|
binding.parentImage.bindArtistImage(item)
|
||||||
binding.parentName.textSafe = data.resolvedName
|
binding.parentName.textSafe = item.resolvedName
|
||||||
binding.parentInfo.textSafe =
|
binding.parentInfo.textSafe =
|
||||||
binding.context.getString(
|
binding.context.getString(
|
||||||
R.string.fmt_two,
|
R.string.fmt_two,
|
||||||
binding.context.getPluralSafe(R.plurals.fmt_album_count, data.albums.size),
|
binding.context.getPluralSafe(R.plurals.fmt_album_count, item.albums.size),
|
||||||
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 {
|
companion object {
|
||||||
/** Create an instance of [ArtistViewHolder] */
|
val CREATOR =
|
||||||
fun from(
|
object : Creator<ArtistViewHolder> {
|
||||||
context: Context,
|
override val viewType: Int
|
||||||
doOnClick: (Artist) -> Unit,
|
get() = IntegerTable.ITEM_TYPE_ARTIST
|
||||||
doOnLongClick: (view: View, data: Artist) -> Unit
|
|
||||||
): ArtistViewHolder {
|
override fun create(context: Context) =
|
||||||
return ArtistViewHolder(
|
ArtistViewHolder(ItemParentBinding.inflate(context.inflater))
|
||||||
ItemParentBinding.inflate(context.inflater), doOnClick, doOnLongClick)
|
}
|
||||||
|
|
||||||
|
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
|
class GenreViewHolder
|
||||||
private constructor(
|
private constructor(
|
||||||
private val binding: ItemParentBinding,
|
private val binding: ItemParentBinding,
|
||||||
doOnClick: (Genre) -> Unit,
|
) : BindingViewHolder<Genre, MenuItemListener>(binding.root) {
|
||||||
doOnLongClick: (view: View, data: Genre) -> Unit
|
|
||||||
) : BaseViewHolder<Genre>(binding, doOnClick, doOnLongClick) {
|
|
||||||
|
|
||||||
override fun onBind(data: Genre) {
|
override fun bind(item: Genre, listener: MenuItemListener) {
|
||||||
binding.parentImage.bindGenreImage(data)
|
binding.parentImage.bindGenreImage(item)
|
||||||
binding.parentName.textSafe = data.resolvedName
|
binding.parentName.textSafe = item.resolvedName
|
||||||
binding.parentInfo.textSafe =
|
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 {
|
companion object {
|
||||||
/** Create an instance of [GenreViewHolder] */
|
val CREATOR =
|
||||||
fun from(
|
object : Creator<GenreViewHolder> {
|
||||||
context: Context,
|
override val viewType: Int
|
||||||
doOnClick: (Genre) -> Unit,
|
get() = IntegerTable.ITEM_TYPE_GENRE
|
||||||
doOnLongClick: (view: View, data: Genre) -> Unit
|
|
||||||
): GenreViewHolder {
|
override fun create(context: Context) =
|
||||||
return GenreViewHolder(
|
GenreViewHolder(ItemParentBinding.inflate(context.inflater))
|
||||||
ItemParentBinding.inflate(context.inflater), doOnClick, doOnLongClick)
|
}
|
||||||
|
|
||||||
|
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] */
|
/** The Shared ViewHolder for a [Header]. Instantiation should be done with [from] */
|
||||||
class HeaderViewHolder private constructor(private val binding: ItemHeaderBinding) :
|
class NewHeaderViewHolder private constructor(private val binding: ItemHeaderBinding) :
|
||||||
BaseViewHolder<Header>(binding) {
|
BindingViewHolder<Header, Unit>(binding.root) {
|
||||||
|
|
||||||
override fun onBind(data: Header) {
|
override fun bind(item: Header, listener: Unit) {
|
||||||
binding.title.textSafe = binding.context.getString(data.string)
|
binding.title.textSafe = binding.context.getString(item.string)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/** Create an instance of [HeaderViewHolder] */
|
val CREATOR =
|
||||||
fun from(context: Context): HeaderViewHolder {
|
object : Creator<NewHeaderViewHolder> {
|
||||||
return HeaderViewHolder(ItemHeaderBinding.inflate(context.inflater))
|
override val viewType: Int
|
||||||
}
|
get() = IntegerTable.ITEM_TYPE_HEADER
|
||||||
}
|
|
||||||
|
override fun create(context: Context) =
|
||||||
|
NewHeaderViewHolder(ItemHeaderBinding.inflate(context.inflater))
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The Shared ViewHolder for an [ActionHeader]. Instantiation should be done with [from] */
|
val DIFFER =
|
||||||
class ActionHeaderViewHolder private constructor(private val binding: ItemActionHeaderBinding) :
|
object : ItemDiffCallback<Header>() {
|
||||||
BaseViewHolder<ActionHeader>(binding) {
|
override fun areItemsTheSame(oldItem: Header, newItem: Header): Boolean =
|
||||||
|
oldItem.string == newItem.string
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ package org.oxycblt.auxio.util
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import android.database.sqlite.SQLiteDatabase
|
import android.database.sqlite.SQLiteDatabase
|
||||||
import android.os.Looper
|
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
|
* 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"
|
"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. */
|
/** Returns whether a recyclerview can scroll. */
|
||||||
fun RecyclerView.canScroll(): Boolean = computeVerticalScrollRange() > height
|
fun RecyclerView.canScroll(): Boolean = computeVerticalScrollRange() > height
|
||||||
|
|
||||||
|
|
|
@ -26,11 +26,12 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:maxLines="1"
|
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:textAlignment="center"
|
||||||
android:textAppearance="@style/TextAppearance.Auxio.BodyLarge"
|
android:textAppearance="@style/TextAppearance.Auxio.BodyLarge"
|
||||||
android:textColor="@color/sel_accented_secondary"
|
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_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toStartOf="@+id/song_name"
|
app:layout_constraintEnd_toStartOf="@+id/song_name"
|
||||||
app:layout_constraintHorizontal_bias="0.5"
|
app:layout_constraintHorizontal_bias="0.5"
|
||||||
|
|
|
@ -29,8 +29,8 @@
|
||||||
app:layout_constraintBottom_toTopOf="@id/header_divider"
|
app:layout_constraintBottom_toTopOf="@id/header_divider"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
tools:ignore="ContentDescription"
|
android:contentDescription="@string/lbl_sort"
|
||||||
tools:src="@drawable/ic_sort" />
|
android:src="@drawable/ic_sort" />
|
||||||
|
|
||||||
<com.google.android.material.divider.MaterialDivider
|
<com.google.android.material.divider.MaterialDivider
|
||||||
android:id="@+id/header_divider"
|
android:id="@+id/header_divider"
|
|
@ -26,6 +26,7 @@
|
||||||
|
|
||||||
<dimen name="text_size_ext_label_larger">16sp</dimen>
|
<dimen name="text_size_ext_label_larger">16sp</dimen>
|
||||||
<dimen name="text_size_ext_title_mid_large">18sp</dimen>
|
<dimen name="text_size_ext_title_mid_large">18sp</dimen>
|
||||||
|
<dimen name="text_size_track_number">22sp</dimen>
|
||||||
|
|
||||||
<!-- Misc -->
|
<!-- Misc -->
|
||||||
<dimen name="elevation_small">2dp</dimen>
|
<dimen name="elevation_small">2dp</dimen>
|
||||||
|
|
Loading…
Reference in a new issue