Add filtering to library search
Add filtering to the library search bar.
This commit is contained in:
parent
406ba212f8
commit
6627de4b62
7 changed files with 195 additions and 66 deletions
|
@ -41,8 +41,7 @@ import org.oxycblt.auxio.ui.toColor
|
|||
* search functionality.
|
||||
* FIXME: Heisenleak when navving from search
|
||||
* FIXME: Heisenleak on older versions
|
||||
* TODO: Filtering & Search order upgrades
|
||||
* TODO: Show result counts?
|
||||
* TODO: Filtering
|
||||
*/
|
||||
class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
|
||||
|
||||
|
@ -61,10 +60,46 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
|
|||
val searchAdapter = SearchAdapter(::onItemSelection, ::showActionsForItem)
|
||||
|
||||
val sortAction = binding.libraryToolbar.menu.findItem(R.id.submenu_sorting)
|
||||
val filterAction = binding.libraryToolbar.menu.findItem(R.id.submenu_filtering)
|
||||
val searchView: SearchView
|
||||
|
||||
// --- UI SETUP ---
|
||||
|
||||
binding.libraryToolbar.apply {
|
||||
menu.apply {
|
||||
val searchAction = findItem(R.id.action_search)
|
||||
searchView = searchAction.actionView as SearchView
|
||||
|
||||
searchView.queryHint = getString(R.string.hint_search_library)
|
||||
searchView.maxWidth = Int.MAX_VALUE
|
||||
searchView.setOnQueryTextListener(this@LibraryFragment)
|
||||
|
||||
searchAction.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
|
||||
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
|
||||
binding.libraryRecycler.adapter = searchAdapter
|
||||
|
||||
searchAction.isVisible = false
|
||||
sortAction.isVisible = false
|
||||
filterAction.isVisible = true
|
||||
|
||||
libraryModel.resetQuery()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
|
||||
binding.libraryRecycler.adapter = libraryAdapter
|
||||
|
||||
searchAction.isVisible = true
|
||||
sortAction.isVisible = true
|
||||
filterAction.isVisible = false
|
||||
|
||||
libraryModel.resetQuery()
|
||||
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
setOnMenuItemClickListener {
|
||||
when (it.itemId) {
|
||||
R.id.action_search -> {
|
||||
|
@ -74,45 +109,22 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
|
|||
it.expandActionView()
|
||||
}
|
||||
|
||||
R.id.submenu_sorting -> {
|
||||
}
|
||||
R.id.submenu_sorting -> {}
|
||||
|
||||
else -> libraryModel.updateSortMode(it.itemId)
|
||||
R.id.submenu_filtering -> {}
|
||||
|
||||
else -> {
|
||||
if (sortAction.isVisible) {
|
||||
libraryModel.updateSortMode(it.itemId)
|
||||
} else if (filterAction.isVisible) {
|
||||
libraryModel.updateFilterMode(it.itemId)
|
||||
libraryModel.doSearch(searchView.query.toString(), requireContext())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
menu.apply {
|
||||
val searchAction = findItem(R.id.action_search)
|
||||
val searchView = searchAction.actionView as SearchView
|
||||
|
||||
searchView.queryHint = getString(R.string.hint_search_library)
|
||||
searchView.maxWidth = Int.MAX_VALUE
|
||||
searchView.setOnQueryTextListener(this@LibraryFragment)
|
||||
|
||||
searchAction.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
|
||||
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
|
||||
binding.libraryRecycler.adapter = searchAdapter
|
||||
item.isVisible = false
|
||||
sortAction.isVisible = false
|
||||
|
||||
libraryModel.resetQuery()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
|
||||
binding.libraryRecycler.adapter = libraryAdapter
|
||||
item.isVisible = true
|
||||
sortAction.isVisible = true
|
||||
|
||||
libraryModel.resetQuery()
|
||||
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
binding.libraryRecycler.apply {
|
||||
|
@ -164,6 +176,20 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
|
|||
}
|
||||
}
|
||||
|
||||
libraryModel.filterMode.observe(viewLifecycleOwner) { mode ->
|
||||
logD("Updating filter mode to $mode")
|
||||
|
||||
val modeId = mode.toMenuId()
|
||||
|
||||
filterAction.subMenu.forEach {
|
||||
if (it.itemId == modeId) {
|
||||
it.applyColor(accent.first.toColor(requireContext()))
|
||||
} else {
|
||||
it.applyColor(resolveAttr(requireContext(), android.R.attr.textColorPrimary))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
detailModel.navToItem.observe(viewLifecycleOwner) {
|
||||
if (it != null) {
|
||||
libraryModel.updateNavigationStatus(false)
|
||||
|
|
|
@ -22,16 +22,22 @@ import org.oxycblt.auxio.settings.SettingsManager
|
|||
*/
|
||||
class LibraryViewModel : ViewModel(), SettingsManager.Callback {
|
||||
private val mSortMode = MutableLiveData(SortMode.ALPHA_DOWN)
|
||||
private val mLibraryData = MutableLiveData(listOf<BaseModel>())
|
||||
private val mSearchResults = MutableLiveData(listOf<BaseModel>())
|
||||
private var mDisplayMode = DisplayMode.SHOW_ARTISTS
|
||||
private var mIsNavigating = false
|
||||
|
||||
val sortMode: LiveData<SortMode> get() = mSortMode
|
||||
|
||||
private val mLibraryData = MutableLiveData(listOf<BaseModel>())
|
||||
val libraryData: LiveData<List<BaseModel>> get() = mLibraryData
|
||||
|
||||
private val mFilterMode = MutableLiveData(DisplayMode.SHOW_ALL)
|
||||
val filterMode: LiveData<DisplayMode> get() = mFilterMode
|
||||
|
||||
private val mSearchResults = MutableLiveData(listOf<BaseModel>())
|
||||
val searchResults: LiveData<List<BaseModel>> get() = mSearchResults
|
||||
|
||||
private var mIsNavigating = false
|
||||
val isNavigating: Boolean get() = mIsNavigating
|
||||
|
||||
private var mDisplayMode = DisplayMode.SHOW_ARTISTS
|
||||
|
||||
private val settingsManager = SettingsManager.getInstance()
|
||||
private val musicStore = MusicStore.getInstance()
|
||||
|
||||
|
@ -41,6 +47,7 @@ class LibraryViewModel : ViewModel(), SettingsManager.Callback {
|
|||
// Set up the display/sort modes
|
||||
mDisplayMode = settingsManager.libraryDisplayMode
|
||||
mSortMode.value = settingsManager.librarySortMode
|
||||
mFilterMode.value = settingsManager.libraryFilterMode
|
||||
|
||||
updateLibraryData()
|
||||
}
|
||||
|
@ -74,8 +81,8 @@ class LibraryViewModel : ViewModel(), SettingsManager.Callback {
|
|||
}
|
||||
|
||||
DisplayMode.SHOW_ARTISTS -> {
|
||||
searchForArtists(combined, query, context) -
|
||||
searchForAlbums(combined, query, context)
|
||||
searchForArtists(combined, query, context)
|
||||
searchForAlbums(combined, query, context)
|
||||
searchForGenres(combined, query, context)
|
||||
}
|
||||
|
||||
|
@ -84,6 +91,8 @@ class LibraryViewModel : ViewModel(), SettingsManager.Callback {
|
|||
searchForArtists(combined, query, context)
|
||||
searchForGenres(combined, query, context)
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
|
||||
mSearchResults.value = combined
|
||||
|
@ -95,11 +104,15 @@ class LibraryViewModel : ViewModel(), SettingsManager.Callback {
|
|||
query: String,
|
||||
context: Context
|
||||
): MutableList<BaseModel> {
|
||||
val genres = musicStore.genres.filter { it.name.contains(query, true) }
|
||||
if (mFilterMode.value == DisplayMode.SHOW_ALL ||
|
||||
mFilterMode.value == DisplayMode.SHOW_GENRES
|
||||
) {
|
||||
val genres = musicStore.genres.filter { it.name.contains(query, true) }
|
||||
|
||||
if (genres.isNotEmpty()) {
|
||||
data.add(Header(id = 0, name = context.getString(R.string.label_genres)))
|
||||
data.addAll(genres)
|
||||
if (genres.isNotEmpty()) {
|
||||
data.add(Header(id = 0, name = context.getString(R.string.label_genres)))
|
||||
data.addAll(genres)
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
|
@ -110,11 +123,15 @@ class LibraryViewModel : ViewModel(), SettingsManager.Callback {
|
|||
query: String,
|
||||
context: Context
|
||||
): MutableList<BaseModel> {
|
||||
val artists = musicStore.artists.filter { it.name.contains(query, true) }
|
||||
if (mFilterMode.value == DisplayMode.SHOW_ALL ||
|
||||
mFilterMode.value == DisplayMode.SHOW_ARTISTS
|
||||
) {
|
||||
val artists = musicStore.artists.filter { it.name.contains(query, true) }
|
||||
|
||||
if (artists.isNotEmpty()) {
|
||||
data.add(Header(id = 1, name = context.getString(R.string.label_artists)))
|
||||
data.addAll(artists)
|
||||
if (artists.isNotEmpty()) {
|
||||
data.add(Header(id = 1, name = context.getString(R.string.label_artists)))
|
||||
data.addAll(artists)
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
|
@ -125,16 +142,36 @@ class LibraryViewModel : ViewModel(), SettingsManager.Callback {
|
|||
query: String,
|
||||
context: Context
|
||||
): MutableList<BaseModel> {
|
||||
val albums = musicStore.albums.filter { it.name.contains(query, true) }
|
||||
if (mFilterMode.value == DisplayMode.SHOW_ALL ||
|
||||
mFilterMode.value == DisplayMode.SHOW_ALBUMS
|
||||
) {
|
||||
val albums = musicStore.albums.filter { it.name.contains(query, true) }
|
||||
|
||||
if (albums.isNotEmpty()) {
|
||||
data.add(Header(id = 2, name = context.getString(R.string.label_albums)))
|
||||
data.addAll(albums)
|
||||
if (albums.isNotEmpty()) {
|
||||
data.add(Header(id = 2, name = context.getString(R.string.label_albums)))
|
||||
data.addAll(albums)
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
fun updateFilterMode(@IdRes itemId: Int) {
|
||||
val mode = when (itemId) {
|
||||
R.id.option_filter_all -> DisplayMode.SHOW_ALL
|
||||
R.id.option_filter_albums -> DisplayMode.SHOW_ALBUMS
|
||||
R.id.option_filter_artists -> DisplayMode.SHOW_ARTISTS
|
||||
R.id.option_filter_genres -> DisplayMode.SHOW_GENRES
|
||||
|
||||
else -> DisplayMode.SHOW_ALL
|
||||
}
|
||||
|
||||
if (mFilterMode.value != mode) {
|
||||
mFilterMode.value = mode
|
||||
settingsManager.libraryFilterMode = mode
|
||||
}
|
||||
}
|
||||
|
||||
fun resetQuery() {
|
||||
mSearchResults.value = listOf()
|
||||
}
|
||||
|
@ -203,6 +240,8 @@ class LibraryViewModel : ViewModel(), SettingsManager.Callback {
|
|||
DisplayMode.SHOW_ALBUMS -> {
|
||||
mSortMode.value!!.getSortedAlbumList(musicStore.albums)
|
||||
}
|
||||
|
||||
else -> error("DisplayMode $mDisplayMode is unsupported.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package org.oxycblt.auxio.recycler
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.IdRes
|
||||
import org.oxycblt.auxio.R
|
||||
|
||||
/**
|
||||
|
@ -8,20 +9,22 @@ import org.oxycblt.auxio.R
|
|||
* @author OxygenCobalt
|
||||
*/
|
||||
enum class DisplayMode(@DrawableRes val iconRes: Int) {
|
||||
SHOW_ALL(R.drawable.ic_sort_none),
|
||||
SHOW_GENRES(R.drawable.ic_genre),
|
||||
SHOW_ARTISTS(R.drawable.ic_artist),
|
||||
SHOW_ALBUMS(R.drawable.ic_album);
|
||||
|
||||
/**
|
||||
* Make a slice of all the values that this DisplayMode covers.
|
||||
*
|
||||
* ex. SHOW_ARTISTS would return SHOW_ARTISTS, SHOW_ALBUMS, and SHOW_SONGS
|
||||
* @return The values that this DisplayMode covers.
|
||||
* Get a menu action for this show mode. Corresponds to filter actions.
|
||||
*/
|
||||
fun getChildren(): List<DisplayMode> {
|
||||
val vals = values()
|
||||
|
||||
return vals.slice(vals.indexOf(this) until vals.size)
|
||||
@IdRes
|
||||
fun toMenuId(): Int {
|
||||
return when (this) {
|
||||
SHOW_ALL -> (R.id.option_filter_all)
|
||||
SHOW_ALBUMS -> (R.id.option_filter_albums)
|
||||
SHOW_ARTISTS -> (R.id.option_filter_artists)
|
||||
SHOW_GENRES -> (R.id.option_filter_genres)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -144,6 +144,22 @@ class SettingsManager private constructor(context: Context) :
|
|||
.apply()
|
||||
}
|
||||
|
||||
/**
|
||||
* The current [DisplayMode] of the library search filtering
|
||||
*/
|
||||
var libraryFilterMode: DisplayMode
|
||||
get() = DisplayMode.valueOfOrFallback(
|
||||
sharedPrefs.getString(
|
||||
Keys.KEY_LIBRARY_FILTER_MODE,
|
||||
DisplayMode.SHOW_ARTISTS.toString()
|
||||
)
|
||||
)
|
||||
set(value) {
|
||||
sharedPrefs.edit()
|
||||
.putString(Keys.KEY_LIBRARY_FILTER_MODE, value.toString())
|
||||
.apply()
|
||||
}
|
||||
|
||||
// --- CALLBACKS ---
|
||||
|
||||
private val callbacks = mutableListOf<Callback>()
|
||||
|
@ -231,6 +247,7 @@ class SettingsManager private constructor(context: Context) :
|
|||
const val KEY_PREV_REWIND = "KEY_PREV_REWIND"
|
||||
|
||||
const val KEY_LIBRARY_SORT_MODE = "KEY_LIBRARY_SORT_MODE"
|
||||
const val KEY_LIBRARY_FILTER_MODE = "KEY_LIBRARY_FILTER_MODE"
|
||||
const val KEY_DEBUG_SAVE = "KEY_SAVE_STATE"
|
||||
}
|
||||
|
||||
|
|
11
app/src/main/res/drawable/ic_filter.xml
Normal file
11
app/src/main/res/drawable/ic_filter.xml
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M10,18h4v-2h-4v2zM3,6v2h18L21,6L3,6zM6,13h12v-2L6,11v2z"/>
|
||||
</vector>
|
|
@ -12,8 +12,8 @@
|
|||
tools:ignore="AlwaysShowAction" />
|
||||
|
||||
<!--
|
||||
This action has to be always shown since android mangles this action when I make it invisible
|
||||
and then visible again.
|
||||
This action has to be always shown since otherwise android mangles this action when I make '
|
||||
it invisible and then visible again.
|
||||
I hate this platform so much.
|
||||
-->
|
||||
<item
|
||||
|
@ -38,4 +38,32 @@
|
|||
</group>
|
||||
</menu>
|
||||
</item>
|
||||
|
||||
<item
|
||||
android:id="@+id/submenu_filtering"
|
||||
android:title="@string/label_filter"
|
||||
android:icon="@drawable/ic_filter"
|
||||
android:visible="false"
|
||||
app:showAsAction="always">
|
||||
<menu>
|
||||
<group android:id="@+id/group_filtering">
|
||||
<item
|
||||
android:id="@+id/option_filter_all"
|
||||
android:title="@string/label_filter_all"
|
||||
app:showAsAction="never" />
|
||||
<item
|
||||
android:id="@+id/option_filter_albums"
|
||||
android:title="@string/label_albums"
|
||||
app:showAsAction="never" />
|
||||
<item
|
||||
android:id="@+id/option_filter_artists"
|
||||
android:title="@string/label_artists"
|
||||
app:showAsAction="never" />
|
||||
<item
|
||||
android:id="@+id/option_filter_genres"
|
||||
android:title="@string/label_genres"
|
||||
app:showAsAction="never" />
|
||||
</group>
|
||||
</menu>
|
||||
</item>
|
||||
</menu>
|
|
@ -11,7 +11,10 @@
|
|||
<string name="label_genres">Genres</string>
|
||||
<string name="label_artists">Artists</string>
|
||||
<string name="label_albums">Albums</string>
|
||||
|
||||
<string name="label_search">Search</string>
|
||||
<string name="label_filter">Filter</string>
|
||||
<string name="label_filter_all">All</string>
|
||||
|
||||
<string name="label_sort">Sort</string>
|
||||
<string name="label_sort_none">Default</string>
|
||||
|
@ -69,7 +72,9 @@
|
|||
<string name="setting_show_covers_desc">Turn off to save memory usage</string>
|
||||
|
||||
<string name="setting_quality_covers">Ignore MediaStore covers</string>
|
||||
<string name="setting_quality_covers_desc">Results in higher quality album covers, but causes slower loading and higher memory usage</string>
|
||||
<string name="setting_quality_covers_desc">
|
||||
Results in higher quality album covers, but causes slower loading and higher memory usage
|
||||
</string>
|
||||
|
||||
<string name="setting_use_alt_action">Use alternate notification action</string>
|
||||
<string name="setting_use_alt_loop">Prefer repeat mode action</string>
|
||||
|
|
Loading…
Reference in a new issue