fix: 修复换源配置卡死

This commit is contained in:
HapeLee committed 2026-06-14 16:43:08 +08:00
1 parent 7c9b08d21a
commit 4ad6b1396b
2 files changed
+76 -54

No files matched your search

@@ -1,8 +1,15 @@
package io.legado.app.domain.model package io.legado.app.domain.model
import io.legado.app.utils.splitNotBlank import io.legado.app.utils.splitNotBlank
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonArray
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
data class BookSearchScope(val raw: String) { data class BookSearchScope(val raw: String) {
@@ -39,19 +46,11 @@ data class BookSearchScope(val raw: String) {
get() = groups.isEmpty() && sources.isEmpty() get() = groups.isEmpty() && sources.isEmpty()
} }
@Serializable
data class ScopeSourceItem( data class ScopeSourceItem(
val name: String, val name: String,
val url: String, val url: String,
) )
@Serializable
private data class SerializedSearchScope(
val type: String = "",
val groups: List<String> = emptyList(),
val sources: List<ScopeSourceItem> = emptyList(),
)
companion object { companion object {
fun encodeGroups(groups: List<String>): String { fun encodeGroups(groups: List<String>): String {
@@ -59,9 +58,12 @@ data class BookSearchScope(val raw: String) {
return if (selected.isEmpty()) { return if (selected.isEmpty()) {
"" ""
} else { } else {
scopeJson.encodeToString( buildJsonObject {
SerializedSearchScope(type = TYPE_GROUP, groups = selected) put("type", JsonPrimitive(TYPE_GROUP))
) put("groups", buildJsonArray {
selected.forEach { add(JsonPrimitive(it)) }
})
}.toString()
} }
} }
@@ -70,9 +72,17 @@ data class BookSearchScope(val raw: String) {
return if (selected.isEmpty()) { return if (selected.isEmpty()) {
"" ""
} else { } else {
scopeJson.encodeToString( buildJsonObject {
SerializedSearchScope(type = TYPE_SOURCE, sources = selected) put("type", JsonPrimitive(TYPE_SOURCE))
) put("sources", buildJsonArray {
selected.forEach { source ->
add(buildJsonObject {
put("name", JsonPrimitive(source.name))
put("url", JsonPrimitive(source.url))
})
}
})
}.toString()
} }
} }
@@ -91,15 +101,22 @@ data class BookSearchScope(val raw: String) {
if (!json.startsWith("{") || !json.endsWith("}")) return null if (!json.startsWith("{") || !json.endsWith("}")) return null
return runCatching { return runCatching {
scopeJson.decodeFromString<SerializedSearchScope>(json) scopeJson.parseToJsonElement(json).jsonObject
}.getOrNull()?.let { scope -> }.getOrNull()?.let { scope ->
when (scope.type) { when (scope["type"]?.jsonPrimitive?.contentOrNull) {
TYPE_SOURCE -> ParsedSearchScope( TYPE_SOURCE -> ParsedSearchScope(
sources = scope.sources.filter { it.url.isNotBlank() } sources = scope["sources"]
?.safeJsonArray()
?.mapNotNull { it.toSourceItemOrNull() }
.orEmpty()
) )
TYPE_GROUP -> ParsedSearchScope( TYPE_GROUP -> ParsedSearchScope(
groups = scope.groups.filter { it.isNotBlank() } groups = scope["groups"]
?.safeJsonArray()
?.mapNotNull { it.toStringOrNull() }
?.filter { it.isNotBlank() }
.orEmpty()
) )
else -> null else -> null
@@ -130,12 +147,32 @@ data class BookSearchScope(val raw: String) {
} }
} }
private fun JsonElement.safeJsonArray() = runCatching {
jsonArray
}.getOrNull()
private fun JsonElement.toStringOrNull() = runCatching {
jsonPrimitive.contentOrNull
}.getOrNull()
private fun JsonElement.toSourceItemOrNull(): ScopeSourceItem? {
return runCatching {
val item = jsonObject
val url = item["url"]?.jsonPrimitive?.contentOrNull
if (url.isNullOrBlank()) {
null
} else {
ScopeSourceItem(
name = item["name"]?.jsonPrimitive?.contentOrNull.orEmpty(),
url = url
)
}
}.getOrNull()
}
private const val TYPE_GROUP = "group" private const val TYPE_GROUP = "group"
private const val TYPE_SOURCE = "source" private const val TYPE_SOURCE = "source"
private val scopeJson = Json { private val scopeJson = Json
encodeDefaults = false
ignoreUnknownKeys = true
}
} }
} }
@@ -1,7 +1,9 @@
package io.legado.app.ui.book.changesource package io.legado.app.ui.book.changesource
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.snapshots.Snapshot import androidx.compose.runtime.snapshots.Snapshot
import androidx.datastore.preferences.core.Preferences
import io.legado.app.data.local.preferences.LocalPreferencesKeys import io.legado.app.data.local.preferences.LocalPreferencesKeys
import io.legado.app.data.local.preferences.LocalPreferencesRepository import io.legado.app.data.local.preferences.LocalPreferencesRepository
import io.legado.app.domain.usecase.ChangeSourceMigrationOptions import io.legado.app.domain.usecase.ChangeSourceMigrationOptions
@@ -9,9 +11,7 @@ import io.legado.app.ui.config.prefDelegate
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import splitties.init.appCtx import splitties.init.appCtx
object ChangeSourceConfig { object ChangeSourceConfig {
@@ -113,40 +113,25 @@ object ChangeSourceConfig {
) )
init { init {
runBlocking(Dispatchers.IO) { observe(LocalPreferencesKeys.CHANGE_SOURCE_SEARCH_SCOPE, "", _searchScope)
Snapshot.withMutableSnapshot { observe(LocalPreferencesKeys.CHANGE_SOURCE_CHECK_AUTHOR, false, _checkAuthor)
_searchScope.value = observe(LocalPreferencesKeys.CHANGE_SOURCE_LOAD_INFO, false, _loadInfo)
repo.getPreference(LocalPreferencesKeys.CHANGE_SOURCE_SEARCH_SCOPE, "").first() observe(LocalPreferencesKeys.CHANGE_SOURCE_LOAD_TOC, false, _loadToc)
_checkAuthor.value = observe(LocalPreferencesKeys.CHANGE_SOURCE_LOAD_WORD_COUNT, false, _loadWordCount)
repo.getPreference(LocalPreferencesKeys.CHANGE_SOURCE_CHECK_AUTHOR, false).first()
_loadInfo.value =
repo.getPreference(LocalPreferencesKeys.CHANGE_SOURCE_LOAD_INFO, false).first()
_loadToc.value =
repo.getPreference(LocalPreferencesKeys.CHANGE_SOURCE_LOAD_TOC, false).first()
_loadWordCount.value =
repo.getPreference(LocalPreferencesKeys.CHANGE_SOURCE_LOAD_WORD_COUNT, false).first()
}
} }
private fun <T> observe(
key: Preferences.Key<T>,
defaultValue: T,
state: MutableState<T>,
) {
scope.launch { scope.launch {
repo.getPreference(LocalPreferencesKeys.CHANGE_SOURCE_SEARCH_SCOPE, "") repo.getPreference(key, defaultValue)
.collect { Snapshot.withMutableSnapshot { _searchScope.value = it } } .collect { value ->
Snapshot.withMutableSnapshot {
state.value = value
} }
scope.launch {
repo.getPreference(LocalPreferencesKeys.CHANGE_SOURCE_CHECK_AUTHOR, false)
.collect { Snapshot.withMutableSnapshot { _checkAuthor.value = it } }
} }
scope.launch {
repo.getPreference(LocalPreferencesKeys.CHANGE_SOURCE_LOAD_INFO, false)
.collect { Snapshot.withMutableSnapshot { _loadInfo.value = it } }
}
scope.launch {
repo.getPreference(LocalPreferencesKeys.CHANGE_SOURCE_LOAD_TOC, false)
.collect { Snapshot.withMutableSnapshot { _loadToc.value = it } }
}
scope.launch {
repo.getPreference(LocalPreferencesKeys.CHANGE_SOURCE_LOAD_WORD_COUNT, false)
.collect { Snapshot.withMutableSnapshot { _loadWordCount.value = it } }
} }
} }