diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f5da9922..da542f0e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ at the cost of longer loading times - Added support for sort tags [#172, dependent on this feature] - Added support for date tags, including more fine-grained dates [#159, dependent on this feature] - Added Last Added sorting +- Search now takes sort tags and file names in account [#184] ## 2.5.0 diff --git a/app/src/main/java/org/oxycblt/auxio/music/Music.kt b/app/src/main/java/org/oxycblt/auxio/music/Music.kt index 0e2047b99..d2aba0d6a 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Music.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Music.kt @@ -292,18 +292,18 @@ data class Genre(override val rawName: String?, override val songs: List) /** * An ISO-8601/RFC 3339 Date. * - * Unlike a typical Date within the standard library, this class is simply a 1:1 mapping between - * the tag date format of ID3v2 and (presumably) the Vorbis date format, implementing only format + * Unlike a typical Date within the standard library, this class is simply a 1:1 mapping between the + * tag date format of ID3v2 and (presumably) the Vorbis date format, implementing only format * validation and excluding advanced or locale-specific date functionality.. * - * The reasoning behind Date is that Auxio cannot trust any kind of metadata date to actually - * make sense in a calendar, due to bad tagging, locale-specific issues, or simply from the - * limited nature of tag formats. Thus, it's better to use an analogous data structure that - * will not mangle or reject valid-ish dates. - * - * Date instances are immutable and their internal implementation is hidden. To instantiate one, - * use [fromYear] or [parseTimestamp]. The string representation of a Date is RFC 3339, with - * granular position depending on the presence of particular tokens. + * The reasoning behind Date is that Auxio cannot trust any kind of metadata date to actually make + * sense in a calendar, due to bad tagging, locale-specific issues, or simply from the limited + * nature of tag formats. Thus, it's better to use an analogous data structure that will not mangle + * or reject valid-ish dates. + * + * Date instances are immutable and their internal implementation is hidden. To instantiate one, use + * [fromYear] or [parseTimestamp]. The string representation of a Date is RFC 3339, with granular + * position depending on the presence of particular tokens. * * Please, *Do not use this for anything important related to time.* I cannot stress this enough. * This class will blow up if you try to do that. diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt index cac9c9435..5de47cfb0 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt @@ -29,12 +29,15 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import org.oxycblt.auxio.R import org.oxycblt.auxio.music.Music +import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicStore +import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.ui.DisplayMode import org.oxycblt.auxio.ui.Sort import org.oxycblt.auxio.ui.recycler.Header import org.oxycblt.auxio.ui.recycler.Item +import org.oxycblt.auxio.util.TaskGuard import org.oxycblt.auxio.util.application import org.oxycblt.auxio.util.logD @@ -56,11 +59,13 @@ class SearchViewModel(application: Application) : get() = settings.searchFilterMode private var lastQuery: String? = null + private var guard = TaskGuard() /** * Use [query] to perform a search of the music library. Will push results to [searchResults]. */ fun search(query: String?) { + val handle = guard.newHandle() lastQuery = query val library = musicStore.library @@ -80,33 +85,34 @@ class SearchViewModel(application: Application) : // Note: a filter mode of null means to not filter at all. if (filterMode == null || filterMode == DisplayMode.SHOW_ARTISTS) { - library.artists.filterByOrNull(query)?.let { artists -> + library.artists.filterParentsBy(query)?.let { artists -> results.add(Header(-1, R.string.lbl_artists)) results.addAll(sort.artists(artists)) } } if (filterMode == null || filterMode == DisplayMode.SHOW_ALBUMS) { - library.albums.filterByOrNull(query)?.let { albums -> + library.albums.filterParentsBy(query)?.let { albums -> results.add(Header(-2, R.string.lbl_albums)) results.addAll(sort.albums(albums)) } } if (filterMode == null || filterMode == DisplayMode.SHOW_GENRES) { - library.genres.filterByOrNull(query)?.let { genres -> + library.genres.filterParentsBy(query)?.let { genres -> results.add(Header(-3, R.string.lbl_genres)) results.addAll(sort.genres(genres)) } } if (filterMode == null || filterMode == DisplayMode.SHOW_SONGS) { - library.songs.filterByOrNull(query)?.let { songs -> + library.songs.filterSongsBy(query)?.let { songs -> results.add(Header(-4, R.string.lbl_songs)) results.addAll(sort.songs(songs)) } } + guard.yield(handle) _searchResults.value = results } } @@ -131,20 +137,24 @@ class SearchViewModel(application: Application) : search(lastQuery) } - /** - * Shortcut that will run a ignoreCase filter on a list and only return a value if the resulting - * list is empty. - */ - private fun List.filterByOrNull(value: String): List? { - val filtered = filter { - // Compare normalized names, which are names with unicode characters that are - // normalized to their non-unicode forms. This is just for quality-of-life, - // and I hope it doesn't bork search functionality for other languages. - it.resolveNameNormalized(application).contains(value, ignoreCase = true) || - it.resolveNameNormalized(application).contains(value, ignoreCase = true) - } + /** Searches the song list by the normalized name, then the sort name, and then the file name. */ + private fun List.filterSongsBy(value: String) = + baseFilterBy(value) { it.path.name.contains(value) }.ifEmpty { null } - return filtered.ifEmpty { null } + /** Searches a list of parents by the normalized name, and then the sort name. */ + private fun List.filterParentsBy(value: String) = + baseFilterBy(value) { false }.ifEmpty { null } + + private inline fun List.baseFilterBy( + value: String, + additional: (T) -> Boolean + ) = filter { + // The basic comparison is first by the *normalized* name, as that allows a non-unicode + // search to match with some unicode characters. If that fails, we if there is a sort name + // we can leverage, as those are often used to make non-unicode variants of unicode titles. + it.resolveNameNormalized(application).contains(value, ignoreCase = true) || + it.rawSortName?.contains(value, ignoreCase = true) == true || + additional(it) } private fun Music.resolveNameNormalized(context: Context): String {