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 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
/** /**
@ -14,19 +15,44 @@ enum class DisplayMode(@DrawableRes val iconRes: Int) {
SHOW_ALBUMS(R.drawable.ic_album), SHOW_ALBUMS(R.drawable.ic_album),
SHOW_SONGS(R.drawable.ic_song); 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 { companion object {
/** /**
* A valueOf wrapper that will return a default value if given a null/invalid string. * 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) { if (value == null) {
return SHOW_ARTISTS return fallback
} }
return try { return try {
valueOf(value) valueOf(value)
} catch (e: IllegalArgumentException) { } 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.requireCompatActivity
import org.oxycblt.auxio.ui.toColor import org.oxycblt.auxio.ui.toColor
// TODO: Fix TextView memory leak /**
// TODO: Add Filtering * A [Fragment] that allows for the searching of the entire music library.
// TODO: Add "No Results" marker * TODO: Add "Recent Searches" & No Results indicator
* TODO: Filtering
* @author OxygenCobalt
*/
class SearchFragment : Fragment() { class SearchFragment : Fragment() {
// SearchViewModel only scoped to this Fragment // SearchViewModel only scoped to this Fragment
private val searchModel: SearchViewModel by viewModels() private val searchModel: SearchViewModel by viewModels()
@ -56,6 +59,19 @@ class SearchFragment : Fragment() {
// --- UI SETUP -- // --- 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 { binding.searchTextLayout.apply {
boxStrokeColor = accent boxStrokeColor = accent
hintTextColor = ColorStateList.valueOf(accent) hintTextColor = ColorStateList.valueOf(accent)
@ -113,7 +129,7 @@ class SearchFragment : Fragment() {
invoke(this@SearchFragment, null) invoke(this@SearchFragment, null)
} }
} catch (e: Exception) { } catch (e: Exception) {
logE("Hacky reflection leak fix failed. Oh well.") logE("Hacky reflection leak fix failed.")
e.printStackTrace() e.printStackTrace()
} }

View file

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

View file

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

View file

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