Improve search suggestions

This commit is contained in:
Koitharu
2023-08-19 15:06:42 +03:00
parent 856524c9f8
commit bc488a6878
10 changed files with 104 additions and 8 deletions

View File

@@ -73,6 +73,20 @@ class MangaSearchRepository @Inject constructor(
}.orEmpty()
}
suspend fun getQueryHintSuggestion(
query: String,
limit: Int,
): List<String> {
if (query.isEmpty()) {
return emptyList()
}
val titles = db.suggestionDao.getTitles("$query%")
if (titles.isEmpty()) {
return emptyList()
}
return titles.shuffled().take(limit)
}
suspend fun getTagsSuggestion(query: String, limit: Int, source: MangaSource?): List<MangaTag> {
return when {
query.isNotEmpty() && source != null -> db.tagsDao.findTags(source.name, "%$query%", limit)

View File

@@ -69,6 +69,11 @@ class SearchSuggestionFragment :
viewModel.deleteQuery(query)
}
override fun onResume() {
super.onResume()
viewModel.onResume()
}
companion object {
fun newInstance() = SearchSuggestionFragment()

View File

@@ -30,6 +30,7 @@ import javax.inject.Inject
private const val DEBOUNCE_TIMEOUT = 500L
private const val MAX_MANGA_ITEMS = 6
private const val MAX_QUERY_ITEMS = 16
private const val MAX_HINTS_ITEMS = 3
private const val MAX_TAGS_ITEMS = 8
private const val MAX_SOURCES_ITEMS = 6
@@ -42,6 +43,7 @@ class SearchSuggestionViewModel @Inject constructor(
private val query = MutableStateFlow("")
private var suggestionJob: Job? = null
private var invalidateOnResume = false
val isIncognitoModeEnabled = settings.observeAsStateFlow(
scope = viewModelScope + Dispatchers.Default,
@@ -60,11 +62,10 @@ class SearchSuggestionViewModel @Inject constructor(
}
fun saveQuery(query: String) {
launchJob(Dispatchers.Default) {
if (!settings.isIncognitoModeEnabled) {
repository.saveSearchQuery(query)
}
if (!settings.isIncognitoModeEnabled) {
repository.saveSearchQuery(query)
}
invalidateOnResume = true
}
fun clearSearchHistory() {
@@ -80,6 +81,13 @@ class SearchSuggestionViewModel @Inject constructor(
}
}
fun onResume() {
if (invalidateOnResume) {
invalidateOnResume = false
setupSuggestion()
}
}
fun deleteQuery(query: String) {
launchJob {
repository.deleteSearchQuery(query)
@@ -108,6 +116,9 @@ class SearchSuggestionViewModel @Inject constructor(
val queriesDeferred = async {
repository.getQuerySuggestion(searchQuery, MAX_QUERY_ITEMS)
}
val hintsDeferred = async {
repository.getQueryHintSuggestion(searchQuery, MAX_HINTS_ITEMS)
}
val tagsDeferred = async {
repository.getTagsSuggestion(searchQuery, MAX_TAGS_ITEMS, null)
}
@@ -119,16 +130,18 @@ class SearchSuggestionViewModel @Inject constructor(
val tags = tagsDeferred.await()
val mangaList = mangaDeferred.await()
val queries = queriesDeferred.await()
val hints = hintsDeferred.await()
buildList(queries.size + sources.size + 2) {
buildList(queries.size + sources.size + hints.size + 2) {
if (tags.isNotEmpty()) {
add(SearchSuggestionItem.Tags(mapTags(tags)))
}
if (mangaList.isNotEmpty()) {
add(SearchSuggestionItem.MangaList(mangaList))
}
queries.mapTo(this) { SearchSuggestionItem.RecentQuery(it) }
sources.mapTo(this) { SearchSuggestionItem.Source(it, it in enabledSources) }
queries.mapTo(this) { SearchSuggestionItem.RecentQuery(it) }
hints.mapTo(this) { SearchSuggestionItem.Hint(it) }
}
}

View File

@@ -20,5 +20,6 @@ class SearchSuggestionAdapter(
.addDelegate(searchSuggestionSourceAD(coil, lifecycleOwner, listener))
.addDelegate(searchSuggestionTagsAD(listener))
.addDelegate(searchSuggestionMangaListAD(coil, lifecycleOwner, listener))
.addDelegate(searchSuggestionQueryHintAD(listener))
}
}

View File

@@ -0,0 +1,24 @@
package org.koitharu.kotatsu.search.ui.suggestion.adapter
import android.view.View
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.databinding.ItemSearchSuggestionQueryHintBinding
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener
import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem
fun searchSuggestionQueryHintAD(
listener: SearchSuggestionListener,
) = adapterDelegateViewBinding<SearchSuggestionItem.Hint, SearchSuggestionItem, ItemSearchSuggestionQueryHintBinding>(
{ inflater, parent -> ItemSearchSuggestionQueryHintBinding.inflate(inflater, parent, false) },
) {
val viewClickListener = View.OnClickListener { v ->
listener.onQueryClick(item.query, true)
}
binding.root.setOnClickListener(viewClickListener)
bind {
binding.root.text = item.query
}
}

View File

@@ -27,6 +27,15 @@ sealed interface SearchSuggestionItem : ListModel {
}
}
data class Hint(
val query: String,
) : SearchSuggestionItem {
override fun areItemsTheSame(other: ListModel): Boolean {
return other is Hint && query == other.query
}
}
data class Source(
val source: MangaSource,
val isEnabled: Boolean,

View File

@@ -26,6 +26,9 @@ abstract class SuggestionDao {
@Query("SELECT COUNT(*) FROM suggestions")
abstract suspend fun count(): Int
@Query("SELECT manga.title FROM suggestions LEFT JOIN manga ON suggestions.manga_id = manga.manga_id WHERE manga.title LIKE :query")
abstract suspend fun getTitles(query: String): List<String>
@Insert(onConflict = OnConflictStrategy.IGNORE)
abstract suspend fun insert(entity: SuggestionEntity): Long

View File

@@ -8,6 +8,7 @@
android:background="?selectableItemBackground"
android:gravity="center_vertical"
android:minHeight="?listPreferredItemHeightSmall"
android:orientation="horizontal"
android:paddingStart="?listPreferredItemPaddingStart"
android:paddingEnd="?listPreferredItemPaddingEnd">
@@ -24,6 +25,12 @@
app:drawableStartCompat="@drawable/ic_history"
tools:text="@tools:sample/lorem[6]" />
<com.google.android.material.divider.MaterialDivider
android:layout_width="1dp"
android:layout_height="match_parent"
android:layout_marginVertical="6dp"
android:layout_marginHorizontal="4dp" />
<ImageButton
android:id="@+id/button_complete"
android:layout_width="wrap_content"
@@ -33,4 +40,4 @@
android:src="@drawable/abc_ic_commit_search_api_mtrl_alpha"
app:tint="?colorControlNormal" />
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:drawablePadding="12dp"
android:ellipsize="end"
android:gravity="center_vertical"
android:minHeight="?listPreferredItemHeightSmall"
android:orientation="horizontal"
android:paddingStart="?listPreferredItemPaddingStart"
android:paddingEnd="?listPreferredItemPaddingEnd"
android:singleLine="true"
android:textAppearance="?attr/textAppearanceBodyLarge"
app:drawableStartCompat="@drawable/ic_suggestion"
tools:text="@tools:sample/lorem[6]" />

View File

@@ -4,8 +4,9 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingVertical="4dp"
android:paddingStart="?listPreferredItemPaddingStart"
android:paddingTop="4dp"
android:paddingEnd="?listPreferredItemPaddingEnd"
android:paddingBottom="6dp"
app:chipSpacingHorizontal="6dp"
app:chipSpacingVertical="6dp" />