Add genre support to search

Add the ability for genres to show up in search, as long as the library is currently displaying them.
This commit is contained in:
OxygenCobalt 2020-10-04 10:21:48 -06:00
parent 127f700700
commit 711d9a0b00
15 changed files with 50 additions and 27 deletions

View file

@ -34,7 +34,7 @@ TODOs surrounded with !s are things I tried to do, but failed for reasons includ
- Fix issue where fast navigations will cause the app to not display anything - Fix issue where fast navigations will cause the app to not display anything
/other/ /other/
- Collapse show constants into ShowMode enum - Standardize fragment init
To be added: To be added:
/prefs/ /prefs/

View file

@ -6,6 +6,7 @@ import android.view.LayoutInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.forEach import androidx.core.view.forEach
@ -58,8 +59,9 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
val item = findItem(R.id.action_search) val item = findItem(R.id.action_search)
val searchView = item.actionView as SearchView val searchView = item.actionView as SearchView
// Set up the SearchView itself searchView.findViewById<ImageView>(androidx.appcompat.R.id.search_go_btn)
searchView.queryHint = getString(R.string.hint_search_library) .setImageResource(R.drawable.ic_back)
searchView.setOnQueryTextListener(this@LibraryFragment) searchView.setOnQueryTextListener(this@LibraryFragment)
searchView.setOnQueryTextFocusChangeListener { _, hasFocus -> searchView.setOnQueryTextFocusChangeListener { _, hasFocus ->
libraryModel.updateSearchFocusStatus(hasFocus) libraryModel.updateSearchFocusStatus(hasFocus)
@ -139,7 +141,9 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
libraryModel.searchResults.observe(viewLifecycleOwner) { libraryModel.searchResults.observe(viewLifecycleOwner) {
if (libraryModel.searchHasFocus) { if (libraryModel.searchHasFocus) {
searchAdapter.submitList(it) searchAdapter.submitList(it) {
binding.libraryRecycler.scrollToPosition(0)
}
} }
} }

View file

@ -45,6 +45,7 @@ class LibraryViewModel : ViewModel() {
} }
fun updateSearchQuery(query: String, musicModel: MusicViewModel) { fun updateSearchQuery(query: String, musicModel: MusicViewModel) {
// Don't bother if the query is blank.
if (query == "") { if (query == "") {
resetQuery() resetQuery()
@ -52,18 +53,32 @@ class LibraryViewModel : ViewModel() {
} }
// Search MusicViewModel for all the items [Artists, Albums, Songs] that contain // Search MusicViewModel for all the items [Artists, Albums, Songs] that contain
// the query, and update the LiveData with those items. This is done on a seperate // the query, and update the LiveData with those items. This is done on a separate
// thread as it can be a very intensive operation for large music libraries. // thread as it can be a very long operation for large music libraries.
viewModelScope.launch { viewModelScope.launch {
val combined = mutableListOf<BaseModel>() val combined = mutableListOf<BaseModel>()
val children = showMode.value!!.getChildren()
val artists = musicModel.artists.value!!.filter { it.name.contains(query, true) } // If the Library ShowMode supports it, include artists / genres in the search.
if (children.contains(ShowMode.SHOW_GENRES)) {
val genres = musicModel.genres.value!!.filter { it.name.contains(query, true) }
if (artists.isNotEmpty()) { if (genres.isNotEmpty()) {
combined.add(Header(id = ShowMode.SHOW_ARTISTS.constant)) combined.add(Header(id = ShowMode.SHOW_GENRES.constant))
combined.addAll(artists) combined.addAll(genres)
}
} }
if (children.contains(ShowMode.SHOW_ARTISTS)) {
val artists = musicModel.artists.value!!.filter { it.name.contains(query, true) }
if (artists.isNotEmpty()) {
combined.add(Header(id = ShowMode.SHOW_ARTISTS.constant))
combined.addAll(artists)
}
}
// Albums & Songs are always included.
val albums = musicModel.albums.value!!.filter { it.name.contains(query, true) } val albums = musicModel.albums.value!!.filter { it.name.contains(query, true) }
if (albums.isNotEmpty()) { if (albums.isNotEmpty()) {

View file

@ -97,7 +97,7 @@ fun TextView.bindGenreCounts(genre: Genre) {
R.plurals.format_artist_count, genre.numArtists, genre.numArtists R.plurals.format_artist_count, genre.numArtists, genre.numArtists
) )
val albums = context.resources.getQuantityString( val albums = context.resources.getQuantityString(
R.plurals.format_song_count, genre.numAlbums, genre.numAlbums R.plurals.format_album_count, genre.numAlbums, genre.numAlbums
) )
text = context.getString(R.string.format_double_counts, artists, albums) text = context.getString(R.string.format_double_counts, artists, albums)

View file

@ -1,5 +1,13 @@
package org.oxycblt.auxio.recycler package org.oxycblt.auxio.recycler
enum class ShowMode(val constant: Long) { enum class ShowMode(val constant: Long) {
SHOW_GENRES(10), SHOW_ARTISTS(11), SHOW_ALBUMS(12), SHOW_SONGS(13); SHOW_GENRES(0), SHOW_ARTISTS(1), SHOW_ALBUMS(2), SHOW_SONGS(3);
// Make a slice of all the values that this ShowMode covers.
// ex. SHOW_ARTISTS would return SHOW_ARTISTS, SHOW_ALBUMS, and SHOW_SONGS
fun getChildren(): List<ShowMode> {
val vals = values()
return vals.slice(vals.indexOf(this) until vals.size)
}
} }

View file

@ -14,8 +14,8 @@ import org.oxycblt.auxio.music.Header
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.recycler.ClickListener import org.oxycblt.auxio.recycler.ClickListener
// Basic ViewHolders for each music model. // Shared ViewHolders for each ViewModel, providing basic information
// All new instances should be created with from(context, listener) instead of direct instantiation.
class GenreViewHolder private constructor( class GenreViewHolder private constructor(
listener: ClickListener<Genre>, listener: ClickListener<Genre>,
private val binding: ItemGenreBinding private val binding: ItemGenreBinding

View file

@ -65,6 +65,7 @@ fun Int.toColor(context: Context): Int {
} }
} }
// Resolve an attribute into a color
@ColorInt @ColorInt
fun resolveAttr(context: Context, @AttrRes attr: Int): Int { fun resolveAttr(context: Context, @AttrRes attr: Int): Int {
// Convert the attribute into its color // Convert the attribute into its color
@ -82,6 +83,7 @@ fun resolveAttr(context: Context, @AttrRes attr: Int): Int {
return color.toColor(context) return color.toColor(context)
} }
// Apply a color to a Menu Item
fun MenuItem.applyColor(color: Int) { fun MenuItem.applyColor(color: Int) {
SpannableString(title).apply { SpannableString(title).apply {
setSpan(ForegroundColorSpan(color), 0, length, SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE) setSpan(ForegroundColorSpan(color), 0, length, SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE)

View file

@ -3,7 +3,7 @@
android:height="24dp" android:height="24dp"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24" android:viewportHeight="24"
android:tint="?android:attr/colorControlNormal"> android:tint="@color/control_color">
<path <path
android:fillColor="@android:color/white" android:fillColor="@android:color/white"
android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z" /> android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z" />

View file

@ -4,7 +4,7 @@
android:height="24dp" android:height="24dp"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24" android:viewportHeight="24"
android:tint="?attr/colorControlNormal"> android:tint="@color/control_color">
<path <path
android:fillColor="@android:color/white" android:fillColor="@android:color/white"
android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z" /> android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z" />

View file

@ -4,7 +4,7 @@
android:height="24dp" android:height="24dp"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24" android:viewportHeight="24"
android:tint="?android:attr/colorControlNormal"> android:tint="@color/control_color">
<path <path
android:fillColor="@android:color/white" android:fillColor="@android:color/white"
android:pathData="M3,18h6v-2L3,16v2zM3,6v2h18L21,6L3,6zM3,13h12v-2L3,11v2z" /> android:pathData="M3,18h6v-2L3,16v2zM3,6v2h18L21,6L3,6zM3,13h12v-2L3,11v2z" />

View file

@ -4,6 +4,7 @@
<color name="divider_color">#323232</color> <color name="divider_color">#323232</color>
<color name="selection_color">#484848</color> <color name="selection_color">#484848</color>
<color name="inactive_color">#404040</color> <color name="inactive_color">#404040</color>
<color name="control_color">#ffffff</color>
<!-- <!--
Base color set derived from Music Player GO. Base color set derived from Music Player GO.

View file

@ -4,6 +4,7 @@
<color name="divider_color">#cbcbcb</color> <color name="divider_color">#cbcbcb</color>
<color name="selection_color">#cbcbcb</color> <color name="selection_color">#cbcbcb</color>
<color name="inactive_color">#c4c4c4</color> <color name="inactive_color">#c4c4c4</color>
<color name="control_color">#202020</color>
<!-- <!--
Base color set derived from Music Player GO. Base color set derived from Music Player GO.

View file

@ -43,12 +43,11 @@
<string name="placeholder_album">Unknown Album</string> <string name="placeholder_album">Unknown Album</string>
<string name="placeholder_no_date">No Date</string> <string name="placeholder_no_date">No Date</string>
<!-- Format Namespace | Value formatting --> <!-- Format Namespace | Value formatting/plurals -->
<string name="format_info">%1$s / %2$s</string> <string name="format_info">%1$s / %2$s</string>
<string name="format_double_info">%1$s / %2$s / %3$s</string> <string name="format_double_info">%1$s / %2$s / %3$s</string>
<string name="format_double_counts">%1$s, %2$s</string> <string name="format_double_counts">%1$s, %2$s</string>
<!-- Format Namespace | Value formatting with plurals -->
<plurals name="format_song_count"> <plurals name="format_song_count">
<item quantity="one">%s Song</item> <item quantity="one">%s Song</item>
<item quantity="other">%s Songs</item> <item quantity="other">%s Songs</item>

View file

@ -18,10 +18,7 @@
<item name="android:textColor">?android:attr/colorPrimary</item> <item name="android:textColor">?android:attr/colorPrimary</item>
</style> </style>
<style name="Toolbar.SearchViewStyle" parent="Widget.AppCompat.SearchView"> <style name="Toolbar.SearchViewStyle" parent="Widget.AppCompat.SearchView"/>
<item name="android:searchIcon">@drawable/ic_search</item>
<item name="android:closeIcon">@drawable/ic_back</item>
</style>
<style name="DetailHeader"> <style name="DetailHeader">
<item name="android:textAppearance">?android:attr/textAppearanceLarge</item> <item name="android:textAppearance">?android:attr/textAppearanceLarge</item>

View file

@ -38,25 +38,21 @@
<item name="colorSecondary">@color/light_blue</item> <item name="colorSecondary">@color/light_blue</item>
</style> </style>
<style name="Theme.Cyan" parent="Theme.Base"> <style name="Theme.Cyan" parent="Theme.Base">
<item name="colorPrimary">@color/cyan</item> <item name="colorPrimary">@color/cyan</item>
<item name="colorSecondary">@color/cyan</item> <item name="colorSecondary">@color/cyan</item>
</style> </style>
<style name="Theme.Teal" parent="Theme.Base"> <style name="Theme.Teal" parent="Theme.Base">
<item name="colorPrimary">@color/teal</item> <item name="colorPrimary">@color/teal</item>
<item name="colorSecondary">@color/teal</item> <item name="colorSecondary">@color/teal</item>
</style> </style>
<style name="Theme.Green" parent="Theme.Base"> <style name="Theme.Green" parent="Theme.Base">
<item name="colorPrimary">@color/green</item> <item name="colorPrimary">@color/green</item>
<item name="colorSecondary">@color/green</item> <item name="colorSecondary">@color/green</item>
</style> </style>
<style name="Theme.LightGreen" parent="Theme.Base"> <style name="Theme.LightGreen" parent="Theme.Base">
<item name="colorPrimary">@color/light_green</item> <item name="colorPrimary">@color/light_green</item>
<item name="colorSecondary">@color/light_green</item> <item name="colorSecondary">@color/light_green</item>