fix: 搜索问题
This commit is contained in:
@@ -14,6 +14,7 @@ val Context.localDataStore: DataStore<Preferences> by preferencesDataStore(name
|
||||
object LocalPreferencesKeys {
|
||||
val SHOW_THEME_REFACTOR_TIP = booleanPreferencesKey("show_theme_refactor_tip")
|
||||
val SEARCH_LAYOUT_MODE = intPreferencesKey("search_layout_mode")
|
||||
val SEARCH_SCOPE = stringPreferencesKey("search_scope")
|
||||
val MATCH_MODE = intPreferencesKey("match_mode")
|
||||
val EXPLORE_LAYOUT_MODE = intPreferencesKey("explore_layout_mode")
|
||||
val EXPLORE_LAYOUT_GRID_PORTRAIT = intPreferencesKey("explore_layout_grid_portrait")
|
||||
|
||||
@@ -1,40 +1,130 @@
|
||||
package io.legado.app.domain.model
|
||||
|
||||
import com.google.gson.GsonBuilder
|
||||
import io.legado.app.utils.fromJsonObject
|
||||
import io.legado.app.utils.splitNotBlank
|
||||
|
||||
data class BookSearchScope(val raw: String) {
|
||||
|
||||
val items: List<String>
|
||||
get() = raw.splitNotBlank(",").toList()
|
||||
|
||||
val isAll: Boolean
|
||||
get() = raw.isEmpty()
|
||||
|
||||
val isSource: Boolean
|
||||
get() = items.isNotEmpty() && parseSourceItems().size == items.size
|
||||
|
||||
val groupNames: List<String>
|
||||
get() = if (isSource) emptyList() else items
|
||||
|
||||
val sourceUrls: List<String>
|
||||
get() = parseSourceItems().map { it.url }
|
||||
|
||||
private fun parseSourceItems(): List<ScopeSourceItem> {
|
||||
return items.mapNotNull { item ->
|
||||
val splitIndex = item.indexOf("::")
|
||||
if (splitIndex <= 0 || splitIndex >= item.lastIndex) {
|
||||
null
|
||||
} else {
|
||||
ScopeSourceItem(
|
||||
name = item.substring(0, splitIndex),
|
||||
url = item.substring(splitIndex + 2)
|
||||
)
|
||||
}
|
||||
}
|
||||
private val parsed: ParsedSearchScope by lazy {
|
||||
parse(raw)
|
||||
}
|
||||
|
||||
private data class ScopeSourceItem(
|
||||
val items: List<String>
|
||||
get() = when {
|
||||
isSource -> sourceUrls
|
||||
else -> groupNames
|
||||
}
|
||||
|
||||
val isAll: Boolean
|
||||
get() = parsed.isAll
|
||||
|
||||
val isSource: Boolean
|
||||
get() = parsed.sources.isNotEmpty()
|
||||
|
||||
val groupNames: List<String>
|
||||
get() = parsed.groups
|
||||
|
||||
val sourceUrls: List<String>
|
||||
get() = parsed.sources.map { it.url }
|
||||
|
||||
val sourceNames: List<String>
|
||||
get() = parsed.sources.map { it.name }
|
||||
|
||||
private data class ParsedSearchScope(
|
||||
val groups: List<String> = emptyList(),
|
||||
val sources: List<ScopeSourceItem> = emptyList(),
|
||||
) {
|
||||
val isAll: Boolean
|
||||
get() = groups.isEmpty() && sources.isEmpty()
|
||||
}
|
||||
|
||||
data class ScopeSourceItem(
|
||||
val name: String,
|
||||
val url: String,
|
||||
)
|
||||
|
||||
private data class SerializedSearchScope(
|
||||
val type: String = "",
|
||||
val groups: List<String> = emptyList(),
|
||||
val sources: List<ScopeSourceItem> = emptyList(),
|
||||
)
|
||||
|
||||
companion object {
|
||||
|
||||
fun encodeGroups(groups: List<String>): String {
|
||||
val selected = groups.filter { it.isNotBlank() }
|
||||
return if (selected.isEmpty()) {
|
||||
""
|
||||
} else {
|
||||
scopeGson.toJson(SerializedSearchScope(type = TYPE_GROUP, groups = selected))
|
||||
}
|
||||
}
|
||||
|
||||
fun encodeSources(sources: List<ScopeSourceItem>): String {
|
||||
val selected = sources.filter { it.url.isNotBlank() }
|
||||
return if (selected.isEmpty()) {
|
||||
""
|
||||
} else {
|
||||
scopeGson.toJson(SerializedSearchScope(type = TYPE_SOURCE, sources = selected))
|
||||
}
|
||||
}
|
||||
|
||||
private fun parse(raw: String): ParsedSearchScope {
|
||||
if (raw.isEmpty()) return ParsedSearchScope()
|
||||
|
||||
parseJson(raw)?.let {
|
||||
return it
|
||||
}
|
||||
|
||||
return parseLegacy(raw)
|
||||
}
|
||||
|
||||
private fun parseJson(raw: String): ParsedSearchScope? {
|
||||
val json = raw.trim()
|
||||
if (!json.startsWith("{") || !json.endsWith("}")) return null
|
||||
|
||||
return scopeGson.fromJsonObject<SerializedSearchScope>(json).getOrNull()?.let { scope ->
|
||||
when (scope.type) {
|
||||
TYPE_SOURCE -> ParsedSearchScope(
|
||||
sources = scope.sources.filter { it.url.isNotBlank() }
|
||||
)
|
||||
|
||||
TYPE_GROUP -> ParsedSearchScope(
|
||||
groups = scope.groups.filter { it.isNotBlank() }
|
||||
)
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseLegacy(raw: String): ParsedSearchScope {
|
||||
val rawItems = raw.split(",").filter { it.isNotBlank() }
|
||||
val sourceItems = parseLegacySourceItems(rawItems)
|
||||
if (rawItems.isNotEmpty() && sourceItems.size == rawItems.size) {
|
||||
return ParsedSearchScope(sources = sourceItems)
|
||||
}
|
||||
return ParsedSearchScope(groups = raw.splitNotBlank(",").toList())
|
||||
}
|
||||
|
||||
private fun parseLegacySourceItems(items: List<String>): List<ScopeSourceItem> {
|
||||
return items.mapNotNull { item ->
|
||||
val splitIndex = item.indexOf("::")
|
||||
if (splitIndex <= 0 || splitIndex >= item.lastIndex) {
|
||||
null
|
||||
} else {
|
||||
ScopeSourceItem(
|
||||
name = item.substring(0, splitIndex),
|
||||
url = item.substring(splitIndex + 2)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val TYPE_GROUP = "group"
|
||||
private const val TYPE_SOURCE = "source"
|
||||
private val scopeGson = GsonBuilder().disableHtmlEscaping().create()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -634,18 +634,6 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
val paddingDisplayCutouts
|
||||
get() = ReadConfig.paddingDisplayCutouts
|
||||
|
||||
var searchScope: String
|
||||
get() = appCtx.getPrefString("searchScope") ?: ""
|
||||
set(value) {
|
||||
appCtx.putPrefString("searchScope", value)
|
||||
}
|
||||
|
||||
var searchGroup: String
|
||||
get() = appCtx.getPrefString("searchGroup") ?: ""
|
||||
set(value) {
|
||||
appCtx.putPrefString("searchGroup", value)
|
||||
}
|
||||
|
||||
var pageTouchSlop: Int
|
||||
get() = ReadConfig.pageTouchSlop
|
||||
set(value) {
|
||||
|
||||
@@ -5,8 +5,8 @@ import io.legado.app.R
|
||||
import io.legado.app.data.appDb
|
||||
import io.legado.app.data.entities.BookSource
|
||||
import io.legado.app.data.entities.BookSourcePart
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.utils.splitNotBlank
|
||||
import io.legado.app.domain.model.BookSearchScope
|
||||
import io.legado.app.domain.model.BookSearchScope.ScopeSourceItem
|
||||
import splitties.init.appCtx
|
||||
|
||||
/**
|
||||
@@ -15,14 +15,14 @@ import splitties.init.appCtx
|
||||
@Suppress("unused")
|
||||
data class SearchScope(private var scope: String) {
|
||||
|
||||
constructor(groups: List<String>) : this(groups.joinToString(","))
|
||||
constructor(groups: List<String>) : this(BookSearchScope.encodeGroups(groups))
|
||||
|
||||
constructor(source: BookSource) : this(
|
||||
encodeSourceToken(source.bookSourceName, source.bookSourceUrl)
|
||||
encodeSourceScope(source.bookSourceName, source.bookSourceUrl)
|
||||
)
|
||||
|
||||
constructor(source: BookSourcePart) : this(
|
||||
encodeSourceToken(source.bookSourceName, source.bookSourceUrl)
|
||||
encodeSourceScope(source.bookSourceName, source.bookSourceUrl)
|
||||
)
|
||||
|
||||
override fun toString(): String {
|
||||
@@ -34,43 +34,36 @@ data class SearchScope(private var scope: String) {
|
||||
fun update(scope: String, postValue: Boolean = true) {
|
||||
this.scope = scope
|
||||
if (postValue) stateLiveData.postValue(scope)
|
||||
save()
|
||||
}
|
||||
|
||||
fun update(groups: List<String>) {
|
||||
scope = groups.joinToString(",")
|
||||
scope = BookSearchScope.encodeGroups(groups)
|
||||
stateLiveData.postValue(scope)
|
||||
save()
|
||||
}
|
||||
|
||||
fun update(source: BookSource) {
|
||||
scope = encodeSourceToken(source.bookSourceName, source.bookSourceUrl)
|
||||
scope = encodeSourceScope(source.bookSourceName, source.bookSourceUrl)
|
||||
stateLiveData.postValue(scope)
|
||||
save()
|
||||
}
|
||||
|
||||
fun update(source: BookSourcePart) {
|
||||
scope = encodeSourceToken(source.bookSourceName, source.bookSourceUrl)
|
||||
scope = encodeSourceScope(source.bookSourceName, source.bookSourceUrl)
|
||||
stateLiveData.postValue(scope)
|
||||
save()
|
||||
}
|
||||
|
||||
fun updateSources(sources: List<BookSourcePart>) {
|
||||
scope = encodeSourceScope(sources)
|
||||
stateLiveData.postValue(scope)
|
||||
save()
|
||||
}
|
||||
|
||||
fun isSource(): Boolean {
|
||||
val items = scopeItems()
|
||||
if (items.isEmpty()) return false
|
||||
return parseSourceItems(items).size == items.size
|
||||
return parsedScope().isSource
|
||||
}
|
||||
|
||||
val display: String
|
||||
get() {
|
||||
if (isSource()) {
|
||||
val sourceNames = parseSourceItems().map { it.name }
|
||||
val sourceNames = parsedScope().sourceNames
|
||||
if (sourceNames.isEmpty()) return appCtx.getString(R.string.all_source)
|
||||
return sourceNames.joinToString(",")
|
||||
}
|
||||
@@ -87,40 +80,26 @@ data class SearchScope(private var scope: String) {
|
||||
get() {
|
||||
val list = arrayListOf<String>()
|
||||
if (isSource()) {
|
||||
parseSourceItems().forEach {
|
||||
list.add(it.name)
|
||||
}
|
||||
list.addAll(parsedScope().sourceNames)
|
||||
} else {
|
||||
scopeItems().forEach {
|
||||
list.add(it)
|
||||
}
|
||||
list.addAll(parsedScope().groupNames)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
val sourceUrls: List<String>
|
||||
get() = parseSourceItems().map { it.url }
|
||||
get() = parsedScope().sourceUrls
|
||||
|
||||
fun remove(scope: String) {
|
||||
if (isSource()) {
|
||||
val sourceItems = parseSourceItems().filterNot {
|
||||
val sourceItems = sourceItems().filterNot {
|
||||
it.name == scope || it.url == scope
|
||||
}
|
||||
this.scope = sourceItems.joinToString(",") { "${it.name}::${it.url}" }
|
||||
this.scope = BookSearchScope.encodeSources(sourceItems)
|
||||
} else {
|
||||
val stringBuilder = StringBuilder()
|
||||
scopeItems().forEach {
|
||||
if (it != scope) {
|
||||
if (stringBuilder.isNotEmpty()) {
|
||||
stringBuilder.append(",")
|
||||
}
|
||||
stringBuilder.append(it)
|
||||
}
|
||||
}
|
||||
this.scope = stringBuilder.toString()
|
||||
this.scope = BookSearchScope.encodeGroups(displayNames.filterNot { it == scope })
|
||||
}
|
||||
stateLiveData.postValue(this.scope)
|
||||
save()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -132,13 +111,13 @@ data class SearchScope(private var scope: String) {
|
||||
list.addAll(appDb.bookSourceDao.allEnabledPart)
|
||||
} else {
|
||||
if (isSource()) {
|
||||
parseSourceItems().forEach { sourceItem ->
|
||||
sourceItems().forEach { sourceItem ->
|
||||
appDb.bookSourceDao.getBookSourcePart(sourceItem.url)?.let { source ->
|
||||
list.add(source)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val oldScope = scopeItems()
|
||||
val oldScope = parsedScope().groupNames
|
||||
val newScope = oldScope.filter {
|
||||
val bookSources = appDb.bookSourceDao.getEnabledPartByGroup(it)
|
||||
list.addAll(bookSources)
|
||||
@@ -163,54 +142,38 @@ data class SearchScope(private var scope: String) {
|
||||
}
|
||||
|
||||
fun isAll(): Boolean {
|
||||
return scope.isEmpty()
|
||||
return parsedScope().isAll
|
||||
}
|
||||
|
||||
fun save() {
|
||||
AppConfig.searchScope = scope
|
||||
if (isAll() || isSource() || scope.contains(",")) {
|
||||
AppConfig.searchGroup = ""
|
||||
} else {
|
||||
AppConfig.searchGroup = scope
|
||||
private fun parsedScope(): BookSearchScope = BookSearchScope(scope)
|
||||
|
||||
private fun sourceItems(): List<ScopeSourceItem> {
|
||||
val parsed = parsedScope()
|
||||
return parsed.sourceNames.zip(parsed.sourceUrls) { name, url ->
|
||||
ScopeSourceItem(name, url)
|
||||
}
|
||||
}
|
||||
|
||||
private fun scopeItems(): List<String> = scope.splitNotBlank(",").toList()
|
||||
|
||||
private fun parseSourceItems(
|
||||
items: List<String> = scopeItems()
|
||||
): List<ScopeSourceItem> {
|
||||
return items.mapNotNull { item ->
|
||||
val splitIndex = item.indexOf("::")
|
||||
if (splitIndex <= 0 || splitIndex >= item.lastIndex) {
|
||||
null
|
||||
} else {
|
||||
ScopeSourceItem(
|
||||
name = item.substring(0, splitIndex),
|
||||
url = item.substring(splitIndex + 2)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class ScopeSourceItem(
|
||||
val name: String,
|
||||
val url: String,
|
||||
)
|
||||
|
||||
companion object {
|
||||
private fun sanitizeSourceName(name: String): String {
|
||||
return name.replace(":", "").replace(",", "")
|
||||
}
|
||||
|
||||
private fun encodeSourceToken(name: String, url: String): String {
|
||||
return "${sanitizeSourceName(name)}::${url}"
|
||||
private fun encodeSourceScope(name: String, url: String): String {
|
||||
return BookSearchScope.encodeSources(
|
||||
listOf(ScopeSourceItem(sanitizeSourceName(name), url))
|
||||
)
|
||||
}
|
||||
|
||||
private fun encodeSourceScope(sources: List<BookSourcePart>): String {
|
||||
return sources.joinToString(",") { source ->
|
||||
encodeSourceToken(source.bookSourceName, source.bookSourceUrl)
|
||||
}
|
||||
return BookSearchScope.encodeSources(
|
||||
sources.map { source ->
|
||||
ScopeSourceItem(
|
||||
name = sanitizeSourceName(source.bookSourceName),
|
||||
url = source.bookSourceUrl
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ import io.legado.app.domain.usecase.ExploreBooksUseCase
|
||||
import io.legado.app.domain.usecase.ResolveBookShelfStateUseCase
|
||||
import io.legado.app.domain.usecase.SearchBooksUseCase
|
||||
import io.legado.app.domain.usecase.SearchRunEvent
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.ui.config.otherConfig.OtherConfig
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
@@ -69,10 +68,10 @@ class SearchViewModel(
|
||||
|
||||
private val _uiState = MutableStateFlow(
|
||||
SearchUiState(
|
||||
scopeDisplay = SearchScope(AppConfig.searchScope).display,
|
||||
scopeDisplayNames = SearchScope(AppConfig.searchScope).displayNames.toImmutableList(),
|
||||
isAllScope = SearchScope(AppConfig.searchScope).isAll(),
|
||||
isSourceScope = SearchScope(AppConfig.searchScope).isSource(),
|
||||
scopeDisplay = SearchScope("").display,
|
||||
scopeDisplayNames = SearchScope("").displayNames.toImmutableList(),
|
||||
isAllScope = SearchScope("").isAll(),
|
||||
isSourceScope = SearchScope("").isSource(),
|
||||
)
|
||||
)
|
||||
val uiState = _uiState.asStateFlow()
|
||||
@@ -82,7 +81,9 @@ class SearchViewModel(
|
||||
|
||||
private val queryFlow = MutableStateFlow("")
|
||||
private val bookshelfKeys = MutableStateFlow<Set<BookShelfKey>>(emptySet())
|
||||
private val searchScope = SearchScope(AppConfig.searchScope)
|
||||
private var persistedSearchScopeRaw = ""
|
||||
private var hasTemporaryScope = false
|
||||
private val searchScope = SearchScope("")
|
||||
private val searchControl = BookSearchControl()
|
||||
private val searchResultBooks = LinkedHashMap<SearchResultKey, SearchBook>()
|
||||
|
||||
@@ -93,6 +94,7 @@ class SearchViewModel(
|
||||
|
||||
init {
|
||||
syncScopeState()
|
||||
observeSearchScope()
|
||||
observeEnabledGroups()
|
||||
observeEnabledSources()
|
||||
observeBookshelf()
|
||||
@@ -205,6 +207,7 @@ class SearchViewModel(
|
||||
SearchIntent.SelectAllScope -> {
|
||||
val oldScope = searchScope.toString()
|
||||
searchScope.update("")
|
||||
persistSearchScope()
|
||||
syncScopeState(restartSearch = true, oldScope = oldScope)
|
||||
}
|
||||
|
||||
@@ -213,6 +216,7 @@ class SearchViewModel(
|
||||
is SearchIntent.RemoveScopeItem -> {
|
||||
val oldScope = searchScope.toString()
|
||||
searchScope.remove(intent.scopeName)
|
||||
persistSearchScope()
|
||||
syncScopeState(restartSearch = true, oldScope = oldScope)
|
||||
}
|
||||
|
||||
@@ -313,10 +317,15 @@ class SearchViewModel(
|
||||
}
|
||||
|
||||
private fun initialize(key: String?, scopeRaw: String?) {
|
||||
scopeRaw?.let {
|
||||
searchScope.update(it, postValue = false)
|
||||
if (scopeRaw != null) {
|
||||
hasTemporaryScope = true
|
||||
searchScope.update(scopeRaw, postValue = false)
|
||||
syncScopeState()
|
||||
} else if (hasTemporaryScope) {
|
||||
hasTemporaryScope = false
|
||||
searchScope.update(persistedSearchScopeRaw, postValue = false)
|
||||
syncScopeState()
|
||||
}
|
||||
syncScopeState()
|
||||
|
||||
// When the ViewModel already holds a non-empty committed query,
|
||||
// it means a search session is in progress or completed.
|
||||
@@ -613,6 +622,7 @@ class SearchViewModel(
|
||||
selected.add(groupName)
|
||||
}
|
||||
searchScope.update(selected.toList())
|
||||
persistSearchScope()
|
||||
syncScopeState(restartSearch = true, oldScope = oldScope)
|
||||
}
|
||||
|
||||
@@ -638,6 +648,7 @@ class SearchViewModel(
|
||||
}
|
||||
searchScope.updateSources(selectedSources)
|
||||
}
|
||||
persistSearchScope()
|
||||
syncScopeState(restartSearch = true, oldScope = oldScope)
|
||||
}
|
||||
|
||||
@@ -653,6 +664,7 @@ class SearchViewModel(
|
||||
}
|
||||
} else {
|
||||
searchScope.update("")
|
||||
persistSearchScope()
|
||||
syncScopeState()
|
||||
}
|
||||
|
||||
@@ -685,6 +697,32 @@ class SearchViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeSearchScope() {
|
||||
viewModelScope.launch {
|
||||
localPreferencesRepository
|
||||
.getPreference(LocalPreferencesKeys.SEARCH_SCOPE, "")
|
||||
.distinctUntilChanged()
|
||||
.collect { scopeRaw ->
|
||||
persistedSearchScopeRaw = scopeRaw
|
||||
if (!hasTemporaryScope && scopeRaw != searchScope.toString()) {
|
||||
searchScope.update(scopeRaw, postValue = false)
|
||||
syncScopeState()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun persistSearchScope() {
|
||||
hasTemporaryScope = false
|
||||
persistedSearchScopeRaw = searchScope.toString()
|
||||
viewModelScope.launch {
|
||||
localPreferencesRepository.updatePreference(
|
||||
LocalPreferencesKeys.SEARCH_SCOPE,
|
||||
searchScope.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<SearchBook>.toSearchResultItems(
|
||||
shelf: Set<BookShelfKey>
|
||||
): List<SearchResultItemUi> {
|
||||
|
||||
@@ -15,7 +15,6 @@ import io.legado.app.domain.usecase.BookSearchControl
|
||||
import io.legado.app.domain.usecase.BookSearchRequest
|
||||
import io.legado.app.domain.usecase.SearchBooksUseCase
|
||||
import io.legado.app.domain.usecase.SearchRunEvent
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.ui.config.otherConfig.OtherConfig
|
||||
import io.legado.app.utils.GSON
|
||||
import io.legado.app.utils.fromJsonObject
|
||||
@@ -82,7 +81,11 @@ class BookSearchWebSocket(private val session: DefaultWebSocketServerSession) :
|
||||
BookSearchRequest(
|
||||
keyword = key,
|
||||
page = 1,
|
||||
scope = BookSearchScope(AppConfig.searchScope),
|
||||
scope = BookSearchScope(
|
||||
localPreferencesRepository
|
||||
.getPreference(LocalPreferencesKeys.SEARCH_SCOPE, "")
|
||||
.first()
|
||||
),
|
||||
matchMode = MatchMode.of(
|
||||
localPreferencesRepository
|
||||
.getPreference(
|
||||
|
||||
Reference in New Issue
Block a user