Horis 2 лет назад
Родитель
Сommit
267f0b97f6

+ 0 - 11
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
-}

+ 4 - 2
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<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)

+ 44 - 59
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<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)
     }

+ 42 - 19
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<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)
             }
-            adapter.notifyDataSetChanged()
         }
     }
 
     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

+ 2 - 7
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<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) {