diff --git a/app/build.gradle b/app/build.gradle index d3435c46f..bc7924a1a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -54,6 +54,7 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { jvmTarget = JavaVersion.VERSION_1_8.toString() freeCompilerArgs += [ '-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi', + '-Xopt-in=kotlinx.coroutines.FlowPreview', '-Xopt-in=org.koin.core.component.KoinApiExtension' ] } diff --git a/app/src/main/java/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt b/app/src/main/java/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt index 7e46ae4d1..1efec6723 100644 --- a/app/src/main/java/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt @@ -1,40 +1,46 @@ package org.koitharu.kotatsu.search.domain -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow +import android.annotation.SuppressLint +import kotlinx.coroutines.flow.* import org.koitharu.kotatsu.base.domain.MangaProviderFactory import org.koitharu.kotatsu.core.model.Manga -import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.model.SortOrder import org.koitharu.kotatsu.core.prefs.AppSettings -import java.util.* +import org.koitharu.kotatsu.utils.ext.levenshteinDistance class MangaSearchRepository(private val settings: AppSettings) { - fun globalSearch(query: String, batchSize: Int = 4): Flow> = flow { - val sources = MangaProviderFactory.getSources(settings, includeHidden = false) - val lists = EnumMap>(MangaSource::class.java) - var i = 0 - while (true) { - var isEmitted = false - for (source in sources) { - val list = lists.getOrPut(source) { - try { - source.repository.getList(0, query, SortOrder.POPULARITY) - } catch (e: Throwable) { - e.printStackTrace() - emptyList() + fun globalSearch(query: String, concurrency: Int = DEFAULT_CONCURRENCY): Flow = + MangaProviderFactory.getSources(settings, includeHidden = false).asFlow() + .flatMapMerge(concurrency) { source -> + runCatching { + source.repository.getList(0, query, SortOrder.POPULARITY) + }.getOrElse { + emptyList() + }.asFlow() + }.filter { + match(it, query) + } + + private companion object { + + private val REGEX_SPACE = Regex("\\s+") + + @SuppressLint("DefaultLocale") + fun match(manga: Manga, query: String): Boolean { + val words = HashSet() + words += manga.title.toLowerCase().split(REGEX_SPACE) + words += manga.altTitle?.toLowerCase()?.split(REGEX_SPACE).orEmpty() + val words2 = query.toLowerCase().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 } } - if (i < list.size) { - emit(list.subList(i, (i + batchSize).coerceAtMost(list.lastIndex))) - isEmitted = true - } - } - i += batchSize - if (!isEmitted) { - return@flow } + return false } } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/global/GlobalSearchViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/global/GlobalSearchViewModel.kt index 3486d583f..72b64f509 100644 --- a/app/src/main/java/org/koitharu/kotatsu/search/ui/global/GlobalSearchViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/global/GlobalSearchViewModel.kt @@ -62,8 +62,8 @@ class GlobalSearchViewModel( .catch { e -> listError.value = e isLoading.postValue(false) - }.filterNot { x -> x.isEmpty() } - .onStart { + }.onStart { + mangaList.value = null listError.value = null isLoading.postValue(true) hasNextPage.value = true @@ -75,7 +75,7 @@ class GlobalSearchViewModel( }.onFirst { isLoading.postValue(false) }.onEach { - mangaList.value = mangaList.value?.plus(it) ?: it + mangaList.value = mangaList.value?.plus(it) ?: listOf(it) }.launchIn(viewModelScope + Dispatchers.Default) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/StringExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/StringExt.kt index c6a52ecd5..05ec233ae 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/StringExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/StringExt.kt @@ -6,6 +6,7 @@ import java.math.BigInteger import java.net.URLEncoder import java.security.MessageDigest import java.util.* +import kotlin.math.min fun String.longHashCode(): Long { var h = 1125899906842597L @@ -65,7 +66,7 @@ fun String.toUriOrNull(): Uri? = if (isEmpty()) { Uri.parse(this) } -fun ByteArray.byte2HexFormatted(): String? { +fun ByteArray.byte2HexFormatted(): String { val str = StringBuilder(size * 2) for (i in indices) { var h = Integer.toHexString(this[i].toInt()) @@ -104,4 +105,42 @@ fun String.substringBetween(from: String, to: String, fallbackValue: String): St } } -fun String.find(regex: Regex) = regex.find(this)?.value \ No newline at end of file +fun String.find(regex: Regex) = regex.find(this)?.value + +fun String.levenshteinDistance(other: String): Int { + if (this == other) { + return 0 + } + if (this.isEmpty()) { + return other.length + } + if (other.isEmpty()) { + return this.length + } + + val lhsLength = this.length + 1 + val rhsLength = other.length + 1 + + var cost = Array(lhsLength) { it } + var newCost = Array(lhsLength) { 0 } + + for (i in 1 until rhsLength) { + newCost[0] = i + + for (j in 1 until lhsLength) { + val match = if (this[j - 1] == other[i - 1]) 0 else 1 + + val costReplace = cost[j - 1] + match + val costInsert = cost[j] + 1 + val costDelete = newCost[j - 1] + 1 + + newCost[j] = min(min(costInsert, costDelete), costReplace) + } + + val swap = cost + cost = newCost + newCost = swap + } + + return cost[lhsLength - 1] +} \ No newline at end of file