Add filtering to library search

Add filtering to the library search bar.
This commit is contained in:
OxygenCobalt 2021-01-07 10:40:10 -07:00
parent 406ba212f8
commit 6627de4b62
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
7 changed files with 195 additions and 66 deletions

View file

@ -41,8 +41,7 @@ import org.oxycblt.auxio.ui.toColor
* search functionality. * search functionality.
* FIXME: Heisenleak when navving from search * FIXME: Heisenleak when navving from search
* FIXME: Heisenleak on older versions * FIXME: Heisenleak on older versions
* TODO: Filtering & Search order upgrades * TODO: Filtering
* TODO: Show result counts?
*/ */
class LibraryFragment : Fragment(), SearchView.OnQueryTextListener { class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
@ -61,10 +60,46 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
val searchAdapter = SearchAdapter(::onItemSelection, ::showActionsForItem) val searchAdapter = SearchAdapter(::onItemSelection, ::showActionsForItem)
val sortAction = binding.libraryToolbar.menu.findItem(R.id.submenu_sorting) 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 --- // --- UI SETUP ---
binding.libraryToolbar.apply { 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 { setOnMenuItemClickListener {
when (it.itemId) { when (it.itemId) {
R.id.action_search -> { R.id.action_search -> {
@ -74,45 +109,22 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
it.expandActionView() 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 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 { 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) { detailModel.navToItem.observe(viewLifecycleOwner) {
if (it != null) { if (it != null) {
libraryModel.updateNavigationStatus(false) libraryModel.updateNavigationStatus(false)

View file

@ -22,16 +22,22 @@ import org.oxycblt.auxio.settings.SettingsManager
*/ */
class LibraryViewModel : ViewModel(), SettingsManager.Callback { class LibraryViewModel : ViewModel(), SettingsManager.Callback {
private val mSortMode = MutableLiveData(SortMode.ALPHA_DOWN) 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 val sortMode: LiveData<SortMode> get() = mSortMode
private val mLibraryData = MutableLiveData(listOf<BaseModel>())
val libraryData: LiveData<List<BaseModel>> get() = mLibraryData 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 val searchResults: LiveData<List<BaseModel>> get() = mSearchResults
private var mIsNavigating = false
val isNavigating: Boolean get() = mIsNavigating val isNavigating: Boolean get() = mIsNavigating
private var mDisplayMode = DisplayMode.SHOW_ARTISTS
private val settingsManager = SettingsManager.getInstance() private val settingsManager = SettingsManager.getInstance()
private val musicStore = MusicStore.getInstance() private val musicStore = MusicStore.getInstance()
@ -41,6 +47,7 @@ class LibraryViewModel : ViewModel(), SettingsManager.Callback {
// Set up the display/sort modes // Set up the display/sort modes
mDisplayMode = settingsManager.libraryDisplayMode mDisplayMode = settingsManager.libraryDisplayMode
mSortMode.value = settingsManager.librarySortMode mSortMode.value = settingsManager.librarySortMode
mFilterMode.value = settingsManager.libraryFilterMode
updateLibraryData() updateLibraryData()
} }
@ -74,7 +81,7 @@ class LibraryViewModel : ViewModel(), SettingsManager.Callback {
} }
DisplayMode.SHOW_ARTISTS -> { DisplayMode.SHOW_ARTISTS -> {
searchForArtists(combined, query, context) - searchForArtists(combined, query, context)
searchForAlbums(combined, query, context) searchForAlbums(combined, query, context)
searchForGenres(combined, query, context) searchForGenres(combined, query, context)
} }
@ -84,6 +91,8 @@ class LibraryViewModel : ViewModel(), SettingsManager.Callback {
searchForArtists(combined, query, context) searchForArtists(combined, query, context)
searchForGenres(combined, query, context) searchForGenres(combined, query, context)
} }
else -> {}
} }
mSearchResults.value = combined mSearchResults.value = combined
@ -95,12 +104,16 @@ class LibraryViewModel : ViewModel(), SettingsManager.Callback {
query: String, query: String,
context: Context context: Context
): MutableList<BaseModel> { ): MutableList<BaseModel> {
if (mFilterMode.value == DisplayMode.SHOW_ALL ||
mFilterMode.value == DisplayMode.SHOW_GENRES
) {
val genres = musicStore.genres.filter { it.name.contains(query, true) } val genres = musicStore.genres.filter { it.name.contains(query, true) }
if (genres.isNotEmpty()) { if (genres.isNotEmpty()) {
data.add(Header(id = 0, name = context.getString(R.string.label_genres))) data.add(Header(id = 0, name = context.getString(R.string.label_genres)))
data.addAll(genres) data.addAll(genres)
} }
}
return data return data
} }
@ -110,12 +123,16 @@ class LibraryViewModel : ViewModel(), SettingsManager.Callback {
query: String, query: String,
context: Context context: Context
): MutableList<BaseModel> { ): MutableList<BaseModel> {
if (mFilterMode.value == DisplayMode.SHOW_ALL ||
mFilterMode.value == DisplayMode.SHOW_ARTISTS
) {
val artists = musicStore.artists.filter { it.name.contains(query, true) } val artists = musicStore.artists.filter { it.name.contains(query, true) }
if (artists.isNotEmpty()) { if (artists.isNotEmpty()) {
data.add(Header(id = 1, name = context.getString(R.string.label_artists))) data.add(Header(id = 1, name = context.getString(R.string.label_artists)))
data.addAll(artists) data.addAll(artists)
} }
}
return data return data
} }
@ -125,16 +142,36 @@ class LibraryViewModel : ViewModel(), SettingsManager.Callback {
query: String, query: String,
context: Context context: Context
): MutableList<BaseModel> { ): MutableList<BaseModel> {
if (mFilterMode.value == DisplayMode.SHOW_ALL ||
mFilterMode.value == DisplayMode.SHOW_ALBUMS
) {
val albums = musicStore.albums.filter { it.name.contains(query, true) } val albums = musicStore.albums.filter { it.name.contains(query, true) }
if (albums.isNotEmpty()) { if (albums.isNotEmpty()) {
data.add(Header(id = 2, name = context.getString(R.string.label_albums))) data.add(Header(id = 2, name = context.getString(R.string.label_albums)))
data.addAll(albums) data.addAll(albums)
} }
}
return data 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() { fun resetQuery() {
mSearchResults.value = listOf() mSearchResults.value = listOf()
} }
@ -203,6 +240,8 @@ class LibraryViewModel : ViewModel(), SettingsManager.Callback {
DisplayMode.SHOW_ALBUMS -> { DisplayMode.SHOW_ALBUMS -> {
mSortMode.value!!.getSortedAlbumList(musicStore.albums) mSortMode.value!!.getSortedAlbumList(musicStore.albums)
} }
else -> error("DisplayMode $mDisplayMode is unsupported.")
} }
} }
} }

View file

@ -1,6 +1,7 @@
package org.oxycblt.auxio.recycler package org.oxycblt.auxio.recycler
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.annotation.IdRes
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
/** /**
@ -8,20 +9,22 @@ import org.oxycblt.auxio.R
* @author OxygenCobalt * @author OxygenCobalt
*/ */
enum class DisplayMode(@DrawableRes val iconRes: Int) { enum class DisplayMode(@DrawableRes val iconRes: Int) {
SHOW_ALL(R.drawable.ic_sort_none),
SHOW_GENRES(R.drawable.ic_genre), SHOW_GENRES(R.drawable.ic_genre),
SHOW_ARTISTS(R.drawable.ic_artist), SHOW_ARTISTS(R.drawable.ic_artist),
SHOW_ALBUMS(R.drawable.ic_album); SHOW_ALBUMS(R.drawable.ic_album);
/** /**
* Make a slice of all the values that this DisplayMode covers. * Get a menu action for this show mode. Corresponds to filter actions.
*
* ex. SHOW_ARTISTS would return SHOW_ARTISTS, SHOW_ALBUMS, and SHOW_SONGS
* @return The values that this DisplayMode covers.
*/ */
fun getChildren(): List<DisplayMode> { @IdRes
val vals = values() fun toMenuId(): Int {
return when (this) {
return vals.slice(vals.indexOf(this) until vals.size) 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 { companion object {

View file

@ -144,6 +144,22 @@ class SettingsManager private constructor(context: Context) :
.apply() .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 --- // --- CALLBACKS ---
private val callbacks = mutableListOf<Callback>() 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_PREV_REWIND = "KEY_PREV_REWIND"
const val KEY_LIBRARY_SORT_MODE = "KEY_LIBRARY_SORT_MODE" 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" const val KEY_DEBUG_SAVE = "KEY_SAVE_STATE"
} }

View 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>

View file

@ -12,8 +12,8 @@
tools:ignore="AlwaysShowAction" /> tools:ignore="AlwaysShowAction" />
<!-- <!--
This action has to be always shown since android mangles this action when I make it invisible This action has to be always shown since otherwise android mangles this action when I make '
and then visible again. it invisible and then visible again.
I hate this platform so much. I hate this platform so much.
--> -->
<item <item
@ -38,4 +38,32 @@
</group> </group>
</menu> </menu>
</item> </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> </menu>

View file

@ -11,7 +11,10 @@
<string name="label_genres">Genres</string> <string name="label_genres">Genres</string>
<string name="label_artists">Artists</string> <string name="label_artists">Artists</string>
<string name="label_albums">Albums</string> <string name="label_albums">Albums</string>
<string name="label_search">Search</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">Sort</string>
<string name="label_sort_none">Default</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_show_covers_desc">Turn off to save memory usage</string>
<string name="setting_quality_covers">Ignore MediaStore covers</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_action">Use alternate notification action</string>
<string name="setting_use_alt_loop">Prefer repeat mode action</string> <string name="setting_use_alt_loop">Prefer repeat mode action</string>