Improve search suggestions
This commit is contained in:
@@ -73,6 +73,20 @@ class MangaSearchRepository @Inject constructor(
|
|||||||
}.orEmpty()
|
}.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> {
|
suspend fun getTagsSuggestion(query: String, limit: Int, source: MangaSource?): List<MangaTag> {
|
||||||
return when {
|
return when {
|
||||||
query.isNotEmpty() && source != null -> db.tagsDao.findTags(source.name, "%$query%", limit)
|
query.isNotEmpty() && source != null -> db.tagsDao.findTags(source.name, "%$query%", limit)
|
||||||
|
|||||||
@@ -69,6 +69,11 @@ class SearchSuggestionFragment :
|
|||||||
viewModel.deleteQuery(query)
|
viewModel.deleteQuery(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
viewModel.onResume()
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
fun newInstance() = SearchSuggestionFragment()
|
fun newInstance() = SearchSuggestionFragment()
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import javax.inject.Inject
|
|||||||
private const val DEBOUNCE_TIMEOUT = 500L
|
private const val DEBOUNCE_TIMEOUT = 500L
|
||||||
private const val MAX_MANGA_ITEMS = 6
|
private const val MAX_MANGA_ITEMS = 6
|
||||||
private const val MAX_QUERY_ITEMS = 16
|
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_TAGS_ITEMS = 8
|
||||||
private const val MAX_SOURCES_ITEMS = 6
|
private const val MAX_SOURCES_ITEMS = 6
|
||||||
|
|
||||||
@@ -42,6 +43,7 @@ class SearchSuggestionViewModel @Inject constructor(
|
|||||||
|
|
||||||
private val query = MutableStateFlow("")
|
private val query = MutableStateFlow("")
|
||||||
private var suggestionJob: Job? = null
|
private var suggestionJob: Job? = null
|
||||||
|
private var invalidateOnResume = false
|
||||||
|
|
||||||
val isIncognitoModeEnabled = settings.observeAsStateFlow(
|
val isIncognitoModeEnabled = settings.observeAsStateFlow(
|
||||||
scope = viewModelScope + Dispatchers.Default,
|
scope = viewModelScope + Dispatchers.Default,
|
||||||
@@ -60,11 +62,10 @@ class SearchSuggestionViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun saveQuery(query: String) {
|
fun saveQuery(query: String) {
|
||||||
launchJob(Dispatchers.Default) {
|
if (!settings.isIncognitoModeEnabled) {
|
||||||
if (!settings.isIncognitoModeEnabled) {
|
repository.saveSearchQuery(query)
|
||||||
repository.saveSearchQuery(query)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
invalidateOnResume = true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearSearchHistory() {
|
fun clearSearchHistory() {
|
||||||
@@ -80,6 +81,13 @@ class SearchSuggestionViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onResume() {
|
||||||
|
if (invalidateOnResume) {
|
||||||
|
invalidateOnResume = false
|
||||||
|
setupSuggestion()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun deleteQuery(query: String) {
|
fun deleteQuery(query: String) {
|
||||||
launchJob {
|
launchJob {
|
||||||
repository.deleteSearchQuery(query)
|
repository.deleteSearchQuery(query)
|
||||||
@@ -108,6 +116,9 @@ class SearchSuggestionViewModel @Inject constructor(
|
|||||||
val queriesDeferred = async {
|
val queriesDeferred = async {
|
||||||
repository.getQuerySuggestion(searchQuery, MAX_QUERY_ITEMS)
|
repository.getQuerySuggestion(searchQuery, MAX_QUERY_ITEMS)
|
||||||
}
|
}
|
||||||
|
val hintsDeferred = async {
|
||||||
|
repository.getQueryHintSuggestion(searchQuery, MAX_HINTS_ITEMS)
|
||||||
|
}
|
||||||
val tagsDeferred = async {
|
val tagsDeferred = async {
|
||||||
repository.getTagsSuggestion(searchQuery, MAX_TAGS_ITEMS, null)
|
repository.getTagsSuggestion(searchQuery, MAX_TAGS_ITEMS, null)
|
||||||
}
|
}
|
||||||
@@ -119,16 +130,18 @@ class SearchSuggestionViewModel @Inject constructor(
|
|||||||
val tags = tagsDeferred.await()
|
val tags = tagsDeferred.await()
|
||||||
val mangaList = mangaDeferred.await()
|
val mangaList = mangaDeferred.await()
|
||||||
val queries = queriesDeferred.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()) {
|
if (tags.isNotEmpty()) {
|
||||||
add(SearchSuggestionItem.Tags(mapTags(tags)))
|
add(SearchSuggestionItem.Tags(mapTags(tags)))
|
||||||
}
|
}
|
||||||
if (mangaList.isNotEmpty()) {
|
if (mangaList.isNotEmpty()) {
|
||||||
add(SearchSuggestionItem.MangaList(mangaList))
|
add(SearchSuggestionItem.MangaList(mangaList))
|
||||||
}
|
}
|
||||||
queries.mapTo(this) { SearchSuggestionItem.RecentQuery(it) }
|
|
||||||
sources.mapTo(this) { SearchSuggestionItem.Source(it, it in enabledSources) }
|
sources.mapTo(this) { SearchSuggestionItem.Source(it, it in enabledSources) }
|
||||||
|
queries.mapTo(this) { SearchSuggestionItem.RecentQuery(it) }
|
||||||
|
hints.mapTo(this) { SearchSuggestionItem.Hint(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,5 +20,6 @@ class SearchSuggestionAdapter(
|
|||||||
.addDelegate(searchSuggestionSourceAD(coil, lifecycleOwner, listener))
|
.addDelegate(searchSuggestionSourceAD(coil, lifecycleOwner, listener))
|
||||||
.addDelegate(searchSuggestionTagsAD(listener))
|
.addDelegate(searchSuggestionTagsAD(listener))
|
||||||
.addDelegate(searchSuggestionMangaListAD(coil, lifecycleOwner, listener))
|
.addDelegate(searchSuggestionMangaListAD(coil, lifecycleOwner, listener))
|
||||||
|
.addDelegate(searchSuggestionQueryHintAD(listener))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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(
|
data class Source(
|
||||||
val source: MangaSource,
|
val source: MangaSource,
|
||||||
val isEnabled: Boolean,
|
val isEnabled: Boolean,
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ abstract class SuggestionDao {
|
|||||||
@Query("SELECT COUNT(*) FROM suggestions")
|
@Query("SELECT COUNT(*) FROM suggestions")
|
||||||
abstract suspend fun count(): Int
|
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)
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
abstract suspend fun insert(entity: SuggestionEntity): Long
|
abstract suspend fun insert(entity: SuggestionEntity): Long
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
android:background="?selectableItemBackground"
|
android:background="?selectableItemBackground"
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:minHeight="?listPreferredItemHeightSmall"
|
android:minHeight="?listPreferredItemHeightSmall"
|
||||||
|
android:orientation="horizontal"
|
||||||
android:paddingStart="?listPreferredItemPaddingStart"
|
android:paddingStart="?listPreferredItemPaddingStart"
|
||||||
android:paddingEnd="?listPreferredItemPaddingEnd">
|
android:paddingEnd="?listPreferredItemPaddingEnd">
|
||||||
|
|
||||||
@@ -24,6 +25,12 @@
|
|||||||
app:drawableStartCompat="@drawable/ic_history"
|
app:drawableStartCompat="@drawable/ic_history"
|
||||||
tools:text="@tools:sample/lorem[6]" />
|
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
|
<ImageButton
|
||||||
android:id="@+id/button_complete"
|
android:id="@+id/button_complete"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
@@ -33,4 +40,4 @@
|
|||||||
android:src="@drawable/abc_ic_commit_search_api_mtrl_alpha"
|
android:src="@drawable/abc_ic_commit_search_api_mtrl_alpha"
|
||||||
app:tint="?colorControlNormal" />
|
app:tint="?colorControlNormal" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|||||||
@@ -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]" />
|
||||||
@@ -4,8 +4,9 @@
|
|||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:paddingVertical="4dp"
|
|
||||||
android:paddingStart="?listPreferredItemPaddingStart"
|
android:paddingStart="?listPreferredItemPaddingStart"
|
||||||
|
android:paddingTop="4dp"
|
||||||
android:paddingEnd="?listPreferredItemPaddingEnd"
|
android:paddingEnd="?listPreferredItemPaddingEnd"
|
||||||
|
android:paddingBottom="6dp"
|
||||||
app:chipSpacingHorizontal="6dp"
|
app:chipSpacingHorizontal="6dp"
|
||||||
app:chipSpacingVertical="6dp" />
|
app:chipSpacingVertical="6dp" />
|
||||||
|
|||||||
Reference in New Issue
Block a user