Refactor library structure

Refactor LibraryFragment & LibraryViewModel so that LibraryViewModel only holds the data and LibraryFragment displays it.
This commit is contained in:
OxygenCobalt 2020-12-12 12:22:06 -07:00
parent 60750d976b
commit 859391e69b
7 changed files with 102 additions and 70 deletions

View file

@ -23,7 +23,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.music.MusicStore
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.playback.state.PlaybackMode import org.oxycblt.auxio.playback.state.PlaybackMode
@ -51,10 +50,7 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
): View { ): View {
val binding = FragmentLibraryBinding.inflate(inflater) val binding = FragmentLibraryBinding.inflate(inflater)
val musicStore = MusicStore.getInstance()
val libraryAdapter = LibraryAdapter( val libraryAdapter = LibraryAdapter(
libraryModel.displayMode.value!!,
doOnClick = { navToItem(it) }, doOnClick = { navToItem(it) },
doOnLongClick = { data, view -> showActionsForItem(data, view) } doOnLongClick = { data, view -> showActionsForItem(data, view) }
) )
@ -127,16 +123,21 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
// --- VIEWMODEL SETUP --- // --- VIEWMODEL SETUP ---
libraryModel.libraryData.observe(viewLifecycleOwner) {
libraryAdapter.updateData(it)
}
libraryModel.searchResults.observe(viewLifecycleOwner) {
if (libraryModel.searchHasFocus) {
searchAdapter.submitList(it) {
binding.libraryRecycler.scrollToPosition(0)
}
}
}
libraryModel.sortMode.observe(viewLifecycleOwner) { mode -> libraryModel.sortMode.observe(viewLifecycleOwner) { mode ->
Log.d(this::class.simpleName, "Updating sort mode to $mode") Log.d(this::class.simpleName, "Updating sort mode to $mode")
// Update the adapter with the new data
libraryAdapter.updateData(
mode.getSortedBaseModelList(
musicStore.getListForShowMode(libraryModel.displayMode.value!!)
)
)
// Then update the menu item in the toolbar to reflect the new mode // Then update the menu item in the toolbar to reflect the new mode
binding.libraryToolbar.menu.forEach { binding.libraryToolbar.menu.forEach {
if (it.itemId == libraryModel.sortMode.value!!.toMenuId()) { if (it.itemId == libraryModel.sortMode.value!!.toMenuId()) {
@ -147,14 +148,6 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
} }
} }
libraryModel.searchResults.observe(viewLifecycleOwner) {
if (libraryModel.searchHasFocus) {
searchAdapter.submitList(it) {
binding.libraryRecycler.scrollToPosition(0)
}
}
}
playbackModel.navToItem.observe(viewLifecycleOwner) { playbackModel.navToItem.observe(viewLifecycleOwner) {
if (it != null) { if (it != null) {
libraryModel.updateNavigationStatus(false) libraryModel.updateNavigationStatus(false)
@ -188,6 +181,7 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
private fun showActionsForItem(data: BaseModel, view: View) { private fun showActionsForItem(data: BaseModel, view: View) {
val menu = PopupMenu(requireContext(), view) val menu = PopupMenu(requireContext(), view)
when (data) { when (data) {
is Song -> menu.setupSongActions(data, requireContext(), playbackModel) is Song -> menu.setupSongActions(data, requireContext(), playbackModel)
is Album -> menu.setupAlbumActions(data, requireContext(), playbackModel) is Album -> menu.setupAlbumActions(data, requireContext(), playbackModel)

View file

@ -21,15 +21,17 @@ import org.oxycblt.auxio.settings.SettingsManager
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class LibraryViewModel : ViewModel(), SettingsManager.Callback { class LibraryViewModel : ViewModel(), SettingsManager.Callback {
private val mDisplayMode = MutableLiveData(DisplayMode.SHOW_ARTISTS)
val displayMode: LiveData<DisplayMode> get() = mDisplayMode
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 mLibraryData = MutableLiveData(listOf<BaseModel>())
val libraryData: LiveData<List<BaseModel>> get() = mLibraryData
private val mSearchResults = MutableLiveData(listOf<BaseModel>()) private val mSearchResults = MutableLiveData(listOf<BaseModel>())
val searchResults: LiveData<List<BaseModel>> get() = mSearchResults val searchResults: LiveData<List<BaseModel>> get() = mSearchResults
private var mDisplayMode = DisplayMode.SHOW_ARTISTS
private var mIsNavigating = false private var mIsNavigating = false
val isNavigating: Boolean get() = mIsNavigating val isNavigating: Boolean get() = mIsNavigating
@ -37,14 +39,19 @@ class LibraryViewModel : ViewModel(), SettingsManager.Callback {
val searchHasFocus: Boolean get() = mSearchHasFocus val searchHasFocus: Boolean get() = mSearchHasFocus
private val settingsManager = SettingsManager.getInstance() private val settingsManager = SettingsManager.getInstance()
private val musicStore = MusicStore.getInstance()
init { init {
settingsManager.addCallback(this) settingsManager.addCallback(this)
mDisplayMode.value = settingsManager.libraryDisplayMode mDisplayMode = settingsManager.libraryDisplayMode
mSortMode.value = settingsManager.librarySortMode mSortMode.value = settingsManager.librarySortMode
updateLibraryData()
} }
// --- SEARCH FUNCTIONS ---
/** /**
* Perform a search of the music library, given a query. * Perform a search of the music library, given a query.
* Results are pushed to [searchResults]. * Results are pushed to [searchResults].
@ -63,9 +70,8 @@ class LibraryViewModel : ViewModel(), SettingsManager.Callback {
// the query, and update the LiveData with those items. This is done on a separate // the query, and update the LiveData with those items. This is done on a separate
// thread as it can be a very long operation for large music libraries. // thread as it can be a very long operation for large music libraries.
viewModelScope.launch { viewModelScope.launch {
val musicStore = MusicStore.getInstance()
val combined = mutableListOf<BaseModel>() val combined = mutableListOf<BaseModel>()
val children = displayMode.value!!.getChildren() val children = mDisplayMode.getChildren()
// If the Library DisplayMode supports it, include artists / genres in the search. // If the Library DisplayMode supports it, include artists / genres in the search.
if (children.contains(DisplayMode.SHOW_GENRES)) { if (children.contains(DisplayMode.SHOW_GENRES)) {
@ -105,17 +111,15 @@ class LibraryViewModel : ViewModel(), SettingsManager.Callback {
} }
} }
fun updateSearchFocusStatus(value: Boolean) {
mSearchHasFocus = value
}
fun resetQuery() { fun resetQuery() {
mSearchResults.value = listOf() mSearchResults.value = listOf()
} }
fun updateNavigationStatus(value: Boolean) { // --- LIBRARY FUNCTIONS ---
mIsNavigating = value
}
fun updateSearchFocusStatus(value: Boolean) {
mSearchHasFocus = value
}
fun updateSortMode(@IdRes itemId: Int) { fun updateSortMode(@IdRes itemId: Int) {
val mode = when (itemId) { val mode = when (itemId) {
@ -128,11 +132,16 @@ class LibraryViewModel : ViewModel(), SettingsManager.Callback {
if (mode != mSortMode.value) { if (mode != mSortMode.value) {
mSortMode.value = mode mSortMode.value = mode
settingsManager.librarySortMode = mode settingsManager.librarySortMode = mode
updateLibraryData()
} }
} }
fun updateNavigationStatus(value: Boolean) {
mIsNavigating = value
}
// --- OVERRIDES --- // --- OVERRIDES ---
override fun onCleared() { override fun onCleared() {
@ -142,6 +151,16 @@ class LibraryViewModel : ViewModel(), SettingsManager.Callback {
} }
override fun onLibDisplayModeUpdate(displayMode: DisplayMode) { override fun onLibDisplayModeUpdate(displayMode: DisplayMode) {
mDisplayMode.value = displayMode mDisplayMode = displayMode
updateLibraryData()
}
// --- UTILS ---
private fun updateLibraryData() {
mLibraryData.value = mSortMode.value!!.getSortedBaseModelList(
musicStore.getListForShowMode(mDisplayMode)
)
} }
} }

View file

@ -1,69 +1,77 @@
package org.oxycblt.auxio.library.adapters package org.oxycblt.auxio.library.adapters
import android.util.Log
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
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
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.recycler.DisplayMode import org.oxycblt.auxio.music.Header
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.recycler.DiffCallback
import org.oxycblt.auxio.recycler.viewholders.AlbumViewHolder import org.oxycblt.auxio.recycler.viewholders.AlbumViewHolder
import org.oxycblt.auxio.recycler.viewholders.ArtistViewHolder import org.oxycblt.auxio.recycler.viewholders.ArtistViewHolder
import org.oxycblt.auxio.recycler.viewholders.GenreViewHolder import org.oxycblt.auxio.recycler.viewholders.GenreViewHolder
import org.oxycblt.auxio.recycler.viewholders.HeaderViewHolder
import org.oxycblt.auxio.recycler.viewholders.SongViewHolder
/** /**
* The primary recyclerview for the library. Can display either Genres, Artists, and Albums. * A near-identical adapter as [SearchAdapter] but this one isn't a [ListAdapter]
* Id love to unify these two adapters but that triggers a bug on the android backend, so...
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class LibraryAdapter( class LibraryAdapter(
private val displayMode: DisplayMode,
private val doOnClick: (data: BaseModel) -> Unit, private val doOnClick: (data: BaseModel) -> Unit,
private val doOnLongClick: (data: BaseModel, view: View) -> Unit private val doOnLongClick: (data: BaseModel, view: View) -> Unit
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { ) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private var data: List<BaseModel> private var data = listOf<BaseModel>()
init {
// Assign the data on startup depending on the type
data = when (displayMode) {
DisplayMode.SHOW_GENRES -> listOf<Genre>()
DisplayMode.SHOW_ARTISTS -> listOf<Artist>()
DisplayMode.SHOW_ALBUMS -> listOf<Album>()
else -> listOf<Artist>()
}
}
override fun getItemCount(): Int = data.size override fun getItemCount(): Int = data.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { override fun getItemViewType(position: Int): Int {
// Return a different View Holder depending on the show type return when (data[position]) {
return when (displayMode) { is Genre -> GenreViewHolder.ITEM_TYPE
DisplayMode.SHOW_GENRES -> GenreViewHolder.from(parent.context, doOnClick, doOnLongClick) is Artist -> ArtistViewHolder.ITEM_TYPE
DisplayMode.SHOW_ARTISTS -> ArtistViewHolder.from(parent.context, doOnClick, doOnLongClick) is Album -> AlbumViewHolder.ITEM_TYPE
DisplayMode.SHOW_ALBUMS -> AlbumViewHolder.from(parent.context, doOnClick, doOnLongClick)
else -> error("Bad DisplayMode given.") else -> -1
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
GenreViewHolder.ITEM_TYPE -> GenreViewHolder.from(
parent.context, doOnClick, doOnLongClick
)
ArtistViewHolder.ITEM_TYPE -> ArtistViewHolder.from(
parent.context, doOnClick, doOnLongClick
)
AlbumViewHolder.ITEM_TYPE -> AlbumViewHolder.from(
parent.context, doOnClick, doOnLongClick
)
else -> error("Invalid viewholder item type.")
} }
} }
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (displayMode) { when (val item = data[position]) {
DisplayMode.SHOW_GENRES -> (holder as GenreViewHolder).bind(data[position] as Genre) is Genre -> (holder as GenreViewHolder).bind(item)
DisplayMode.SHOW_ARTISTS -> (holder as ArtistViewHolder).bind(data[position] as Artist) is Artist -> (holder as ArtistViewHolder).bind(item)
DisplayMode.SHOW_ALBUMS -> (holder as AlbumViewHolder).bind(data[position] as Album) is Album -> (holder as AlbumViewHolder).bind(item)
else -> return
} }
} }
// Update the data, as its an internal value.
fun updateData(newData: List<BaseModel>) { fun updateData(newData: List<BaseModel>) {
if (data != newData) { data = newData
data = newData
notifyDataSetChanged() notifyDataSetChanged()
}
} }
} }

View file

@ -1,7 +1,9 @@
package org.oxycblt.auxio.library.adapters package org.oxycblt.auxio.library.adapters
import android.util.Log
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album

View file

@ -29,9 +29,10 @@ import org.oxycblt.auxio.ui.toColor
/** /**
* A semi-copy, semi-custom implementation of [com.reddit.indicatorfastscroll.FastScrollerThumbView] * A semi-copy, semi-custom implementation of [com.reddit.indicatorfastscroll.FastScrollerThumbView]
* that fixes a memory leak that occurs from a bug fix they added. All credit goes to the authors of * that fixes a memory leak that occurs from a bug fix they added.
* the fast scroll library. * All credit goes to the authors of the fast scroll library.
* <a href="https://github.com/reddit/IndicatorFastScroll"> Link to repo </a> *
* https://github.com/reddit/IndicatorFastScroll
* @author Reddit, OxygenCobalt * @author Reddit, OxygenCobalt
*/ */
class NoLeakThumbView @JvmOverloads constructor( class NoLeakThumbView @JvmOverloads constructor(

View file

@ -68,6 +68,13 @@ class SongsFragment : Fragment() {
binding.songRecycler.apply { binding.songRecycler.apply {
adapter = songAdapter adapter = songAdapter
setHasFixedSize(true) setHasFixedSize(true)
post {
if (computeVerticalScrollRange() < height) {
binding.songFastScroll.visibility = View.GONE
binding.songFastScrollThumb.visibility = View.GONE
}
}
} }
setupFastScroller(binding) setupFastScroller(binding)

View file

@ -80,6 +80,7 @@
<item name="colorPrimary">@color/control_color</item> <item name="colorPrimary">@color/control_color</item>
<item name="colorSecondary">@color/control_color</item> <item name="colorSecondary">@color/control_color</item>
<item name="dialogCornerRadius">0dp</item> <item name="dialogCornerRadius">0dp</item>
<item name="colorControlHighlight">@color/selection_color</item>
</style> </style>
<!-- Custom dialog title theme --> <!-- Custom dialog title theme -->