Improve global search
This commit is contained in:
@@ -35,7 +35,15 @@ abstract class FavouritesDao : MangaQueryBuilder.ConditionCallback {
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT manga.* FROM favourites LEFT JOIN manga ON manga.manga_id = favourites.manga_id WHERE favourites.deleted_at = 0 AND (manga.title LIKE :query OR manga.alt_title LIKE :query) LIMIT :limit")
|
||||
abstract suspend fun search(query: String, limit: Int): List<MangaWithTags>
|
||||
abstract suspend fun searchByTitle(query: String, limit: Int): List<MangaWithTags>
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT manga.* FROM favourites LEFT JOIN manga ON manga.manga_id = favourites.manga_id WHERE favourites.deleted_at = 0 AND (manga.author LIKE :query) LIMIT :limit")
|
||||
abstract suspend fun searchByAuthor(query: String, limit: Int): List<MangaWithTags>
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT manga.* FROM favourites LEFT JOIN manga ON manga.manga_id = favourites.manga_id WHERE favourites.deleted_at = 0 AND EXISTS(SELECT 1 FROM tags LEFT JOIN manga_tags ON manga_tags.tag_id = tags.tag_id WHERE manga_tags.manga_id = manga.manga_id AND tags.title LIKE :query) LIMIT :limit")
|
||||
abstract suspend fun searchByTag(query: String, limit: Int): List<MangaWithTags>
|
||||
|
||||
fun observeAll(
|
||||
order: ListSortOrder,
|
||||
|
||||
@@ -25,6 +25,7 @@ import org.koitharu.kotatsu.list.domain.ListSortOrder
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.util.levenshteinDistance
|
||||
import org.koitharu.kotatsu.search.domain.SearchKind
|
||||
import javax.inject.Inject
|
||||
|
||||
@Reusable
|
||||
@@ -43,9 +44,17 @@ class FavouritesRepository @Inject constructor(
|
||||
return entities.toMangaList()
|
||||
}
|
||||
|
||||
suspend fun search(query: String, limit: Int): List<Manga> {
|
||||
val entities = db.getFavouritesDao().search("%$query%", limit)
|
||||
return entities.toMangaList().sortedBy { it.title.levenshteinDistance(query) }
|
||||
suspend fun search(query: String, kind: SearchKind, limit: Int): List<Manga> {
|
||||
val dao = db.getFavouritesDao()
|
||||
val q = "%$query%"
|
||||
val entities = when (kind) {
|
||||
SearchKind.SIMPLE,
|
||||
SearchKind.TITLE -> dao.searchByTitle(q, limit).sortedBy { it.manga.title.levenshteinDistance(query) }
|
||||
|
||||
SearchKind.AUTHOR -> dao.searchByAuthor(q, limit)
|
||||
SearchKind.TAG -> dao.searchByTag(q, limit)
|
||||
}
|
||||
return entities.toMangaList()
|
||||
}
|
||||
|
||||
fun observeAll(order: ListSortOrder, filterOptions: Set<ListFilterOption>, limit: Int): Flow<List<Manga>> {
|
||||
|
||||
@@ -26,7 +26,15 @@ abstract class HistoryDao : MangaQueryBuilder.ConditionCallback {
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT manga.* FROM history LEFT JOIN manga ON manga.manga_id = history.manga_id WHERE history.deleted_at = 0 AND (manga.title LIKE :query OR manga.alt_title LIKE :query) LIMIT :limit")
|
||||
abstract suspend fun search(query: String, limit: Int): List<MangaWithTags>
|
||||
abstract suspend fun searchByTitle(query: String, limit: Int): List<MangaWithTags>
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT manga.* FROM history LEFT JOIN manga ON manga.manga_id = history.manga_id WHERE history.deleted_at = 0 AND (manga.author LIKE :query) LIMIT :limit")
|
||||
abstract suspend fun searchByAuthor(query: String, limit: Int): List<MangaWithTags>
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT manga.* FROM history LEFT JOIN manga ON manga.manga_id = history.manga_id WHERE history.deleted_at = 0 AND EXISTS(SELECT 1 FROM tags LEFT JOIN manga_tags ON manga_tags.tag_id = tags.tag_id WHERE manga_tags.manga_id = manga.manga_id AND tags.title LIKE :query) LIMIT :limit")
|
||||
abstract suspend fun searchByTag(query: String, limit: Int): List<MangaWithTags>
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM history WHERE deleted_at = 0 ORDER BY updated_at DESC")
|
||||
|
||||
@@ -33,6 +33,7 @@ import org.koitharu.kotatsu.parsers.util.findById
|
||||
import org.koitharu.kotatsu.parsers.util.levenshteinDistance
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.tryScrobble
|
||||
import org.koitharu.kotatsu.search.domain.SearchKind
|
||||
import org.koitharu.kotatsu.tracker.domain.CheckNewChaptersUseCase
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
@@ -52,9 +53,17 @@ class HistoryRepository @Inject constructor(
|
||||
return entities.map { it.toManga() }
|
||||
}
|
||||
|
||||
suspend fun search(query: String, limit: Int): List<Manga> {
|
||||
val entities = db.getHistoryDao().search("%$query%", limit)
|
||||
return entities.toMangaList().sortedBy { it.title.levenshteinDistance(query) }
|
||||
suspend fun search(query: String, kind: SearchKind, limit: Int): List<Manga> {
|
||||
val dao = db.getHistoryDao()
|
||||
val q = "%$query%"
|
||||
val entities = when (kind) {
|
||||
SearchKind.SIMPLE,
|
||||
SearchKind.TITLE -> dao.searchByTitle(q, limit).sortedBy { it.manga.title.levenshteinDistance(query) }
|
||||
|
||||
SearchKind.AUTHOR -> dao.searchByAuthor(q, limit)
|
||||
SearchKind.TAG -> dao.searchByTag(q, limit)
|
||||
}
|
||||
return entities.toMangaList()
|
||||
}
|
||||
|
||||
suspend fun getCount(): Int {
|
||||
|
||||
@@ -99,6 +99,8 @@ class SearchActivity :
|
||||
setSubtitle(R.string.search_results)
|
||||
}
|
||||
|
||||
addMenuProvider(SearchKindMenuProvider(this, viewModel.query, viewModel.kind))
|
||||
|
||||
viewModel.list.observe(this, adapter)
|
||||
viewModel.onError.observeEvent(this, SnackbarErrorObserver(viewBinding.recyclerView, null))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package org.koitharu.kotatsu.search.ui.multi
|
||||
|
||||
import android.os.Build
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import androidx.core.view.MenuProvider
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.nav.router
|
||||
import org.koitharu.kotatsu.search.domain.SearchKind
|
||||
|
||||
class SearchKindMenuProvider(
|
||||
private val activity: SearchActivity,
|
||||
private val query: String,
|
||||
private val kind: SearchKind
|
||||
) : MenuProvider {
|
||||
|
||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||
menuInflater.inflate(R.menu.opt_search_kind, menu)
|
||||
}
|
||||
|
||||
override fun onPrepareMenu(menu: Menu) {
|
||||
super.onPrepareMenu(menu)
|
||||
menu.findItem(
|
||||
when (kind) {
|
||||
SearchKind.SIMPLE -> R.id.action_kind_simple
|
||||
SearchKind.TITLE -> R.id.action_kind_title
|
||||
SearchKind.AUTHOR -> R.id.action_kind_author
|
||||
SearchKind.TAG -> R.id.action_kind_tag
|
||||
},
|
||||
)?.isChecked = true
|
||||
}
|
||||
|
||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
|
||||
val newKind = when (menuItem.itemId) {
|
||||
R.id.action_kind_simple -> SearchKind.SIMPLE
|
||||
R.id.action_kind_title -> SearchKind.TITLE
|
||||
R.id.action_kind_author -> SearchKind.AUTHOR
|
||||
R.id.action_kind_tag -> SearchKind.TAG
|
||||
else -> return false
|
||||
}
|
||||
if (newKind != kind) {
|
||||
activity.router.openSearch(
|
||||
query = query,
|
||||
kind = newKind,
|
||||
)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
activity.overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out, 0)
|
||||
} else {
|
||||
activity.overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
|
||||
}
|
||||
activity.finishAfterTransition()
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -37,10 +37,7 @@ import org.koitharu.kotatsu.list.ui.model.EmptyState
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
|
||||
import org.koitharu.kotatsu.list.ui.model.LoadingState
|
||||
import org.koitharu.kotatsu.local.data.LocalMangaRepository
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import org.koitharu.kotatsu.search.domain.SearchKind
|
||||
import org.koitharu.kotatsu.search.domain.SearchV2Helper
|
||||
@@ -56,7 +53,6 @@ class SearchViewModel @Inject constructor(
|
||||
private val searchHelperFactory: SearchV2Helper.Factory,
|
||||
private val sourcesRepository: MangaSourcesRepository,
|
||||
private val historyRepository: HistoryRepository,
|
||||
private val localMangaRepository: LocalMangaRepository,
|
||||
private val favouritesRepository: FavouritesRepository,
|
||||
) : BaseViewModel() {
|
||||
|
||||
@@ -65,7 +61,7 @@ class SearchViewModel @Inject constructor(
|
||||
|
||||
private val retryCounter = MutableStateFlow(0)
|
||||
private val listData = retryCounter.flatMapLatest {
|
||||
searchImpl(query).withLoading().withErrorHandling()
|
||||
searchImpl().withLoading().withErrorHandling()
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
|
||||
|
||||
val list: StateFlow<List<ListModel>> = combine(
|
||||
@@ -108,10 +104,10 @@ class SearchViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
@CheckResult
|
||||
private fun searchImpl(q: String): Flow<List<SearchResultsListModel>> = channelFlow {
|
||||
searchHistory(q)?.let { send(it) }
|
||||
searchFavorites(q)?.let { send(it) }
|
||||
searchLocal(q)?.let { send(it) }
|
||||
private fun searchImpl(): Flow<List<SearchResultsListModel>> = channelFlow {
|
||||
searchHistory()?.let { send(it) }
|
||||
searchFavorites()?.let { send(it) }
|
||||
searchLocal()?.let { send(it) }
|
||||
val sources = sourcesRepository.getEnabledSources()
|
||||
if (sources.isEmpty()) {
|
||||
return@channelFlow
|
||||
@@ -158,9 +154,9 @@ class SearchViewModel @Inject constructor(
|
||||
.filterNotNull()
|
||||
.onEmpty { emit(emptyList()) }
|
||||
|
||||
private suspend fun searchHistory(q: String): SearchResultsListModel? {
|
||||
private suspend fun searchHistory(): SearchResultsListModel? {
|
||||
return runCatchingCancellable {
|
||||
historyRepository.search(q, Int.MAX_VALUE)
|
||||
historyRepository.search(query, kind, Int.MAX_VALUE)
|
||||
}.fold(
|
||||
onSuccess = { result ->
|
||||
if (result.isNotEmpty()) {
|
||||
@@ -191,9 +187,9 @@ class SearchViewModel @Inject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun searchFavorites(q: String): SearchResultsListModel? {
|
||||
private suspend fun searchFavorites(): SearchResultsListModel? {
|
||||
return runCatchingCancellable {
|
||||
favouritesRepository.search(q, Int.MAX_VALUE)
|
||||
favouritesRepository.search(query, kind, Int.MAX_VALUE)
|
||||
}.fold(
|
||||
onSuccess = { result ->
|
||||
if (result.isNotEmpty()) {
|
||||
@@ -201,7 +197,11 @@ class SearchViewModel @Inject constructor(
|
||||
titleResId = R.string.favourites,
|
||||
source = UnknownMangaSource,
|
||||
hasMore = false,
|
||||
list = mangaListMapper.toListModelList(manga = result, mode = ListMode.GRID),
|
||||
list = mangaListMapper.toListModelList(
|
||||
manga = result,
|
||||
mode = ListMode.GRID,
|
||||
flags = MangaListMapper.NO_FAVORITE,
|
||||
),
|
||||
error = null,
|
||||
listFilter = null,
|
||||
sortOrder = null,
|
||||
@@ -224,20 +224,24 @@ class SearchViewModel @Inject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun searchLocal(q: String): SearchResultsListModel? {
|
||||
private suspend fun searchLocal(): SearchResultsListModel? {
|
||||
return runCatchingCancellable {
|
||||
localMangaRepository.getList(0, SortOrder.RELEVANCE, MangaListFilter(query = q))
|
||||
searchHelperFactory.create(LocalMangaSource).invoke(query, kind)
|
||||
}.fold(
|
||||
onSuccess = { result ->
|
||||
if (result.isNotEmpty()) {
|
||||
if (!result?.manga.isNullOrEmpty()) {
|
||||
SearchResultsListModel(
|
||||
titleResId = 0,
|
||||
source = LocalMangaSource,
|
||||
hasMore = result.size > MIN_HAS_MORE_ITEMS,
|
||||
list = mangaListMapper.toListModelList(manga = result, mode = ListMode.GRID),
|
||||
hasMore = result.manga.size > MIN_HAS_MORE_ITEMS,
|
||||
list = mangaListMapper.toListModelList(
|
||||
manga = result.manga,
|
||||
mode = ListMode.GRID,
|
||||
flags = MangaListMapper.NO_SAVED,
|
||||
),
|
||||
error = null,
|
||||
listFilter = null,
|
||||
sortOrder = null,
|
||||
listFilter = result.listFilter,
|
||||
sortOrder = result.sortOrder,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="top|start"
|
||||
android:layout_marginTop="@dimen/card_indicator_offset"
|
||||
android:layout_marginTop="10dp"
|
||||
android:background="@drawable/bg_list_icons"
|
||||
android:orientation="horizontal"
|
||||
android:padding="4dp"
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
android:id="@+id/iconsView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/card_indicator_offset"
|
||||
android:layout_marginTop="10dp"
|
||||
android:background="@drawable/bg_list_icons"
|
||||
android:orientation="horizontal"
|
||||
android:padding="4dp"
|
||||
|
||||
40
app/src/main/res/menu/opt_search_kind.xml
Normal file
40
app/src/main/res/menu/opt_search_kind.xml
Normal file
@@ -0,0 +1,40 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/action_search_kind"
|
||||
android:icon="@drawable/ic_filter_menu"
|
||||
android:orderInCategory="0"
|
||||
android:title="@string/type"
|
||||
app:showAsAction="ifRoom">
|
||||
|
||||
<menu>
|
||||
|
||||
<group
|
||||
android:id="@+id/group_search_kind"
|
||||
android:checkableBehavior="single">
|
||||
|
||||
<item
|
||||
android:id="@+id/action_kind_simple"
|
||||
android:title="@string/simple" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_kind_title"
|
||||
android:title="@string/name" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_kind_author"
|
||||
android:title="@string/author" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_kind_tag"
|
||||
android:title="@string/genre" />
|
||||
|
||||
</group>
|
||||
</menu>
|
||||
|
||||
</item>
|
||||
|
||||
</menu>
|
||||
@@ -802,4 +802,5 @@
|
||||
<string name="screen_rotation_unlocked">Screen rotation has been unlocked</string>
|
||||
<string name="badges_in_lists">Badges in lists</string>
|
||||
<string name="search_everywhere">Search everywhere</string>
|
||||
<string name="simple">Simple</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user