Add filtering to SearchFragment

Add the ability to filter items to SearchFragment
This commit is contained in:
OxygenCobalt 2021-01-12 16:15:46 -07:00
parent 2cfe0211a5
commit eab260a9c1
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
5 changed files with 128 additions and 22 deletions

View file

@ -1,6 +1,7 @@
package org.oxycblt.auxio.recycler
import androidx.annotation.DrawableRes
import androidx.annotation.IdRes
import org.oxycblt.auxio.R
/**
@ -14,19 +15,44 @@ enum class DisplayMode(@DrawableRes val iconRes: Int) {
SHOW_ALBUMS(R.drawable.ic_album),
SHOW_SONGS(R.drawable.ic_song);
fun isAllOr(value: DisplayMode) = this == SHOW_ALL || this == value
@IdRes
fun toId(): Int {
return when (this) {
SHOW_ALL -> R.id.option_filter_all
SHOW_GENRES -> R.id.option_filter_genres
SHOW_ARTISTS -> R.id.option_filter_artists
SHOW_ALBUMS -> R.id.option_filter_albums
SHOW_SONGS -> R.id.option_filter_songs
}
}
companion object {
/**
* A valueOf wrapper that will return a default value if given a null/invalid string.
*/
fun valueOfOrFallback(value: String?): DisplayMode {
fun valueOfOrFallback(value: String?, fallback: DisplayMode = SHOW_ARTISTS): DisplayMode {
if (value == null) {
return SHOW_ARTISTS
return fallback
}
return try {
valueOf(value)
} catch (e: IllegalArgumentException) {
SHOW_ARTISTS
fallback
}
}
fun fromId(@IdRes id: Int): DisplayMode {
return when (id) {
R.id.option_filter_all -> SHOW_ALL
R.id.option_filter_songs -> SHOW_SONGS
R.id.option_filter_albums -> SHOW_ALBUMS
R.id.option_filter_artists -> SHOW_ARTISTS
R.id.option_filter_genres -> SHOW_GENRES
else -> SHOW_ALL
}
}
}

View file

@ -31,9 +31,12 @@ import org.oxycblt.auxio.ui.isLandscape
import org.oxycblt.auxio.ui.requireCompatActivity
import org.oxycblt.auxio.ui.toColor
// TODO: Fix TextView memory leak
// TODO: Add Filtering
// TODO: Add "No Results" marker
/**
* A [Fragment] that allows for the searching of the entire music library.
* TODO: Add "Recent Searches" & No Results indicator
* TODO: Filtering
* @author OxygenCobalt
*/
class SearchFragment : Fragment() {
// SearchViewModel only scoped to this Fragment
private val searchModel: SearchViewModel by viewModels()
@ -56,6 +59,19 @@ class SearchFragment : Fragment() {
// --- UI SETUP --
binding.searchToolbar.apply {
menu.findItem(searchModel.filterMode.toId()).isChecked = true
setOnMenuItemClickListener {
if (it.itemId != R.id.submenu_filtering) {
it.isChecked = true
searchModel.updateFilterModeWithId(it.itemId, requireContext())
true
} else false
}
}
binding.searchTextLayout.apply {
boxStrokeColor = accent
hintTextColor = ColorStateList.valueOf(accent)
@ -113,7 +129,7 @@ class SearchFragment : Fragment() {
invoke(this@SearchFragment, null)
}
} catch (e: Exception) {
logE("Hacky reflection leak fix failed. Oh well.")
logE("Hacky reflection leak fix failed.")
e.printStackTrace()
}

View file

@ -1,26 +1,49 @@
package org.oxycblt.auxio.search
import android.content.Context
import androidx.annotation.IdRes
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
import org.oxycblt.auxio.R
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.MusicStore
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.recycler.DisplayMode
import org.oxycblt.auxio.settings.SettingsManager
/**
* The [ViewModel] for the search functionality
* @author OxygenCobalt
*/
class SearchViewModel : ViewModel() {
private val mSearchResults = MutableLiveData(listOf<BaseModel>())
val searchResults: LiveData<List<BaseModel>> get() = mSearchResults
private var mFilterMode = DisplayMode.SHOW_ALL
val filterMode: DisplayMode get() = mFilterMode
private var mLastQuery = ""
private var mIsNavigating = false
val isNavigating: Boolean get() = mIsNavigating
private val musicStore = MusicStore.getInstance()
private val settingsManager = SettingsManager.getInstance()
init {
mFilterMode = settingsManager.searchFilterMode
}
fun doSearch(query: String, context: Context) {
mLastQuery = query
if (query.isEmpty()) {
mSearchResults.value = listOf()
@ -30,36 +53,62 @@ class SearchViewModel : ViewModel() {
viewModelScope.launch {
val results = mutableListOf<BaseModel>()
musicStore.artists.filterByOrNull(query)?.let {
results.add(Header(id = -1, name = context.getString(R.string.label_artists)))
results.addAll(it)
if (mFilterMode.isAllOr(DisplayMode.SHOW_ARTISTS)) {
musicStore.artists.filterByOrNull(query)?.let {
results.add(Header(id = -1, name = context.getString(R.string.label_artists)))
results.addAll(it)
}
}
musicStore.albums.filterByOrNull(query)?.let {
results.add(Header(id = -2, name = context.getString(R.string.label_albums)))
results.addAll(it)
if (mFilterMode.isAllOr(DisplayMode.SHOW_ALBUMS)) {
musicStore.albums.filterByOrNull(query)?.let {
results.add(Header(id = -2, name = context.getString(R.string.label_albums)))
results.addAll(it)
}
}
musicStore.genres.filterByOrNull(query)?.let {
results.add(Header(id = -3, name = context.getString(R.string.label_genres)))
results.addAll(it)
if (mFilterMode.isAllOr(DisplayMode.SHOW_GENRES)) {
musicStore.genres.filterByOrNull(query)?.let {
results.add(Header(id = -3, name = context.getString(R.string.label_genres)))
results.addAll(it)
}
}
musicStore.songs.filterByOrNull(query)?.let {
results.add(Header(id = -4, name = context.getString(R.string.label_songs)))
results.addAll(it)
if (mFilterMode.isAllOr(DisplayMode.SHOW_SONGS)) {
musicStore.songs.filterByOrNull(query)?.let {
results.add(Header(id = -4, name = context.getString(R.string.label_songs)))
results.addAll(it)
}
}
mSearchResults.value = results
}
}
fun updateFilterModeWithId(@IdRes id: Int, context: Context) {
mFilterMode = DisplayMode.fromId(id)
settingsManager.searchFilterMode = mFilterMode
doSearch(mLastQuery, context)
}
private fun List<BaseModel>.filterByOrNull(value: String): List<BaseModel>? {
val filtered = filter { it.name.contains(value, ignoreCase = true) }
return if (filtered.isNotEmpty()) filtered else null
}
private fun List<BaseModel>.filterByDisplayMode(mode: DisplayMode): List<BaseModel> {
return when (mode) {
DisplayMode.SHOW_ALL -> this
DisplayMode.SHOW_SONGS -> filterIsInstance<Song>()
DisplayMode.SHOW_ALBUMS -> filterIsInstance<Album>()
DisplayMode.SHOW_ARTISTS -> filterIsInstance<Artist>()
DisplayMode.SHOW_GENRES -> filterIsInstance<Genre>()
}
}
/**
* Update the current navigation status
* @param value Whether LibraryFragment is navigating or not

View file

@ -144,6 +144,22 @@ class SettingsManager private constructor(context: Context) :
.apply()
}
/**
* The current filter mode of the search tab
*/
var searchFilterMode: DisplayMode
get() = DisplayMode.valueOfOrFallback(
sharedPrefs.getString(
Keys.KEY_SEARCH_FILTER_MODE, DisplayMode.SHOW_ALL.toString()
),
fallback = DisplayMode.SHOW_ALL
)
set(value) {
sharedPrefs.edit()
.putString(Keys.KEY_SEARCH_FILTER_MODE, value.toString())
.apply()
}
// --- CALLBACKS ---
private val callbacks = mutableListOf<Callback>()
@ -231,7 +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_SEARCH_FILTER_MODE = "KEY_SEARCH"
const val KEY_DEBUG_SAVE = "KEY_SAVE_STATE"
}

View file

@ -114,8 +114,7 @@
<string name="error_no_browser">Could not open link.</string>
<!-- Hint Namespace | EditText Hints -->
<string name="hint_search_library">Search Library…</string>
<string name="hint_search_songs">Search Songs…</string>
<string name="hint_search_library">Search your library…</string>
<!-- Description Namespace | Accessibility Strings -->
<string name="description_album_cover">Album Cover for %s</string>