diff --git a/app/src/main/java/io/legado/app/help/source/BookSourceExtensions.kt b/app/src/main/java/io/legado/app/help/source/BookSourceExtensions.kt index 61ac5400f..2830e3ddd 100644 --- a/app/src/main/java/io/legado/app/help/source/BookSourceExtensions.kt +++ b/app/src/main/java/io/legado/app/help/source/BookSourceExtensions.kt @@ -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 -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/model/webBook/WebBook.kt b/app/src/main/java/io/legado/app/model/webBook/WebBook.kt index 13946074e..dd565ae95 100644 --- a/app/src/main/java/io/legado/app/model/webBook/WebBook.kt +++ b/app/src/main/java/io/legado/app/model/webBook/WebBook.kt @@ -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, + bookSourceParts: List, name: String, author: String, context: CoroutineContext = Dispatchers.IO, ): Coroutine> { 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) diff --git a/app/src/main/java/io/legado/app/ui/book/changecover/ChangeCoverViewModel.kt b/app/src/main/java/io/legado/app/ui/book/changecover/ChangeCoverViewModel.kt index 96c8f8203..d2291bb7f 100644 --- a/app/src/main/java/io/legado/app/ui/book/changecover/ChangeCoverViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/changecover/ChangeCoverViewModel.kt @@ -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() + private var bookSourceParts = arrayListOf() 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() 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) } diff --git a/app/src/main/java/io/legado/app/ui/book/search/SearchScopeDialog.kt b/app/src/main/java/io/legado/app/ui/book/search/SearchScopeDialog.kt index 475c828df..5906b287e 100644 --- a/app/src/main/java/io/legado/app/ui/book/search/SearchScopeDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/search/SearchScopeDialog.kt @@ -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 = emptyList() - var sources: List = emptyList() - val screenSources = arrayListOf() + val screenSources = arrayListOf() 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() { val selectGroups = arrayListOf() - 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 diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt index bdbab2b76..61ca2c043 100644 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt @@ -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>(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) {