Change search to show all music items
Change the library search to show artists, albums, and songs.
This commit is contained in:
parent
b814ac613d
commit
45f411fdd7
13 changed files with 332 additions and 74 deletions
|
@ -26,8 +26,7 @@ TODOs surrounded with !s are things I tried to do, but failed for reasons includ
|
||||||
- Exit functionality
|
- Exit functionality
|
||||||
- ? Remove gap from where I removed the overflow menu ?
|
- ? Remove gap from where I removed the overflow menu ?
|
||||||
- ? Add icons to overflow menu items ?
|
- ? Add icons to overflow menu items ?
|
||||||
- ? Show Artists, Albums, and Songs in search ?
|
- ? Implement filtering for search [Will resolve gap issue] ?
|
||||||
- ? Implement filtering for above ^^^ [Will resolve gap issue] ?
|
|
||||||
- ? Move into ViewPager ?
|
- ? Move into ViewPager ?
|
||||||
- ! Move Adapter functionality to ListAdapter [RecyclerView scrolls to middle/bottom when data is re-sorted] !
|
- ! Move Adapter functionality to ListAdapter [RecyclerView scrolls to middle/bottom when data is re-sorted] !
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ package org.oxycblt.auxio.library
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.appcompat.widget.SearchView
|
import androidx.appcompat.widget.SearchView
|
||||||
|
@ -16,6 +17,8 @@ import androidx.transition.TransitionManager
|
||||||
import org.oxycblt.auxio.MainFragmentDirections
|
import org.oxycblt.auxio.MainFragmentDirections
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.FragmentLibraryBinding
|
import org.oxycblt.auxio.databinding.FragmentLibraryBinding
|
||||||
|
import org.oxycblt.auxio.library.recycler.LibraryAdapter
|
||||||
|
import org.oxycblt.auxio.library.recycler.SearchAdapter
|
||||||
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.BaseModel
|
import org.oxycblt.auxio.music.BaseModel
|
||||||
|
@ -34,8 +37,6 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
|
||||||
private val musicModel: MusicViewModel by activityViewModels()
|
private val musicModel: MusicViewModel by activityViewModels()
|
||||||
private val libraryModel: LibraryViewModel by activityViewModels()
|
private val libraryModel: LibraryViewModel by activityViewModels()
|
||||||
|
|
||||||
private lateinit var libraryAdapter: LibraryAdapter
|
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
|
@ -43,6 +44,15 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
|
||||||
): View? {
|
): View? {
|
||||||
val binding = FragmentLibraryBinding.inflate(inflater)
|
val binding = FragmentLibraryBinding.inflate(inflater)
|
||||||
|
|
||||||
|
val libraryAdapter = LibraryAdapter(
|
||||||
|
libraryModel.showMode.value!!,
|
||||||
|
ClickListener { navToItem(it) }
|
||||||
|
)
|
||||||
|
|
||||||
|
val searchAdapter = SearchAdapter(
|
||||||
|
ClickListener { navToItem(it) }
|
||||||
|
)
|
||||||
|
|
||||||
// Toolbar setup
|
// Toolbar setup
|
||||||
binding.libraryToolbar.overflowIcon = ContextCompat.getDrawable(
|
binding.libraryToolbar.overflowIcon = ContextCompat.getDrawable(
|
||||||
requireContext(), R.drawable.ic_sort_none
|
requireContext(), R.drawable.ic_sort_none
|
||||||
|
@ -52,18 +62,35 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
|
||||||
val item = findItem(R.id.action_search)
|
val item = findItem(R.id.action_search)
|
||||||
val searchView = item.actionView as SearchView
|
val searchView = item.actionView as SearchView
|
||||||
|
|
||||||
|
// Set up the SearchView itself
|
||||||
searchView.queryHint = getString(R.string.hint_search_library)
|
searchView.queryHint = getString(R.string.hint_search_library)
|
||||||
searchView.setOnQueryTextListener(this@LibraryFragment)
|
searchView.setOnQueryTextListener(this@LibraryFragment)
|
||||||
searchView.setOnQueryTextFocusChangeListener { _, hasFocus ->
|
searchView.setOnQueryTextFocusChangeListener { _, hasFocus ->
|
||||||
this.setGroupVisible(R.id.group_sorting, !hasFocus)
|
libraryModel.updateSearchFocusStatus(hasFocus)
|
||||||
|
|
||||||
// Make sure the search item will still be visible, and then do an animation
|
|
||||||
item.isVisible = !hasFocus
|
item.isVisible = !hasFocus
|
||||||
TransitionManager.beginDelayedTransition(
|
|
||||||
binding.libraryToolbar, Fade()
|
|
||||||
)
|
|
||||||
item.collapseActionView()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
item.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
|
||||||
|
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
|
||||||
|
// When opened, update the adapter to the SearchAdapter
|
||||||
|
// And remove the sorting group
|
||||||
|
binding.libraryRecycler.adapter = searchAdapter
|
||||||
|
setGroupVisible(R.id.group_sorting, false)
|
||||||
|
libraryModel.resetQuery()
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
|
||||||
|
// When closed, switch back to LibraryAdapter, make the sorting
|
||||||
|
// visible again, and reset the query so that the old results wont show
|
||||||
|
// up if the search is opened again.
|
||||||
|
binding.libraryRecycler.adapter = libraryAdapter
|
||||||
|
setGroupVisible(R.id.group_sorting, true)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.libraryToolbar.setOnMenuItemClickListener {
|
binding.libraryToolbar.setOnMenuItemClickListener {
|
||||||
|
@ -82,11 +109,6 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
libraryAdapter = LibraryAdapter(
|
|
||||||
libraryModel.showMode.value!!,
|
|
||||||
ClickListener { navToItem(it) }
|
|
||||||
)
|
|
||||||
|
|
||||||
// RecyclerView setup
|
// RecyclerView setup
|
||||||
binding.libraryRecycler.adapter = libraryAdapter
|
binding.libraryRecycler.adapter = libraryAdapter
|
||||||
binding.libraryRecycler.applyDivider()
|
binding.libraryRecycler.applyDivider()
|
||||||
|
@ -116,17 +138,10 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
libraryModel.searchQuery.observe(viewLifecycleOwner) { query ->
|
libraryModel.searchResults.observe(viewLifecycleOwner) {
|
||||||
// Update the adapter with the new data
|
if (libraryModel.searchHasFocus) {
|
||||||
libraryAdapter.updateData(
|
searchAdapter.submitList(it)
|
||||||
when (libraryModel.showMode.value) {
|
}
|
||||||
SHOW_GENRES -> musicModel.genres.value!!
|
|
||||||
SHOW_ARTISTS -> musicModel.artists.value!!
|
|
||||||
SHOW_ALBUMS -> musicModel.albums.value!!
|
|
||||||
|
|
||||||
else -> musicModel.artists.value!!
|
|
||||||
}.filter { it.name.contains(query, true) }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(this::class.simpleName, "Fragment created.")
|
Log.d(this::class.simpleName, "Fragment created.")
|
||||||
|
@ -143,7 +158,7 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
|
||||||
override fun onQueryTextSubmit(query: String): Boolean = false
|
override fun onQueryTextSubmit(query: String): Boolean = false
|
||||||
|
|
||||||
override fun onQueryTextChange(query: String): Boolean {
|
override fun onQueryTextChange(query: String): Boolean {
|
||||||
libraryModel.updateSearchQuery(query)
|
libraryModel.updateSearchQuery(query, musicModel)
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,14 +4,24 @@ import android.view.MenuItem
|
||||||
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 androidx.lifecycle.viewModelScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
|
import org.oxycblt.auxio.music.BaseModel
|
||||||
|
import org.oxycblt.auxio.music.Header
|
||||||
|
import org.oxycblt.auxio.music.MusicViewModel
|
||||||
import org.oxycblt.auxio.recycler.SortMode
|
import org.oxycblt.auxio.recycler.SortMode
|
||||||
|
import org.oxycblt.auxio.theme.SHOW_ALBUMS
|
||||||
import org.oxycblt.auxio.theme.SHOW_ARTISTS
|
import org.oxycblt.auxio.theme.SHOW_ARTISTS
|
||||||
|
import org.oxycblt.auxio.theme.SHOW_SONGS
|
||||||
|
|
||||||
class LibraryViewModel : ViewModel() {
|
class LibraryViewModel : ViewModel() {
|
||||||
private var mIsNavigating = false
|
private var mIsNavigating = false
|
||||||
val isNavigating: Boolean get() = mIsNavigating
|
val isNavigating: Boolean get() = mIsNavigating
|
||||||
|
|
||||||
|
private var mSearchHasFocus = false
|
||||||
|
val searchHasFocus: Boolean get() = mSearchHasFocus
|
||||||
|
|
||||||
// TODO: Move these to prefs when they're added
|
// TODO: Move these to prefs when they're added
|
||||||
private val mShowMode = MutableLiveData(SHOW_ARTISTS)
|
private val mShowMode = MutableLiveData(SHOW_ARTISTS)
|
||||||
val showMode: LiveData<Int> get() = mShowMode
|
val showMode: LiveData<Int> get() = mShowMode
|
||||||
|
@ -19,8 +29,8 @@ class LibraryViewModel : ViewModel() {
|
||||||
private val mSortMode = MutableLiveData(SortMode.ALPHA_DOWN)
|
private val mSortMode = MutableLiveData(SortMode.ALPHA_DOWN)
|
||||||
val sortMode: LiveData<SortMode> get() = mSortMode
|
val sortMode: LiveData<SortMode> get() = mSortMode
|
||||||
|
|
||||||
private val mSearchQuery = MutableLiveData("")
|
private val mSearchResults = MutableLiveData(listOf<BaseModel>())
|
||||||
val searchQuery: LiveData<String> get() = mSearchQuery
|
val searchResults: LiveData<List<BaseModel>> get() = mSearchResults
|
||||||
|
|
||||||
fun updateSortMode(item: MenuItem) {
|
fun updateSortMode(item: MenuItem) {
|
||||||
val mode = when (item.itemId) {
|
val mode = when (item.itemId) {
|
||||||
|
@ -36,11 +46,53 @@ class LibraryViewModel : ViewModel() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateSearchQuery(query: String) {
|
fun updateSearchQuery(query: String, musicModel: MusicViewModel) {
|
||||||
mSearchQuery.value = query
|
if (query == "") {
|
||||||
|
resetQuery()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search MusicViewModel for all the items [Artists, Albums, Songs] that contain
|
||||||
|
// the query, and update the LiveData with those items. This is done on a seperate
|
||||||
|
// thread as it can be a very intensive operation for large music libraries.
|
||||||
|
viewModelScope.launch {
|
||||||
|
val combined = mutableListOf<BaseModel>()
|
||||||
|
|
||||||
|
val artists = musicModel.artists.value!!.filter { it.name.contains(query, true) }
|
||||||
|
|
||||||
|
if (artists.isNotEmpty()) {
|
||||||
|
combined.add(Header(id = SHOW_ARTISTS.toLong()))
|
||||||
|
combined.addAll(artists)
|
||||||
|
}
|
||||||
|
|
||||||
|
val albums = musicModel.albums.value!!.filter { it.name.contains(query, true) }
|
||||||
|
|
||||||
|
if (albums.isNotEmpty()) {
|
||||||
|
combined.add(Header(id = SHOW_ALBUMS.toLong()))
|
||||||
|
combined.addAll(albums)
|
||||||
|
}
|
||||||
|
|
||||||
|
val songs = musicModel.songs.value!!.filter { it.name.contains(query, true) }
|
||||||
|
|
||||||
|
if (songs.isNotEmpty()) {
|
||||||
|
combined.add(Header(id = SHOW_SONGS.toLong()))
|
||||||
|
combined.addAll(songs)
|
||||||
|
}
|
||||||
|
|
||||||
|
mSearchResults.value = combined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resetQuery() {
|
||||||
|
mSearchResults.value = listOf()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateNavigationStatus(value: Boolean) {
|
fun updateNavigationStatus(value: Boolean) {
|
||||||
mIsNavigating = value
|
mIsNavigating = value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun updateSearchFocusStatus(value: Boolean) {
|
||||||
|
mSearchHasFocus = value
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package org.oxycblt.auxio.library
|
package org.oxycblt.auxio.library.recycler
|
||||||
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
@ -10,7 +10,6 @@ import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.BaseModel
|
import org.oxycblt.auxio.music.BaseModel
|
||||||
import org.oxycblt.auxio.music.Genre
|
import org.oxycblt.auxio.music.Genre
|
||||||
import org.oxycblt.auxio.recycler.BaseViewHolder
|
|
||||||
import org.oxycblt.auxio.recycler.ClickListener
|
import org.oxycblt.auxio.recycler.ClickListener
|
||||||
import org.oxycblt.auxio.theme.SHOW_ALBUMS
|
import org.oxycblt.auxio.theme.SHOW_ALBUMS
|
||||||
import org.oxycblt.auxio.theme.SHOW_ARTISTS
|
import org.oxycblt.auxio.theme.SHOW_ARTISTS
|
||||||
|
@ -22,7 +21,7 @@ class LibraryAdapter(
|
||||||
val listener: ClickListener<BaseModel>
|
val listener: ClickListener<BaseModel>
|
||||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||||
|
|
||||||
var data: List<BaseModel>
|
private var data: List<BaseModel>
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// Assign the data on startup depending on the type
|
// Assign the data on startup depending on the type
|
||||||
|
@ -41,18 +40,22 @@ class LibraryAdapter(
|
||||||
// Return a different View Holder depending on the show type
|
// Return a different View Holder depending on the show type
|
||||||
return when (showMode) {
|
return when (showMode) {
|
||||||
SHOW_GENRES -> GenreViewHolder(
|
SHOW_GENRES -> GenreViewHolder(
|
||||||
|
listener,
|
||||||
ItemGenreBinding.inflate(LayoutInflater.from(parent.context))
|
ItemGenreBinding.inflate(LayoutInflater.from(parent.context))
|
||||||
)
|
)
|
||||||
|
|
||||||
SHOW_ARTISTS -> ArtistViewHolder(
|
SHOW_ARTISTS -> ArtistViewHolder(
|
||||||
|
listener,
|
||||||
ItemArtistBinding.inflate(LayoutInflater.from(parent.context))
|
ItemArtistBinding.inflate(LayoutInflater.from(parent.context))
|
||||||
)
|
)
|
||||||
|
|
||||||
SHOW_ALBUMS -> AlbumViewHolder(
|
SHOW_ALBUMS -> AlbumViewHolder(
|
||||||
|
listener,
|
||||||
ItemAlbumBinding.inflate(LayoutInflater.from(parent.context))
|
ItemAlbumBinding.inflate(LayoutInflater.from(parent.context))
|
||||||
)
|
)
|
||||||
|
|
||||||
else -> ArtistViewHolder(
|
else -> ArtistViewHolder(
|
||||||
|
listener,
|
||||||
ItemArtistBinding.inflate(LayoutInflater.from(parent.context))
|
ItemArtistBinding.inflate(LayoutInflater.from(parent.context))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -69,42 +72,9 @@ class LibraryAdapter(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the data, as its an internal value.
|
// Update the data, as its an internal value.
|
||||||
// TODO: Call this from a coroutine.
|
|
||||||
fun updateData(newData: List<BaseModel>) {
|
fun updateData(newData: List<BaseModel>) {
|
||||||
data = newData
|
data = newData
|
||||||
|
|
||||||
notifyDataSetChanged()
|
notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- VIEWHOLDERS ---
|
|
||||||
|
|
||||||
inner class GenreViewHolder(
|
|
||||||
private val binding: ItemGenreBinding
|
|
||||||
) : BaseViewHolder<BaseModel>(binding, listener) {
|
|
||||||
|
|
||||||
override fun onBind(model: BaseModel) {
|
|
||||||
binding.genre = model as Genre
|
|
||||||
binding.genreName.requestLayout()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class ArtistViewHolder(
|
|
||||||
private val binding: ItemArtistBinding
|
|
||||||
) : BaseViewHolder<BaseModel>(binding, listener) {
|
|
||||||
|
|
||||||
override fun onBind(model: BaseModel) {
|
|
||||||
binding.artist = model as Artist
|
|
||||||
binding.artistName.requestLayout()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class AlbumViewHolder(
|
|
||||||
private val binding: ItemAlbumBinding
|
|
||||||
) : BaseViewHolder<BaseModel>(binding, listener) {
|
|
||||||
|
|
||||||
override fun onBind(model: BaseModel) {
|
|
||||||
binding.album = model as Album
|
|
||||||
binding.albumName.requestLayout()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -0,0 +1,90 @@
|
||||||
|
package org.oxycblt.auxio.library.recycler
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import org.oxycblt.auxio.databinding.ItemAlbumBinding
|
||||||
|
import org.oxycblt.auxio.databinding.ItemArtistBinding
|
||||||
|
import org.oxycblt.auxio.databinding.ItemGenreBinding
|
||||||
|
import org.oxycblt.auxio.databinding.ItemHeaderBinding
|
||||||
|
import org.oxycblt.auxio.databinding.ItemSongBinding
|
||||||
|
import org.oxycblt.auxio.music.Album
|
||||||
|
import org.oxycblt.auxio.music.Artist
|
||||||
|
import org.oxycblt.auxio.music.BaseModel
|
||||||
|
import org.oxycblt.auxio.music.Genre
|
||||||
|
import org.oxycblt.auxio.music.Header
|
||||||
|
import org.oxycblt.auxio.music.Song
|
||||||
|
import org.oxycblt.auxio.recycler.ClickListener
|
||||||
|
import org.oxycblt.auxio.recycler.DiffCallback
|
||||||
|
|
||||||
|
class SearchAdapter(
|
||||||
|
private val listener: ClickListener<BaseModel>
|
||||||
|
) : ListAdapter<BaseModel, RecyclerView.ViewHolder>(DiffCallback<BaseModel>()) {
|
||||||
|
override fun getItemViewType(position: Int): Int {
|
||||||
|
return when (getItem(position)) {
|
||||||
|
is Genre -> ITEM_TYPE_GENRE
|
||||||
|
is Artist -> ITEM_TYPE_ARTIST
|
||||||
|
is Album -> ITEM_TYPE_ALBUM
|
||||||
|
is Song -> ITEM_TYPE_SONG
|
||||||
|
is Header -> ITEM_TYPE_HEADER
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||||
|
return when (viewType) {
|
||||||
|
ITEM_TYPE_GENRE -> GenreViewHolder(
|
||||||
|
listener,
|
||||||
|
ItemGenreBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
ITEM_TYPE_ARTIST -> ArtistViewHolder(
|
||||||
|
listener,
|
||||||
|
ItemArtistBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
ITEM_TYPE_ALBUM -> AlbumViewHolder(
|
||||||
|
listener,
|
||||||
|
ItemAlbumBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
ITEM_TYPE_SONG -> SongViewHolder(
|
||||||
|
listener,
|
||||||
|
ItemSongBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
ITEM_TYPE_HEADER -> HeaderViewHolder(
|
||||||
|
ItemHeaderBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
else -> ArtistViewHolder(
|
||||||
|
listener,
|
||||||
|
ItemArtistBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||||
|
when (holder) {
|
||||||
|
is GenreViewHolder -> holder
|
||||||
|
is ArtistViewHolder -> holder
|
||||||
|
is AlbumViewHolder -> holder
|
||||||
|
is SongViewHolder -> holder
|
||||||
|
is HeaderViewHolder -> holder
|
||||||
|
|
||||||
|
else -> return
|
||||||
|
}.onBind(getItem(position))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,75 @@
|
||||||
|
package org.oxycblt.auxio.library.recycler
|
||||||
|
|
||||||
|
import org.oxycblt.auxio.databinding.ItemAlbumBinding
|
||||||
|
import org.oxycblt.auxio.databinding.ItemArtistBinding
|
||||||
|
import org.oxycblt.auxio.databinding.ItemGenreBinding
|
||||||
|
import org.oxycblt.auxio.databinding.ItemHeaderBinding
|
||||||
|
import org.oxycblt.auxio.databinding.ItemSongBinding
|
||||||
|
import org.oxycblt.auxio.music.Album
|
||||||
|
import org.oxycblt.auxio.music.Artist
|
||||||
|
import org.oxycblt.auxio.music.BaseModel
|
||||||
|
import org.oxycblt.auxio.music.Genre
|
||||||
|
import org.oxycblt.auxio.music.Header
|
||||||
|
import org.oxycblt.auxio.music.Song
|
||||||
|
import org.oxycblt.auxio.recycler.BaseViewHolder
|
||||||
|
import org.oxycblt.auxio.recycler.ClickListener
|
||||||
|
|
||||||
|
const val ITEM_TYPE_GENRE = 10
|
||||||
|
const val ITEM_TYPE_ARTIST = 11
|
||||||
|
const val ITEM_TYPE_ALBUM = 12
|
||||||
|
const val ITEM_TYPE_SONG = 13
|
||||||
|
const val ITEM_TYPE_HEADER = 14
|
||||||
|
|
||||||
|
class GenreViewHolder(
|
||||||
|
listener: ClickListener<BaseModel>,
|
||||||
|
private val binding: ItemGenreBinding
|
||||||
|
) : BaseViewHolder<BaseModel>(binding, listener) {
|
||||||
|
|
||||||
|
override fun onBind(model: BaseModel) {
|
||||||
|
binding.genre = model as Genre
|
||||||
|
binding.genreName.requestLayout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ArtistViewHolder(
|
||||||
|
listener: ClickListener<BaseModel>,
|
||||||
|
private val binding: ItemArtistBinding
|
||||||
|
) : BaseViewHolder<BaseModel>(binding, listener) {
|
||||||
|
|
||||||
|
override fun onBind(model: BaseModel) {
|
||||||
|
binding.artist = model as Artist
|
||||||
|
binding.artistName.requestLayout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AlbumViewHolder(
|
||||||
|
listener: ClickListener<BaseModel>,
|
||||||
|
private val binding: ItemAlbumBinding
|
||||||
|
) : BaseViewHolder<BaseModel>(binding, listener) {
|
||||||
|
|
||||||
|
override fun onBind(model: BaseModel) {
|
||||||
|
binding.album = model as Album
|
||||||
|
binding.albumName.requestLayout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SongViewHolder(
|
||||||
|
listener: ClickListener<BaseModel>,
|
||||||
|
private val binding: ItemSongBinding
|
||||||
|
) : BaseViewHolder<BaseModel>(binding, listener) {
|
||||||
|
|
||||||
|
override fun onBind(model: BaseModel) {
|
||||||
|
binding.song = model as Song
|
||||||
|
|
||||||
|
binding.songName.requestLayout()
|
||||||
|
binding.songInfo.requestLayout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class HeaderViewHolder(
|
||||||
|
private val binding: ItemHeaderBinding
|
||||||
|
) : BaseViewHolder<BaseModel>(binding, null) {
|
||||||
|
override fun onBind(model: BaseModel) {
|
||||||
|
binding.header = model as Header
|
||||||
|
}
|
||||||
|
}
|
|
@ -90,3 +90,9 @@ data class Genre(
|
||||||
return num
|
return num
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Header [Used for search, nothing else]
|
||||||
|
data class Header(
|
||||||
|
override val id: Long = -1,
|
||||||
|
override var name: String = ""
|
||||||
|
) : BaseModel()
|
||||||
|
|
|
@ -8,6 +8,9 @@ import android.text.format.DateUtils
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.databinding.BindingAdapter
|
import androidx.databinding.BindingAdapter
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
|
import org.oxycblt.auxio.theme.SHOW_ALBUMS
|
||||||
|
import org.oxycblt.auxio.theme.SHOW_ARTISTS
|
||||||
|
import org.oxycblt.auxio.theme.SHOW_SONGS
|
||||||
|
|
||||||
// List of ID3 genres + Winamp extensions, each index corresponds to their int value.
|
// List of ID3 genres + Winamp extensions, each index corresponds to their int value.
|
||||||
// There are a lot more int-genre extensions as far as Im aware, but this works for most cases.
|
// There are a lot more int-genre extensions as far as Im aware, but this works for most cases.
|
||||||
|
@ -152,3 +155,16 @@ fun TextView.bindAlbumSongs(album: Album) {
|
||||||
R.plurals.format_song_count, album.numSongs, album.numSongs
|
R.plurals.format_song_count, album.numSongs, album.numSongs
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@BindingAdapter("headerText")
|
||||||
|
fun TextView.bindHeaderText(header: Header) {
|
||||||
|
text = context.getString(
|
||||||
|
when (header.id.toInt()) {
|
||||||
|
SHOW_ARTISTS -> R.string.label_artists
|
||||||
|
SHOW_ALBUMS -> R.string.label_albums
|
||||||
|
SHOW_SONGS -> R.string.label_songs
|
||||||
|
|
||||||
|
else -> R.string.label_artists
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@ import org.oxycblt.auxio.music.processing.MusicLoaderResponse
|
||||||
import org.oxycblt.auxio.music.processing.MusicSorter
|
import org.oxycblt.auxio.music.processing.MusicSorter
|
||||||
|
|
||||||
// ViewModel for music storage.
|
// ViewModel for music storage.
|
||||||
// FIXME: This class can be improved in multiple ways
|
// FIXME: This system can be improved in multiple ways
|
||||||
// - Remove lists/parents from models so that they can be parcelable
|
// - Remove lists/parents from models so that they can be parcelable
|
||||||
// - Move genre usage to songs [If there's a way to find songs without a genre]
|
// - Move genre usage to songs [If there's a way to find songs without a genre]
|
||||||
class MusicViewModel(private val app: Application) : ViewModel() {
|
class MusicViewModel(private val app: Application) : ViewModel() {
|
||||||
|
|
|
@ -7,7 +7,7 @@ import org.oxycblt.auxio.music.BaseModel
|
||||||
// ViewHolder abstraction that automates some of the things that are common for all ViewHolders.
|
// ViewHolder abstraction that automates some of the things that are common for all ViewHolders.
|
||||||
abstract class BaseViewHolder<T : BaseModel>(
|
abstract class BaseViewHolder<T : BaseModel>(
|
||||||
private val baseBinding: ViewDataBinding,
|
private val baseBinding: ViewDataBinding,
|
||||||
protected val listener: ClickListener<T>
|
protected val listener: ClickListener<T>?
|
||||||
) : RecyclerView.ViewHolder(baseBinding.root) {
|
) : RecyclerView.ViewHolder(baseBinding.root) {
|
||||||
init {
|
init {
|
||||||
baseBinding.root.layoutParams = RecyclerView.LayoutParams(
|
baseBinding.root.layoutParams = RecyclerView.LayoutParams(
|
||||||
|
@ -16,8 +16,10 @@ abstract class BaseViewHolder<T : BaseModel>(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun bind(model: T) {
|
fun bind(model: T) {
|
||||||
baseBinding.root.setOnClickListener {
|
if (listener != null) {
|
||||||
listener.onClick(model)
|
baseBinding.root.setOnClickListener {
|
||||||
|
listener.onClick(model)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onBind(model)
|
onBind(model)
|
||||||
|
|
|
@ -3,8 +3,7 @@ package org.oxycblt.auxio.recycler
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import org.oxycblt.auxio.music.BaseModel
|
import org.oxycblt.auxio.music.BaseModel
|
||||||
|
|
||||||
// A RecyclerView click listener that can only be called once.
|
// RecyclerView click listener
|
||||||
// Primarily used for navigation to prevent bugs when multiple items are selected.
|
|
||||||
class ClickListener<T>(val onClick: (T) -> Unit)
|
class ClickListener<T>(val onClick: (T) -> Unit)
|
||||||
|
|
||||||
// Base Diff callback
|
// Base Diff callback
|
||||||
|
|
|
@ -4,3 +4,4 @@ package org.oxycblt.auxio.theme
|
||||||
const val SHOW_ARTISTS = 0
|
const val SHOW_ARTISTS = 0
|
||||||
const val SHOW_ALBUMS = 1
|
const val SHOW_ALBUMS = 1
|
||||||
const val SHOW_GENRES = 2
|
const val SHOW_GENRES = 2
|
||||||
|
const val SHOW_SONGS = 3
|
||||||
|
|
33
app/src/main/res/layout/item_header.xml
Normal file
33
app/src/main/res/layout/item_header.xml
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<data>
|
||||||
|
|
||||||
|
<variable
|
||||||
|
name="header"
|
||||||
|
type="org.oxycblt.auxio.music.Header" />
|
||||||
|
</data>
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/header_text"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@drawable/header_dividers"
|
||||||
|
android:fontFamily="@font/inter_bold"
|
||||||
|
android:paddingStart="@dimen/padding_medium"
|
||||||
|
android:paddingTop="@dimen/padding_small"
|
||||||
|
android:paddingEnd="@dimen/padding_small"
|
||||||
|
android:paddingBottom="@dimen/padding_small"
|
||||||
|
android:textAppearance="@style/TextAppearance.MaterialComponents.Overline"
|
||||||
|
android:textSize="16sp"
|
||||||
|
app:headerText="@{header}"
|
||||||
|
tools:text="Songs" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
</layout>
|
Loading…
Reference in a new issue