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
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue