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")
|
@Query("SELECT * FROM manga WHERE source = :source")
|
||||||
abstract suspend fun findAllBySource(source: String): List<MangaWithTags>
|
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
|
@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")
|
@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>
|
abstract suspend fun searchByTitle(query: String, limit: Int): List<MangaWithTags>
|
||||||
|
|||||||
@@ -12,4 +12,5 @@ enum class SearchSuggestionType(
|
|||||||
QUERIES_SUGGEST(R.string.suggested_queries),
|
QUERIES_SUGGEST(R.string.suggested_queries),
|
||||||
MANGA(R.string.content_type_manga),
|
MANGA(R.string.content_type_manga),
|
||||||
SOURCES(R.string.remote_sources),
|
SOURCES(R.string.remote_sources),
|
||||||
|
AUTHORS(R.string.authors),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import kotlinx.coroutines.currentCoroutineContext
|
|||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
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.toEntity
|
||||||
import org.koitharu.kotatsu.core.db.entity.toManga
|
import org.koitharu.kotatsu.core.db.entity.toManga
|
||||||
import org.koitharu.kotatsu.core.db.entity.toMangaTag
|
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> {
|
suspend fun getMangaSuggestion(query: String, limit: Int, source: MangaSource?): List<Manga> {
|
||||||
if (query.isEmpty()) {
|
return when {
|
||||||
return emptyList()
|
query.isEmpty() -> db.getSuggestionDao().getRandom(limit).map { MangaWithTags(it.manga, it.tags) }
|
||||||
}
|
source != null -> db.getMangaDao().searchByTitle("%$query%", source.name, limit)
|
||||||
val skipNsfw = settings.isNsfwContentDisabled
|
else -> db.getMangaDao().searchByTitle("%$query%", limit)
|
||||||
return if (source != null) {
|
|
||||||
db.getMangaDao().searchByTitle("%$query%", source.name, limit)
|
|
||||||
} else {
|
|
||||||
db.getMangaDao().searchByTitle("%$query%", limit)
|
|
||||||
}.let {
|
}.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(
|
suspend fun getQuerySuggestion(
|
||||||
@@ -90,10 +89,21 @@ class MangaSearchRepository @Inject constructor(
|
|||||||
return titles.shuffled().take(limit)
|
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> {
|
suspend fun getTagsSuggestion(query: String, limit: Int, source: MangaSource?): List<MangaTag> {
|
||||||
return when {
|
return when {
|
||||||
query.isNotEmpty() && source != null -> db.getTagsDao()
|
query.isNotEmpty() && source != null -> db.getTagsDao()
|
||||||
.findTags(source.name, "%$query%", limit)
|
.findTags(source.name, "%$query%", limit)
|
||||||
|
|
||||||
query.isNotEmpty() -> db.getTagsDao().findTags("%$query%", limit)
|
query.isNotEmpty() -> db.getTagsDao().findTags("%$query%", limit)
|
||||||
source != null -> db.getTagsDao().findPopularTags(source.name, limit)
|
source != null -> db.getTagsDao().findPopularTags(source.name, limit)
|
||||||
else -> db.getTagsDao().findPopularTags(limit)
|
else -> db.getTagsDao().findPopularTags(limit)
|
||||||
|
|||||||
@@ -31,9 +31,10 @@ import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem
|
|||||||
import javax.inject.Inject
|
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 = 12
|
||||||
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_HINTS_ITEMS = 3
|
||||||
|
private const val MAX_AUTHORS_ITEMS = 2
|
||||||
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
|
||||||
|
|
||||||
@@ -128,6 +129,11 @@ class SearchSuggestionViewModel @Inject constructor(
|
|||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
val authorsDeferred = if (SearchSuggestionType.AUTHORS in types) {
|
||||||
|
async { repository.getAuthorsSuggestion(searchQuery, MAX_AUTHORS_ITEMS) }
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
val tagsDeferred = if (SearchSuggestionType.GENRES in types) {
|
val tagsDeferred = if (SearchSuggestionType.GENRES in types) {
|
||||||
async { repository.getTagsSuggestion(searchQuery, MAX_TAGS_ITEMS, null) }
|
async { repository.getTagsSuggestion(searchQuery, MAX_TAGS_ITEMS, null) }
|
||||||
} else {
|
} else {
|
||||||
@@ -148,8 +154,9 @@ class SearchSuggestionViewModel @Inject constructor(
|
|||||||
val mangaList = mangaDeferred?.await()
|
val mangaList = mangaDeferred?.await()
|
||||||
val queries = queriesDeferred?.await()
|
val queries = queriesDeferred?.await()
|
||||||
val hints = hintsDeferred?.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()) {
|
if (!tags.isNullOrEmpty()) {
|
||||||
add(SearchSuggestionItem.Tags(mapTags(tags)))
|
add(SearchSuggestionItem.Tags(mapTags(tags)))
|
||||||
}
|
}
|
||||||
@@ -158,6 +165,7 @@ class SearchSuggestionViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
sources?.mapTo(this) { SearchSuggestionItem.Source(it, it in enabledSources) }
|
sources?.mapTo(this) { SearchSuggestionItem.Source(it, it in enabledSources) }
|
||||||
queries?.mapTo(this) { SearchSuggestionItem.RecentQuery(it) }
|
queries?.mapTo(this) { SearchSuggestionItem.RecentQuery(it) }
|
||||||
|
authors?.mapTo(this) { SearchSuggestionItem.Author(it) }
|
||||||
hints?.mapTo(this) { SearchSuggestionItem.Hint(it) }
|
hints?.mapTo(this) { SearchSuggestionItem.Hint(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,5 +21,6 @@ class SearchSuggestionAdapter(
|
|||||||
.addDelegate(searchSuggestionTagsAD(listener))
|
.addDelegate(searchSuggestionTagsAD(listener))
|
||||||
.addDelegate(searchSuggestionMangaListAD(coil, lifecycleOwner, listener))
|
.addDelegate(searchSuggestionMangaListAD(coil, lifecycleOwner, listener))
|
||||||
.addDelegate(searchSuggestionQueryHintAD(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.adapterDelegate
|
||||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||||
import org.koitharu.kotatsu.R
|
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.ui.list.decor.SpacingItemDecoration
|
||||||
import org.koitharu.kotatsu.core.util.RecyclerViewScrollCallback
|
import org.koitharu.kotatsu.core.util.RecyclerViewScrollCallback
|
||||||
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
|
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
|
||||||
@@ -59,6 +60,7 @@ private fun searchSuggestionMangaGridAD(
|
|||||||
binding.imageViewCover.newImageRequest(lifecycleOwner, item.coverUrl)?.run {
|
binding.imageViewCover.newImageRequest(lifecycleOwner, item.coverUrl)?.run {
|
||||||
defaultPlaceholders(context)
|
defaultPlaceholders(context)
|
||||||
allowRgb565(true)
|
allowRgb565(true)
|
||||||
|
transformations(TrimTransformation())
|
||||||
source(item.source)
|
source(item.source)
|
||||||
enqueueWith(coil)
|
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(
|
data class Source(
|
||||||
val source: MangaSource,
|
val source: MangaSource,
|
||||||
val isEnabled: Boolean,
|
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"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="?android:windowBackground"
|
android:background="@drawable/bg_search_suggestion"
|
||||||
android:clickable="true"
|
android:clickable="true"
|
||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:scrollbars="vertical"
|
android:scrollbars="vertical"
|
||||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
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:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
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_width="wrap_content"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
app:contentPadding="4dp"
|
|
||||||
app:shapeAppearance="?shapeAppearanceCornerSmall"
|
app:shapeAppearance="?shapeAppearanceCornerSmall"
|
||||||
tools:layout_height="@dimen/search_suggestions_manga_height">
|
tools:layout_height="@dimen/search_suggestions_manga_height">
|
||||||
|
|
||||||
@@ -32,6 +31,7 @@
|
|||||||
android:elegantTextHeight="false"
|
android:elegantTextHeight="false"
|
||||||
android:ellipsize="end"
|
android:ellipsize="end"
|
||||||
android:lines="1"
|
android:lines="1"
|
||||||
|
android:padding="4dp"
|
||||||
android:textAppearance="?attr/textAppearanceLabelSmall"
|
android:textAppearance="?attr/textAppearanceLabelSmall"
|
||||||
tools:text="@tools:sample/lorem" />
|
tools:text="@tools:sample/lorem" />
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,9 @@
|
|||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:paddingStart="?listPreferredItemPaddingStart"
|
android:paddingStart="?listPreferredItemPaddingStart"
|
||||||
|
android:paddingTop="6dp"
|
||||||
android:paddingEnd="?listPreferredItemPaddingEnd"
|
android:paddingEnd="?listPreferredItemPaddingEnd"
|
||||||
|
android:paddingBottom="12dp"
|
||||||
app:singleLine="true" />
|
app:singleLine="true" />
|
||||||
|
|
||||||
</HorizontalScrollView>
|
</HorizontalScrollView>
|
||||||
|
|||||||
@@ -36,8 +36,8 @@
|
|||||||
<dimen name="side_card_offset">8dp</dimen>
|
<dimen name="side_card_offset">8dp</dimen>
|
||||||
<dimen name="webtoon_pages_gap">24dp</dimen>
|
<dimen name="webtoon_pages_gap">24dp</dimen>
|
||||||
|
|
||||||
<dimen name="search_suggestions_manga_height">124dp</dimen>
|
<dimen name="search_suggestions_manga_height">142dp</dimen>
|
||||||
<dimen name="search_suggestions_manga_spacing">4dp</dimen>
|
<dimen name="search_suggestions_manga_spacing">6dp</dimen>
|
||||||
|
|
||||||
<dimen name="card_indicator_size">32dp</dimen>
|
<dimen name="card_indicator_size">32dp</dimen>
|
||||||
<dimen name="card_indicator_size_small">24dp</dimen>
|
<dimen name="card_indicator_size_small">24dp</dimen>
|
||||||
|
|||||||
@@ -639,4 +639,5 @@
|
|||||||
<string name="search_suggestions">Search suggestions</string>
|
<string name="search_suggestions">Search suggestions</string>
|
||||||
<string name="recent_queries">Recent queries</string>
|
<string name="recent_queries">Recent queries</string>
|
||||||
<string name="suggested_queries">Suggested queries</string>
|
<string name="suggested_queries">Suggested queries</string>
|
||||||
|
<string name="authors">Authors</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user