|
|
|
|
@ -1,6 +1,5 @@
|
|
|
|
|
package org.koitharu.kotatsu.search.domain
|
|
|
|
|
|
|
|
|
|
import android.annotation.SuppressLint
|
|
|
|
|
import android.app.SearchManager
|
|
|
|
|
import android.content.Context
|
|
|
|
|
import android.provider.SearchRecentSuggestions
|
|
|
|
|
@ -8,19 +7,18 @@ import dagger.Reusable
|
|
|
|
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
|
|
|
|
import kotlinx.coroutines.Dispatchers
|
|
|
|
|
import kotlinx.coroutines.currentCoroutineContext
|
|
|
|
|
import kotlinx.coroutines.flow.*
|
|
|
|
|
import kotlinx.coroutines.isActive
|
|
|
|
|
import kotlinx.coroutines.withContext
|
|
|
|
|
import org.koitharu.kotatsu.core.db.MangaDatabase
|
|
|
|
|
import org.koitharu.kotatsu.core.db.entity.toManga
|
|
|
|
|
import org.koitharu.kotatsu.core.db.entity.toMangaTag
|
|
|
|
|
import org.koitharu.kotatsu.core.parser.MangaRepository
|
|
|
|
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
|
|
|
|
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
|
|
|
|
|
import org.koitharu.kotatsu.parsers.model.ContentType
|
|
|
|
|
import org.koitharu.kotatsu.parsers.model.Manga
|
|
|
|
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
|
|
|
|
import org.koitharu.kotatsu.parsers.model.MangaTag
|
|
|
|
|
import org.koitharu.kotatsu.parsers.util.levenshteinDistance
|
|
|
|
|
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
|
|
|
|
import org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider
|
|
|
|
|
import javax.inject.Inject
|
|
|
|
|
|
|
|
|
|
@ -30,34 +28,22 @@ class MangaSearchRepository @Inject constructor(
|
|
|
|
|
private val sourcesRepository: MangaSourcesRepository,
|
|
|
|
|
@ApplicationContext private val context: Context,
|
|
|
|
|
private val recentSuggestions: SearchRecentSuggestions,
|
|
|
|
|
private val mangaRepositoryFactory: MangaRepository.Factory,
|
|
|
|
|
private val settings: AppSettings,
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
|
|
fun globalSearch(query: String, concurrency: Int = DEFAULT_CONCURRENCY): Flow<Manga> =
|
|
|
|
|
flow {
|
|
|
|
|
emitAll(sourcesRepository.getEnabledSources().asFlow())
|
|
|
|
|
}.flatMapMerge(concurrency) { source ->
|
|
|
|
|
runCatchingCancellable {
|
|
|
|
|
mangaRepositoryFactory.create(source).getList(
|
|
|
|
|
offset = 0,
|
|
|
|
|
query = query,
|
|
|
|
|
)
|
|
|
|
|
}.getOrElse {
|
|
|
|
|
emptyList()
|
|
|
|
|
}.asFlow()
|
|
|
|
|
}.filter {
|
|
|
|
|
match(it, query)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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.mangaDao.searchByTitle("%$query%", source.name, limit)
|
|
|
|
|
} else {
|
|
|
|
|
db.mangaDao.searchByTitle("%$query%", limit)
|
|
|
|
|
}.map { it.toManga() }
|
|
|
|
|
}.let {
|
|
|
|
|
if (skipNsfw) it.filterNot { x -> x.manga.isNsfw } else it
|
|
|
|
|
}
|
|
|
|
|
.map { it.toManga() }
|
|
|
|
|
.sortedBy { x -> x.title.levenshteinDistance(query) }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -67,7 +53,7 @@ class MangaSearchRepository @Inject constructor(
|
|
|
|
|
): List<String> = withContext(Dispatchers.IO) {
|
|
|
|
|
context.contentResolver.query(
|
|
|
|
|
MangaSuggestionsProvider.QUERY_URI,
|
|
|
|
|
SUGGESTION_PROJECTION,
|
|
|
|
|
arrayOf(SearchManager.SUGGEST_COLUMN_QUERY),
|
|
|
|
|
"${SearchManager.SUGGEST_COLUMN_QUERY} LIKE ?",
|
|
|
|
|
arrayOf("%$query%"),
|
|
|
|
|
"date DESC",
|
|
|
|
|
@ -102,8 +88,11 @@ class MangaSearchRepository @Inject constructor(
|
|
|
|
|
if (query.length < 3) {
|
|
|
|
|
return emptyList()
|
|
|
|
|
}
|
|
|
|
|
val skipNsfw = settings.isNsfwContentDisabled
|
|
|
|
|
val sources = sourcesRepository.allMangaSources
|
|
|
|
|
.filter { x -> x.title.contains(query, ignoreCase = true) }
|
|
|
|
|
.filter { x ->
|
|
|
|
|
(x.contentType != ContentType.HENTAI || !skipNsfw) && x.title.contains(query, ignoreCase = true)
|
|
|
|
|
}
|
|
|
|
|
return if (limit == 0) {
|
|
|
|
|
sources
|
|
|
|
|
} else {
|
|
|
|
|
@ -130,33 +119,10 @@ class MangaSearchRepository @Inject constructor(
|
|
|
|
|
suspend fun getSearchHistoryCount(): Int = withContext(Dispatchers.IO) {
|
|
|
|
|
context.contentResolver.query(
|
|
|
|
|
MangaSuggestionsProvider.QUERY_URI,
|
|
|
|
|
SUGGESTION_PROJECTION,
|
|
|
|
|
arrayOf(SearchManager.SUGGEST_COLUMN_QUERY),
|
|
|
|
|
null,
|
|
|
|
|
arrayOfNulls(1),
|
|
|
|
|
null,
|
|
|
|
|
)?.use { cursor -> cursor.count } ?: 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private companion object {
|
|
|
|
|
|
|
|
|
|
private val REGEX_SPACE = Regex("\\s+")
|
|
|
|
|
val SUGGESTION_PROJECTION = arrayOf(SearchManager.SUGGEST_COLUMN_QUERY)
|
|
|
|
|
|
|
|
|
|
@SuppressLint("DefaultLocale")
|
|
|
|
|
fun match(manga: Manga, query: String): Boolean {
|
|
|
|
|
val words = HashSet<String>()
|
|
|
|
|
words += manga.title.lowercase().split(REGEX_SPACE)
|
|
|
|
|
words += manga.altTitle?.lowercase()?.split(REGEX_SPACE).orEmpty()
|
|
|
|
|
val words2 = query.lowercase().split(REGEX_SPACE).toSet()
|
|
|
|
|
for (w in words) {
|
|
|
|
|
for (w2 in words2) {
|
|
|
|
|
val diff = w.levenshteinDistance(w2) / ((w.length + w2.length) / 2f)
|
|
|
|
|
if (diff < 0.5) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|