Add authors suggestion and update search suggestion ui
This commit is contained in:
@@ -28,6 +28,9 @@ abstract class MangaDao {
|
||||
@Query("SELECT * FROM manga WHERE source = :source")
|
||||
abstract suspend fun findAllBySource(source: String): List<MangaWithTags>
|
||||
|
||||
@Query("SELECT author FROM manga WHERE author LIKE :query GROUP BY author ORDER BY COUNT(author) DESC LIMIT :limit")
|
||||
abstract suspend fun findAuthors(query: String, limit: Int): List<String>
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM manga WHERE (title LIKE :query OR alt_title LIKE :query) AND manga_id IN (SELECT manga_id FROM favourites UNION SELECT manga_id FROM history) LIMIT :limit")
|
||||
abstract suspend fun searchByTitle(query: String, limit: Int): List<MangaWithTags>
|
||||
|
||||
@@ -12,4 +12,5 @@ enum class SearchSuggestionType(
|
||||
QUERIES_SUGGEST(R.string.suggested_queries),
|
||||
MANGA(R.string.content_type_manga),
|
||||
SOURCES(R.string.remote_sources),
|
||||
AUTHORS(R.string.authors),
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import kotlinx.coroutines.currentCoroutineContext
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaWithTags
|
||||
import org.koitharu.kotatsu.core.db.entity.toEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.toManga
|
||||
import org.koitharu.kotatsu.core.db.entity.toMangaTag
|
||||
@@ -35,19 +36,17 @@ class MangaSearchRepository @Inject constructor(
|
||||
) {
|
||||
|
||||
suspend fun getMangaSuggestion(query: String, limit: Int, source: MangaSource?): List<Manga> {
|
||||
if (query.isEmpty()) {
|
||||
return emptyList()
|
||||
}
|
||||
val skipNsfw = settings.isNsfwContentDisabled
|
||||
return if (source != null) {
|
||||
db.getMangaDao().searchByTitle("%$query%", source.name, limit)
|
||||
} else {
|
||||
db.getMangaDao().searchByTitle("%$query%", limit)
|
||||
return when {
|
||||
query.isEmpty() -> db.getSuggestionDao().getRandom(limit).map { MangaWithTags(it.manga, it.tags) }
|
||||
source != null -> db.getMangaDao().searchByTitle("%$query%", source.name, limit)
|
||||
else -> db.getMangaDao().searchByTitle("%$query%", limit)
|
||||
}.let {
|
||||
if (skipNsfw) it.filterNot { x -> x.manga.isNsfw } else it
|
||||
if (settings.isNsfwContentDisabled) it.filterNot { x -> x.manga.isNsfw } else it
|
||||
}.map {
|
||||
it.toManga()
|
||||
}.sortedBy { x ->
|
||||
x.title.levenshteinDistance(query)
|
||||
}
|
||||
.map { it.toManga() }
|
||||
.sortedBy { x -> x.title.levenshteinDistance(query) }
|
||||
}
|
||||
|
||||
suspend fun getQuerySuggestion(
|
||||
@@ -90,10 +89,21 @@ class MangaSearchRepository @Inject constructor(
|
||||
return titles.shuffled().take(limit)
|
||||
}
|
||||
|
||||
suspend fun getAuthorsSuggestion(
|
||||
query: String,
|
||||
limit: Int,
|
||||
): List<String> {
|
||||
if (query.isEmpty()) {
|
||||
return emptyList()
|
||||
}
|
||||
return db.getMangaDao().findAuthors("$query%", limit)
|
||||
}
|
||||
|
||||
suspend fun getTagsSuggestion(query: String, limit: Int, source: MangaSource?): List<MangaTag> {
|
||||
return when {
|
||||
query.isNotEmpty() && source != null -> db.getTagsDao()
|
||||
.findTags(source.name, "%$query%", limit)
|
||||
|
||||
query.isNotEmpty() -> db.getTagsDao().findTags("%$query%", limit)
|
||||
source != null -> db.getTagsDao().findPopularTags(source.name, limit)
|
||||
else -> db.getTagsDao().findPopularTags(limit)
|
||||
|
||||
@@ -31,9 +31,10 @@ import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val DEBOUNCE_TIMEOUT = 500L
|
||||
private const val MAX_MANGA_ITEMS = 6
|
||||
private const val MAX_MANGA_ITEMS = 12
|
||||
private const val MAX_QUERY_ITEMS = 16
|
||||
private const val MAX_HINTS_ITEMS = 3
|
||||
private const val MAX_AUTHORS_ITEMS = 2
|
||||
private const val MAX_TAGS_ITEMS = 8
|
||||
private const val MAX_SOURCES_ITEMS = 6
|
||||
|
||||
@@ -128,6 +129,11 @@ class SearchSuggestionViewModel @Inject constructor(
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val authorsDeferred = if (SearchSuggestionType.AUTHORS in types) {
|
||||
async { repository.getAuthorsSuggestion(searchQuery, MAX_AUTHORS_ITEMS) }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val tagsDeferred = if (SearchSuggestionType.GENRES in types) {
|
||||
async { repository.getTagsSuggestion(searchQuery, MAX_TAGS_ITEMS, null) }
|
||||
} else {
|
||||
@@ -148,8 +154,9 @@ class SearchSuggestionViewModel @Inject constructor(
|
||||
val mangaList = mangaDeferred?.await()
|
||||
val queries = queriesDeferred?.await()
|
||||
val hints = hintsDeferred?.await()
|
||||
val authors = authorsDeferred?.await()
|
||||
|
||||
buildList(queries.sizeOrZero() + sources.sizeOrZero() + hints.sizeOrZero() + 2) {
|
||||
buildList(queries.sizeOrZero() + sources.sizeOrZero() + authors.sizeOrZero() + hints.sizeOrZero() + 2) {
|
||||
if (!tags.isNullOrEmpty()) {
|
||||
add(SearchSuggestionItem.Tags(mapTags(tags)))
|
||||
}
|
||||
@@ -158,6 +165,7 @@ class SearchSuggestionViewModel @Inject constructor(
|
||||
}
|
||||
sources?.mapTo(this) { SearchSuggestionItem.Source(it, it in enabledSources) }
|
||||
queries?.mapTo(this) { SearchSuggestionItem.RecentQuery(it) }
|
||||
authors?.mapTo(this) { SearchSuggestionItem.Author(it) }
|
||||
hints?.mapTo(this) { SearchSuggestionItem.Hint(it) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,5 +21,6 @@ class SearchSuggestionAdapter(
|
||||
.addDelegate(searchSuggestionTagsAD(listener))
|
||||
.addDelegate(searchSuggestionMangaListAD(coil, lifecycleOwner, listener))
|
||||
.addDelegate(searchSuggestionQueryHintAD(listener))
|
||||
.addDelegate(searchSuggestionAuthorAD(listener))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package org.koitharu.kotatsu.search.ui.suggestion.adapter
|
||||
|
||||
import android.view.View
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.databinding.ItemSearchSuggestionQueryHintBinding
|
||||
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener
|
||||
import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem
|
||||
|
||||
fun searchSuggestionAuthorAD(
|
||||
listener: SearchSuggestionListener,
|
||||
) = adapterDelegateViewBinding<SearchSuggestionItem.Author, SearchSuggestionItem, ItemSearchSuggestionQueryHintBinding>(
|
||||
{ inflater, parent -> ItemSearchSuggestionQueryHintBinding.inflate(inflater, parent, false) },
|
||||
) {
|
||||
|
||||
val viewClickListener = View.OnClickListener { _ ->
|
||||
listener.onQueryClick(item.name, true)
|
||||
}
|
||||
|
||||
binding.root.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_user, 0, 0, 0)
|
||||
binding.root.setOnClickListener(viewClickListener)
|
||||
|
||||
bind {
|
||||
binding.root.text = item.name
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.ui.image.TrimTransformation
|
||||
import org.koitharu.kotatsu.core.ui.list.decor.SpacingItemDecoration
|
||||
import org.koitharu.kotatsu.core.util.RecyclerViewScrollCallback
|
||||
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
|
||||
@@ -59,6 +60,7 @@ private fun searchSuggestionMangaGridAD(
|
||||
binding.imageViewCover.newImageRequest(lifecycleOwner, item.coverUrl)?.run {
|
||||
defaultPlaceholders(context)
|
||||
allowRgb565(true)
|
||||
transformations(TrimTransformation())
|
||||
source(item.source)
|
||||
enqueueWith(coil)
|
||||
}
|
||||
|
||||
@@ -36,6 +36,15 @@ sealed interface SearchSuggestionItem : ListModel {
|
||||
}
|
||||
}
|
||||
|
||||
data class Author(
|
||||
val name: String,
|
||||
) : SearchSuggestionItem {
|
||||
|
||||
override fun areItemsTheSame(other: ListModel): Boolean {
|
||||
return other is Author && name == other.name
|
||||
}
|
||||
}
|
||||
|
||||
data class Source(
|
||||
val source: MangaSource,
|
||||
val isEnabled: Boolean,
|
||||
|
||||
8
app/src/main/res/drawable/bg_search_suggestion.xml
Normal file
8
app/src/main/res/drawable/bg_search_suggestion.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="?android:windowBackground" />
|
||||
<item
|
||||
android:drawable="@drawable/divider_horizontal"
|
||||
android:gravity="top" />
|
||||
</layer-list>
|
||||
6
app/src/main/res/drawable/divider_horizontal.xml
Normal file
6
app/src/main/res/drawable/divider_horizontal.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<size android:height="1dp" />
|
||||
<solid android:color="?colorSurfaceDim" />
|
||||
</shape>
|
||||
@@ -5,10 +5,10 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?android:windowBackground"
|
||||
android:background="@drawable/bg_search_suggestion"
|
||||
android:clickable="true"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="vertical"
|
||||
android:scrollbars="vertical"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:ignore="KeyboardInaccessibleWidget" />
|
||||
tools:ignore="KeyboardInaccessibleWidget" />
|
||||
|
||||
@@ -3,10 +3,9 @@
|
||||
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"
|
||||
style="@style/Widget.Material3.CardView.Outlined"
|
||||
style="@style/Widget.Material3.CardView.Filled"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
app:contentPadding="4dp"
|
||||
app:shapeAppearance="?shapeAppearanceCornerSmall"
|
||||
tools:layout_height="@dimen/search_suggestions_manga_height">
|
||||
|
||||
@@ -32,6 +31,7 @@
|
||||
android:elegantTextHeight="false"
|
||||
android:ellipsize="end"
|
||||
android:lines="1"
|
||||
android:padding="4dp"
|
||||
android:textAppearance="?attr/textAppearanceLabelSmall"
|
||||
tools:text="@tools:sample/lorem" />
|
||||
|
||||
|
||||
@@ -12,7 +12,9 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="?listPreferredItemPaddingStart"
|
||||
android:paddingTop="6dp"
|
||||
android:paddingEnd="?listPreferredItemPaddingEnd"
|
||||
android:paddingBottom="12dp"
|
||||
app:singleLine="true" />
|
||||
|
||||
</HorizontalScrollView>
|
||||
|
||||
@@ -36,8 +36,8 @@
|
||||
<dimen name="side_card_offset">8dp</dimen>
|
||||
<dimen name="webtoon_pages_gap">24dp</dimen>
|
||||
|
||||
<dimen name="search_suggestions_manga_height">124dp</dimen>
|
||||
<dimen name="search_suggestions_manga_spacing">4dp</dimen>
|
||||
<dimen name="search_suggestions_manga_height">142dp</dimen>
|
||||
<dimen name="search_suggestions_manga_spacing">6dp</dimen>
|
||||
|
||||
<dimen name="card_indicator_size">32dp</dimen>
|
||||
<dimen name="card_indicator_size_small">24dp</dimen>
|
||||
|
||||
@@ -639,4 +639,5 @@
|
||||
<string name="search_suggestions">Search suggestions</string>
|
||||
<string name="recent_queries">Recent queries</string>
|
||||
<string name="suggested_queries">Suggested queries</string>
|
||||
<string name="authors">Authors</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user