Refactor library structure
Refactor LibraryFragment & LibraryViewModel so that LibraryViewModel only holds the data and LibraryFragment displays it.
This commit is contained in:
parent
60750d976b
commit
859391e69b
7 changed files with 102 additions and 70 deletions
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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 -->
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue