This commit is contained in:
Horis 2024-11-14 18:13:42 +08:00
parent 9ea26b5eab
commit 8b20f3edbe
5 changed files with 93 additions and 99 deletions

View File

@ -14,7 +14,6 @@ import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import java.util.concurrent.ConcurrentHashMap
import kotlin.collections.set
/**
* 采用md5作为key可以在分类修改后自动重新计算,不需要手动刷新
@ -91,13 +90,3 @@ suspend fun BookSourcePart.clearExploreKindsCache() {
exploreKindsMap.remove(exploreKindsKey)
}
}
fun BookSource.contains(word: String?): Boolean {
if (word.isNullOrEmpty()) {
return true
}
return bookSourceName.contains(word)
|| bookSourceUrl.contains(word)
|| bookSourceGroup?.contains(word) == true
|| bookSourceComment?.contains(word) == true
}

View File

@ -4,6 +4,7 @@ import io.legado.app.constant.AppLog
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter
import io.legado.app.data.entities.BookSource
import io.legado.app.data.entities.BookSourcePart
import io.legado.app.data.entities.SearchBook
import io.legado.app.exception.NoStackTraceException
import io.legado.app.help.book.addType
@ -352,13 +353,14 @@ object WebBook {
*/
fun preciseSearch(
scope: CoroutineScope,
bookSources: List<BookSource>,
bookSourceParts: List<BookSourcePart>,
name: String,
author: String,
context: CoroutineContext = Dispatchers.IO,
): Coroutine<Pair<Book, BookSource>> {
return Coroutine.async(scope, context) {
for (source in bookSources) {
for (s in bookSourceParts) {
val source = s.getBookSource() ?: continue
val book = preciseSearchAwait(scope, source, name, author).getOrNull()
if (book != null) {
return@async Pair(book, source)

View File

@ -6,30 +6,39 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import io.legado.app.base.BaseViewModel
import io.legado.app.constant.AppConst
import io.legado.app.constant.AppLog
import io.legado.app.constant.AppPattern
import io.legado.app.data.appDb
import io.legado.app.data.entities.BookSource
import io.legado.app.data.entities.BookSourcePart
import io.legado.app.data.entities.SearchBook
import io.legado.app.help.config.AppConfig
import io.legado.app.help.coroutine.CompositeCoroutine
import io.legado.app.model.webBook.WebBook
import io.legado.app.utils.mapParallelSafe
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.ExecutorCoroutineDispatcher
import kotlinx.coroutines.Job
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import java.util.*
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeout
import java.util.Collections
import java.util.concurrent.Executors
import kotlin.math.min
class ChangeCoverViewModel(application: Application) : BaseViewModel(application) {
private val threadCount = AppConfig.threadCount
private var searchPool: ExecutorCoroutineDispatcher? = null
private val tasks = CompositeCoroutine()
private var searchSuccess: ((SearchBook) -> Unit)? = null
private var upAdapter: (() -> Unit)? = null
private var bookSourceList = arrayListOf<BookSource>()
private var bookSourceParts = arrayListOf<BookSourcePart>()
private val defaultCover by lazy {
listOf(
SearchBook(
@ -40,6 +49,7 @@ class ChangeCoverViewModel(application: Application) : BaseViewModel(application
)
)
}
private var task: Job? = null
val searchStateData = MutableLiveData<Boolean>()
var name: String = ""
var author: String = ""
@ -73,9 +83,6 @@ class ChangeCoverViewModel(application: Application) : BaseViewModel(application
}
}.flowOn(IO)
@Volatile
private var searchIndex = -1
fun initData(arguments: Bundle?) {
arguments?.let { bundle ->
bundle.getString("name")?.let {
@ -90,7 +97,6 @@ class ChangeCoverViewModel(application: Application) : BaseViewModel(application
private fun initSearchPool() {
searchPool = Executors
.newFixedThreadPool(min(threadCount, AppConst.MAX_THREAD)).asCoroutineDispatcher()
searchIndex = -1
}
private fun startSearch() {
@ -98,71 +104,50 @@ class ChangeCoverViewModel(application: Application) : BaseViewModel(application
stopSearch()
searchBooks.clear()
upAdapter?.invoke()
bookSourceList.clear()
bookSourceList.addAll(appDb.bookSourceDao.allEnabled)
searchStateData.postValue(true)
bookSourceParts.clear()
bookSourceParts.addAll(appDb.bookSourceDao.allEnabledPart)
initSearchPool()
for (i in 0 until threadCount) {
search()
}
search()
}
}
@Synchronized
private fun search() {
if (searchIndex >= bookSourceList.lastIndex) {
return
}
searchIndex++
val source = bookSourceList[searchIndex]
if (source.getSearchRule().coverUrl.isNullOrBlank()) {
searchNext()
return
}
val task = WebBook
.searchBook(
viewModelScope,
source,
name,
context = searchPool!!,
executeContext = searchPool!!
)
.timeout(60000L)
.onSuccess(IO) {
it.firstOrNull()?.let { searchBook ->
if (searchBook.name == name && searchBook.author == author
&& !searchBook.coverUrl.isNullOrEmpty()
) {
appDb.searchBookDao.insert(searchBook)
searchSuccess?.invoke(searchBook)
task = viewModelScope.launch(searchPool!!) {
flow {
for (bs in bookSourceParts) {
bs.getBookSource()?.let {
emit(it)
}
}
}
.onFinally {
searchNext()
}
tasks.add(task)
}.onStart {
searchStateData.postValue(true)
}.mapParallelSafe(threadCount) {
withTimeout(60000L) {
search(it)
}
}.onCompletion {
searchStateData.postValue(false)
}.catch {
AppLog.put("封面换源搜索出错\n${it.localizedMessage}", it)
}.collect()
}
}
@Synchronized
private fun searchNext() {
if (searchIndex < bookSourceList.lastIndex) {
search()
} else {
searchIndex++
private suspend fun search(source: BookSource) {
if (source.getSearchRule().coverUrl.isNullOrBlank()) {
return
}
if (searchIndex >= bookSourceList.lastIndex + min(
bookSourceList.size,
threadCount
)
val searchBook = WebBook.searchBookAwait(source, name).firstOrNull() ?: return
if (searchBook.name == name && searchBook.author == author
&& !searchBook.coverUrl.isNullOrEmpty()
) {
searchStateData.postValue(false)
tasks.clear()
appDb.searchBookDao.insert(searchBook)
searchSuccess?.invoke(searchBook)
}
}
fun startOrStopSearch() {
if (tasks.isEmpty) {
if (task == null || !task!!.isActive) {
startSearch()
} else {
stopSearch()
@ -170,7 +155,7 @@ class ChangeCoverViewModel(application: Application) : BaseViewModel(application
}
private fun stopSearch() {
tasks.clear()
task?.cancel()
searchPool?.close()
searchStateData.postValue(false)
}

View File

@ -10,27 +10,34 @@ import androidx.recyclerview.widget.RecyclerView
import io.legado.app.R
import io.legado.app.base.BaseDialogFragment
import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.constant.AppLog
import io.legado.app.data.AppDatabase
import io.legado.app.data.appDb
import io.legado.app.data.entities.BookSource
import io.legado.app.data.entities.BookSourcePart
import io.legado.app.databinding.DialogSearchScopeBinding
import io.legado.app.databinding.ItemCheckBoxBinding
import io.legado.app.databinding.ItemRadioButtonBinding
import io.legado.app.help.source.contains
import io.legado.app.lib.theme.primaryColor
import io.legado.app.utils.applyTint
import io.legado.app.utils.flowWithLifecycleAndDatabaseChange
import io.legado.app.utils.setLayout
import io.legado.app.utils.viewbindingdelegate.viewBinding
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class SearchScopeDialog : BaseDialogFragment(R.layout.dialog_search_scope) {
private val binding by viewBinding(DialogSearchScopeBinding::bind)
private var sourceFlowJob: Job? = null
val callback: Callback get() = parentFragment as? Callback ?: activity as Callback
var groups: List<String> = emptyList()
var sources: List<BookSource> = emptyList()
val screenSources = arrayListOf<BookSource>()
val screenSources = arrayListOf<BookSourcePart>()
var screenText: String? = null
val adapter by lazy {
@ -49,7 +56,6 @@ class SearchScopeDialog : BaseDialogFragment(R.layout.dialog_search_scope) {
initSearchView()
initOtherView()
initData()
upData()
}
private fun initMenu() {
@ -105,34 +111,49 @@ class SearchScopeDialog : BaseDialogFragment(R.layout.dialog_search_scope) {
groups = withContext(IO) {
appDb.bookSourceDao.allEnabledGroups()
}
sources = withContext(IO) {
appDb.bookSourceDao.allEnabled
}
upData()
}
}
@SuppressLint("NotifyDataSetChanged")
private fun upData() {
lifecycleScope.launch {
withContext(IO) {
if (binding.rbSource.isChecked) {
sources.filter { source ->
source.contains(screenText)
}.let {
screenSources.clear()
screenSources.addAll(it)
}
}
}
if (binding.rbSource.isChecked) {
upBookSource(screenText)
} else {
adapter.notifyDataSetChanged()
}
}
@SuppressLint("NotifyDataSetChanged")
private fun upBookSource(searchKey: String? = null) {
sourceFlowJob?.cancel()
sourceFlowJob = lifecycleScope.launch {
when {
searchKey.isNullOrEmpty() -> {
appDb.bookSourceDao.flowAll()
}
else -> {
appDb.bookSourceDao.flowSearch(searchKey)
}
}.flowWithLifecycleAndDatabaseChange(
lifecycle,
table = AppDatabase.BOOK_SOURCE_TABLE_NAME
).catch {
AppLog.put("多分组/书源界面更新书源出错", it)
}.flowOn(IO).conflate().collect { data ->
screenSources.clear()
screenSources.addAll(data)
adapter.notifyDataSetChanged()
delay(500)
}
}
}
inner class RecyclerAdapter : RecyclerView.Adapter<ItemViewHolder>() {
val selectGroups = arrayListOf<String>()
var selectSource: BookSource? = null
var selectSource: BookSourcePart? = null
override fun getItemViewType(position: Int): Int {
return if (binding.rbSource.isChecked) {
@ -165,6 +186,7 @@ class SearchScopeDialog : BaseDialogFragment(R.layout.dialog_search_scope) {
holder.binding.checkBox.text = it
}
}
is ItemRadioButtonBinding -> {
screenSources.getOrNull(position)?.let {
holder.binding.radioButton.isChecked = selectSource == it
@ -195,6 +217,7 @@ class SearchScopeDialog : BaseDialogFragment(R.layout.dialog_search_scope) {
}
}
}
is ItemRadioButtonBinding -> {
screenSources.getOrNull(position)?.let {
holder.binding.radioButton.isChecked = selectSource == it

View File

@ -27,11 +27,6 @@ import kotlinx.coroutines.isActive
import java.io.File
import java.io.FileOutputStream
import java.io.OutputStreamWriter
import kotlin.collections.List
import kotlin.collections.Map
import kotlin.collections.forEach
import kotlin.collections.hashMapOf
import kotlin.collections.set
class BookshelfViewModel(application: Application) : BaseViewModel(application) {
val addBookProgressLiveData = MutableLiveData(-1)
@ -156,13 +151,13 @@ class BookshelfViewModel(application: Application) : BaseViewModel(application)
private fun importBookshelfByJson(json: String, groupId: Long) {
execute {
val bookSources = appDb.bookSourceDao.allEnabled
val bookSourceParts = appDb.bookSourceDao.allEnabledPart
GSON.fromJsonArray<Map<String, String?>>(json).getOrThrow().forEach { bookInfo ->
if (!isActive) return@execute
val name = bookInfo["name"] ?: ""
val author = bookInfo["author"] ?: ""
if (name.isNotEmpty() && appDb.bookDao.getBook(name, author) == null) {
WebBook.preciseSearch(this, bookSources, name, author)
WebBook.preciseSearch(this, bookSourceParts, name, author)
.onSuccess {
val book = it.first
if (groupId > 0) {