Add filtering to SearchFragment
Add the ability to filter items to SearchFragment
This commit is contained in:
parent
2cfe0211a5
commit
eab260a9c1
5 changed files with 128 additions and 22 deletions
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue