Update the EH search engine to fix issues with the current search features
This commit is contained in:
parent
fb19f6b860
commit
4f803494ff
@ -58,11 +58,11 @@ class LibraryCategoryAdapter(view: LibraryCategoryView) :
|
|||||||
*
|
*
|
||||||
* @param list the list to set.
|
* @param list the list to set.
|
||||||
*/
|
*/
|
||||||
suspend fun setItems(cScope: CoroutineScope, list: List<LibraryItem>) {
|
suspend fun setItems(scope: CoroutineScope, list: List<LibraryItem>) {
|
||||||
// A copy of manga always unfiltered.
|
// A copy of manga always unfiltered.
|
||||||
mangas = list.toList()
|
mangas = list.toList()
|
||||||
|
|
||||||
performFilter(cScope)
|
performFilter(scope)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -78,12 +78,12 @@ class LibraryCategoryAdapter(view: LibraryCategoryView) :
|
|||||||
// Note that we cannot use FlexibleAdapter's built in filtering system as we cannot cancel it
|
// Note that we cannot use FlexibleAdapter's built in filtering system as we cannot cancel it
|
||||||
// (well technically we can cancel it by invoking filterItems again but that doesn't work when
|
// (well technically we can cancel it by invoking filterItems again but that doesn't work when
|
||||||
// we want to perform a no-op filter)
|
// we want to perform a no-op filter)
|
||||||
suspend fun performFilter(cScope: CoroutineScope) {
|
suspend fun performFilter(scope: CoroutineScope) {
|
||||||
lastFilterJob?.cancel()
|
lastFilterJob?.cancel()
|
||||||
if (mangas.isNotEmpty() && searchText.isNotBlank()) {
|
if (mangas.isNotEmpty() && searchText.isNotBlank()) {
|
||||||
val savedSearchText = searchText
|
val savedSearchText = searchText
|
||||||
|
|
||||||
val job = cScope.launch(Dispatchers.IO) {
|
val job = scope.launch(Dispatchers.IO) {
|
||||||
val newManga = try {
|
val newManga = try {
|
||||||
// Prepare filter object
|
// Prepare filter object
|
||||||
val parsedQuery = searchEngine.parseQuery(savedSearchText)
|
val parsedQuery = searchEngine.parseQuery(savedSearchText)
|
||||||
@ -134,11 +134,11 @@ class LibraryCategoryAdapter(view: LibraryCategoryView) :
|
|||||||
// Check if this manga even has metadata
|
// Check if this manga even has metadata
|
||||||
if (mangaWithMetaIds.binarySearch(mangaId) < 0) {
|
if (mangaWithMetaIds.binarySearch(mangaId) < 0) {
|
||||||
// No meta? Filter using title
|
// No meta? Filter using title
|
||||||
item.filter(savedSearchText)
|
item.filter(savedSearchText to true)
|
||||||
} else false
|
} else item.filter(savedSearchText to false)
|
||||||
} else true
|
} else true
|
||||||
} else {
|
} else {
|
||||||
item.filter(savedSearchText)
|
item.filter(savedSearchText to true)
|
||||||
}
|
}
|
||||||
}.toList()
|
}.toList()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
@ -86,7 +86,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
|||||||
|
|
||||||
// EXH -->
|
// EXH -->
|
||||||
private var initialLoadHandle: LoadingHandle? = null
|
private var initialLoadHandle: LoadingHandle? = null
|
||||||
lateinit var scope2: CoroutineScope
|
private lateinit var supervisorScope: CoroutineScope
|
||||||
|
|
||||||
private fun newScope() = object : CoroutineScope {
|
private fun newScope() = object : CoroutineScope {
|
||||||
override val coroutineContext = SupervisorJob() + Dispatchers.Main
|
override val coroutineContext = SupervisorJob() + Dispatchers.Main
|
||||||
@ -150,7 +150,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
|||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
// EXH -->
|
// EXH -->
|
||||||
scope2 = newScope()
|
supervisorScope = newScope()
|
||||||
initialLoadHandle = controller.loaderManager.openProgressBar()
|
initialLoadHandle = controller.loaderManager.openProgressBar()
|
||||||
// EXH <--
|
// EXH <--
|
||||||
|
|
||||||
@ -161,7 +161,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
|||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe {
|
.subscribe {
|
||||||
// EXH -->
|
// EXH -->
|
||||||
scope2.launch {
|
supervisorScope.launch {
|
||||||
val handle = controller.loaderManager.openProgressBar()
|
val handle = controller.loaderManager.openProgressBar()
|
||||||
try {
|
try {
|
||||||
// EXH <--
|
// EXH <--
|
||||||
@ -177,7 +177,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
|||||||
subscriptions += controller.libraryMangaRelay
|
subscriptions += controller.libraryMangaRelay
|
||||||
.subscribe {
|
.subscribe {
|
||||||
// EXH -->
|
// EXH -->
|
||||||
scope2.launch {
|
supervisorScope.launch {
|
||||||
try {
|
try {
|
||||||
// EXH <--
|
// EXH <--
|
||||||
onNextLibraryManga(this, it)
|
onNextLibraryManga(this, it)
|
||||||
@ -249,7 +249,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
|||||||
fun unsubscribe() {
|
fun unsubscribe() {
|
||||||
subscriptions.clear()
|
subscriptions.clear()
|
||||||
// EXH -->
|
// EXH -->
|
||||||
scope2.cancel()
|
supervisorScope.cancel()
|
||||||
controller.loaderManager.closeProgressBar(initialLoadHandle)
|
controller.loaderManager.closeProgressBar(initialLoadHandle)
|
||||||
// EXH <--
|
// EXH <--
|
||||||
}
|
}
|
||||||
|
@ -19,18 +19,26 @@ import eu.kanade.tachiyomi.data.preference.PreferenceValues.DisplayMode
|
|||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
|
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
|
||||||
|
import exh.isNamespaceSource
|
||||||
|
import exh.metadata.metadata.base.RaisedTag
|
||||||
|
import exh.util.SourceTagsUtil.Companion.TAG_TYPE_EXCLUDE
|
||||||
|
import exh.util.SourceTagsUtil.Companion.getRaisedTags
|
||||||
|
import exh.util.SourceTagsUtil.Companion.parseTag
|
||||||
import kotlinx.android.synthetic.main.source_compact_grid_item.view.card
|
import kotlinx.android.synthetic.main.source_compact_grid_item.view.card
|
||||||
import kotlinx.android.synthetic.main.source_compact_grid_item.view.gradient
|
import kotlinx.android.synthetic.main.source_compact_grid_item.view.gradient
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
class LibraryItem(val manga: LibraryManga, private val libraryDisplayMode: Preference<DisplayMode>) :
|
class LibraryItem(val manga: LibraryManga, private val libraryDisplayMode: Preference<DisplayMode>) :
|
||||||
AbstractFlexibleItem<LibraryHolder>(), IFilterable<String> {
|
AbstractFlexibleItem<LibraryHolder>(), IFilterable<Pair<String, Boolean>> {
|
||||||
|
|
||||||
private val sourceManager: SourceManager = Injekt.get()
|
private val sourceManager: SourceManager = Injekt.get()
|
||||||
// SY -->
|
// SY -->
|
||||||
private val trackManager: TrackManager = Injekt.get()
|
private val trackManager: TrackManager = Injekt.get()
|
||||||
private val db: DatabaseHelper = Injekt.get()
|
private val db: DatabaseHelper = Injekt.get()
|
||||||
|
private val source by lazy {
|
||||||
|
sourceManager.get(manga.source)
|
||||||
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
var downloadCount = -1
|
var downloadCount = -1
|
||||||
@ -96,38 +104,13 @@ class LibraryItem(val manga: LibraryManga, private val libraryDisplayMode: Prefe
|
|||||||
* @param constraint the query to apply.
|
* @param constraint the query to apply.
|
||||||
* @return true if the manga should be included, false otherwise.
|
* @return true if the manga should be included, false otherwise.
|
||||||
*/
|
*/
|
||||||
override fun filter(constraint: String): Boolean {
|
override fun filter(constraint: Pair<String, Boolean>): Boolean {
|
||||||
return manga.title.contains(constraint, true) ||
|
return manga.title.contains(constraint.first, true) ||
|
||||||
(manga.author?.contains(constraint, true) ?: false) ||
|
(manga.author?.contains(constraint.first, true) ?: false) ||
|
||||||
(manga.artist?.contains(constraint, true) ?: false) ||
|
(manga.artist?.contains(constraint.first, true) ?: false) ||
|
||||||
sourceManager.getOrStub(manga.source).name.contains(constraint, true) ||
|
(source?.name?.contains(constraint.first, true) ?: false) ||
|
||||||
(Injekt.get<TrackManager>().hasLoggedServices() && filterTracks(constraint, db.getTracks(manga).executeAsBlocking())) ||
|
(Injekt.get<TrackManager>().hasLoggedServices() && filterTracks(constraint.first, db.getTracks(manga).executeAsBlocking())) ||
|
||||||
if (constraint.contains(" ") || constraint.contains("\"")) {
|
constraint.second && ehContainsGenre(constraint.first)
|
||||||
val genres = manga.genre?.split(", ")?.map {
|
|
||||||
it.drop(it.indexOfFirst { it == ':' } + 1).toLowerCase().trim() // tachiEH tag namespaces
|
|
||||||
}
|
|
||||||
var clean_constraint = ""
|
|
||||||
var ignorespace = false
|
|
||||||
for (i in constraint.trim().toLowerCase()) {
|
|
||||||
if (i == ' ') {
|
|
||||||
if (!ignorespace) {
|
|
||||||
clean_constraint = clean_constraint + ","
|
|
||||||
} else {
|
|
||||||
clean_constraint = clean_constraint + " "
|
|
||||||
}
|
|
||||||
} else if (i == '"') {
|
|
||||||
ignorespace = !ignorespace
|
|
||||||
} else {
|
|
||||||
clean_constraint = clean_constraint + Character.toString(i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
clean_constraint.split(",").all { containsGenre(it.trim(), genres) }
|
|
||||||
} else containsGenre(
|
|
||||||
constraint,
|
|
||||||
manga.genre?.split(", ")?.map {
|
|
||||||
it.drop(it.indexOfFirst { it == ':' } + 1).toLowerCase().trim() // tachiEH tag namespaces
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun filterTracks(constraint: String, tracks: List<Track>): Boolean {
|
private fun filterTracks(constraint: String, tracks: List<Track>): Boolean {
|
||||||
@ -141,6 +124,54 @@ class LibraryItem(val manga: LibraryManga, private val libraryDisplayMode: Prefe
|
|||||||
return@any false
|
return@any false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun ehContainsGenre(constraint: String): Boolean {
|
||||||
|
val genres = manga.getGenres()
|
||||||
|
val raisedTags = if (source?.isNamespaceSource() == true) {
|
||||||
|
manga.getRaisedTags(genres)
|
||||||
|
} else null
|
||||||
|
return if (constraint.contains(" ") || constraint.contains("\"")) {
|
||||||
|
var cleanConstraint = ""
|
||||||
|
var ignoreSpace = false
|
||||||
|
for (i in constraint.trim().toLowerCase()) {
|
||||||
|
when (i) {
|
||||||
|
' ' -> {
|
||||||
|
cleanConstraint = if (!ignoreSpace) {
|
||||||
|
"$cleanConstraint,"
|
||||||
|
} else {
|
||||||
|
"$cleanConstraint "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'"' -> {
|
||||||
|
ignoreSpace = !ignoreSpace
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
cleanConstraint += i.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cleanConstraint.split(",").all {
|
||||||
|
if (raisedTags == null) containsGenre(it.trim(), genres) else containsRaisedGenre(
|
||||||
|
parseTag(it.trim()), raisedTags
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if (raisedTags == null) {
|
||||||
|
containsGenre(constraint, genres)
|
||||||
|
} else {
|
||||||
|
containsRaisedGenre(parseTag(constraint), raisedTags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun containsRaisedGenre(tag: RaisedTag, genres: List<RaisedTag>): Boolean {
|
||||||
|
val genre = genres.find {
|
||||||
|
(it.namespace?.toLowerCase() == tag.namespace?.toLowerCase() && it.name.toLowerCase() == tag.name.toLowerCase())
|
||||||
|
}
|
||||||
|
return if (tag.type == TAG_TYPE_EXCLUDE) {
|
||||||
|
genre == null
|
||||||
|
} else {
|
||||||
|
genre != null
|
||||||
|
}
|
||||||
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
private fun containsGenre(tag: String, genres: List<String>?): Boolean {
|
private fun containsGenre(tag: String, genres: List<String>?): Boolean {
|
||||||
|
@ -7,7 +7,7 @@ import exh.metadata.sql.tables.SearchTitleTable
|
|||||||
class SearchEngine {
|
class SearchEngine {
|
||||||
private val queryCache = mutableMapOf<String, List<QueryComponent>>()
|
private val queryCache = mutableMapOf<String, List<QueryComponent>>()
|
||||||
|
|
||||||
fun textToSubQueries(
|
private fun textToSubQueries(
|
||||||
namespace: String?,
|
namespace: String?,
|
||||||
component: Text?
|
component: Text?
|
||||||
): Pair<String, List<String>>? {
|
): Pair<String, List<String>>? {
|
||||||
@ -20,42 +20,46 @@ class SearchEngine {
|
|||||||
}
|
}
|
||||||
val componentTagQuery = maybeLenientComponent?.let {
|
val componentTagQuery = maybeLenientComponent?.let {
|
||||||
val params = mutableListOf<String>()
|
val params = mutableListOf<String>()
|
||||||
it.map { q ->
|
it.joinToString(separator = " OR ", prefix = "(", postfix = ")") { q ->
|
||||||
params += q
|
params += q
|
||||||
"${SearchTagTable.TABLE}.${SearchTagTable.COL_NAME} LIKE ?"
|
"${SearchTagTable.TABLE}.${SearchTagTable.COL_NAME} LIKE ?"
|
||||||
}.joinToString(separator = " OR ", prefix = "(", postfix = ")") to params
|
} to params
|
||||||
}
|
}
|
||||||
return if (namespace != null) {
|
return when {
|
||||||
var query =
|
namespace != null -> {
|
||||||
"""
|
var query =
|
||||||
(SELECT ${SearchTagTable.COL_MANGA_ID} AS $COL_MANGA_ID FROM ${SearchTagTable.TABLE}
|
"""
|
||||||
WHERE ${SearchTagTable.COL_NAMESPACE} IS NOT NULL
|
(SELECT ${SearchTagTable.COL_MANGA_ID} AS $COL_MANGA_ID FROM ${SearchTagTable.TABLE}
|
||||||
AND ${SearchTagTable.COL_NAMESPACE} LIKE ?
|
WHERE ${SearchTagTable.COL_NAMESPACE} IS NOT NULL
|
||||||
""".trimIndent()
|
AND ${SearchTagTable.COL_NAMESPACE} LIKE ?
|
||||||
val params = mutableListOf(escapeLike(namespace))
|
""".trimIndent()
|
||||||
if (componentTagQuery != null) {
|
val params = mutableListOf(escapeLike(namespace))
|
||||||
query += "\n AND ${componentTagQuery.first}"
|
if (componentTagQuery != null) {
|
||||||
params += componentTagQuery.second
|
query += "\n AND ${componentTagQuery.first}"
|
||||||
|
params += componentTagQuery.second
|
||||||
|
}
|
||||||
|
|
||||||
|
"$query)" to params
|
||||||
}
|
}
|
||||||
|
component != null -> {
|
||||||
|
// Match title + tags
|
||||||
|
val tagQuery =
|
||||||
|
"""
|
||||||
|
SELECT ${SearchTagTable.COL_MANGA_ID} AS $COL_MANGA_ID FROM ${SearchTagTable.TABLE}
|
||||||
|
WHERE ${componentTagQuery!!.first}
|
||||||
|
""".trimIndent() to componentTagQuery.second
|
||||||
|
|
||||||
"$query)" to params
|
val titleQuery =
|
||||||
} else if (component != null) {
|
"""
|
||||||
// Match title + tags
|
SELECT ${SearchTitleTable.COL_MANGA_ID} AS $COL_MANGA_ID FROM ${SearchTitleTable.TABLE}
|
||||||
val tagQuery =
|
WHERE ${SearchTitleTable.COL_TITLE} LIKE ?
|
||||||
"""
|
""".trimIndent() to listOf(component.asLenientTitleQuery())
|
||||||
SELECT ${SearchTagTable.COL_MANGA_ID} AS $COL_MANGA_ID FROM ${SearchTagTable.TABLE}
|
|
||||||
WHERE ${componentTagQuery!!.first}
|
|
||||||
""".trimIndent() to componentTagQuery.second
|
|
||||||
|
|
||||||
val titleQuery =
|
"(${tagQuery.first} UNION ${titleQuery.first})".trimIndent() to
|
||||||
"""
|
(tagQuery.second + titleQuery.second)
|
||||||
SELECT ${SearchTitleTable.COL_MANGA_ID} AS $COL_MANGA_ID FROM ${SearchTitleTable.TABLE}
|
}
|
||||||
WHERE ${SearchTitleTable.COL_TITLE} LIKE ?
|
else -> null
|
||||||
""".trimIndent() to listOf(component.asLenientTitleQuery())
|
}
|
||||||
|
|
||||||
"(${tagQuery.first} UNION ${titleQuery.first})".trimIndent() to
|
|
||||||
(tagQuery.second + titleQuery.second)
|
|
||||||
} else null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun queryToSql(q: List<QueryComponent>): Pair<String, List<String>> {
|
fun queryToSql(q: List<QueryComponent>): Pair<String, List<String>> {
|
||||||
@ -158,7 +162,7 @@ class SearchEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (char in query.toLowerCase()) {
|
query.toLowerCase().forEach { char ->
|
||||||
if (char == '"') {
|
if (char == '"') {
|
||||||
inQuotes = !inQuotes
|
inQuotes = !inQuotes
|
||||||
} else if (enableWildcard && (char == '?' || char == '_')) {
|
} else if (enableWildcard && (char == '?' || char == '_')) {
|
||||||
@ -167,7 +171,7 @@ class SearchEngine {
|
|||||||
} else if (enableWildcard && (char == '*' || char == '%')) {
|
} else if (enableWildcard && (char == '*' || char == '%')) {
|
||||||
flushText()
|
flushText()
|
||||||
queuedText.add(MultiWildcard(char.toString()))
|
queuedText.add(MultiWildcard(char.toString()))
|
||||||
} else if (char == '-') {
|
} else if (char == '-' && !inQuotes && (queuedRawText.isBlank() || queuedRawText.last() == ' ')) {
|
||||||
nextIsExcluded = true
|
nextIsExcluded = true
|
||||||
} else if (char == '$') {
|
} else if (char == '$') {
|
||||||
nextIsExact = true
|
nextIsExact = true
|
||||||
|
@ -48,9 +48,21 @@ class SourceTagsUtil {
|
|||||||
"$namespace:$tag"
|
"$namespace:$tag"
|
||||||
}
|
}
|
||||||
companion object {
|
companion object {
|
||||||
fun Manga.getRaisedTags(): List<RaisedTag>? = this.getGenres()?.map { parseTag(it) }
|
fun Manga.getRaisedTags(genres: List<String>? = null): List<RaisedTag>? = (genres ?: this.getGenres())?.map { parseTag(it) }
|
||||||
|
|
||||||
fun parseTag(tag: String) = RaisedTag(tag.substringBefore(':').trimOrNull(), (tag.substringAfter(':').trimOrNull() ?: tag), TAG_TYPE_DEFAULT)
|
fun parseTag(tag: String) = RaisedTag(
|
||||||
|
(
|
||||||
|
if (tag.startsWith("-")) {
|
||||||
|
tag.substringAfter("-")
|
||||||
|
} else {
|
||||||
|
tag
|
||||||
|
}
|
||||||
|
).substringBefore(':', missingDelimiterValue = "").trimOrNull(),
|
||||||
|
tag.substringAfter(':', missingDelimiterValue = tag).trim(),
|
||||||
|
if (tag.startsWith("-")) TAG_TYPE_EXCLUDE else TAG_TYPE_DEFAULT
|
||||||
|
)
|
||||||
|
|
||||||
|
const val TAG_TYPE_EXCLUDE = 69 // why not
|
||||||
|
|
||||||
const val DOUJINSHI_COLOR = "#f44336"
|
const val DOUJINSHI_COLOR = "#f44336"
|
||||||
const val MANGA_COLOR = "#ff9800"
|
const val MANGA_COLOR = "#ff9800"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user