Horis há 2 anos atrás
pai
commit
bd870c399c

+ 2 - 0
app/src/main/java/io/legado/app/base/adapter/RecyclerAdapter.kt

@@ -11,6 +11,7 @@ import androidx.recyclerview.widget.RecyclerView
 import androidx.viewbinding.ViewBinding
 import io.legado.app.help.coroutine.Coroutine
 import io.legado.app.utils.buildMainHandler
+import kotlinx.coroutines.ensureActive
 import kotlinx.coroutines.withTimeoutOrNull
 import splitties.views.onLongClick
 import java.util.Collections
@@ -156,6 +157,7 @@ abstract class RecyclerAdapter<ITEM, VB : ViewBinding>(protected val context: Co
                 } else {
                     DiffUtil.calculateDiff(callback)
                 }
+                ensureActive()
                 handler.post {
                     if (diffResult == null) {
                         setItems(items)

+ 88 - 121
app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceViewModel.kt

@@ -4,6 +4,7 @@ import android.app.Application
 import android.os.Bundle
 import androidx.annotation.CallSuper
 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
@@ -12,27 +13,34 @@ import io.legado.app.data.appDb
 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.BookHelp
 import io.legado.app.help.book.ContentProcessor
 import io.legado.app.help.config.AppConfig
 import io.legado.app.help.config.SourceConfig
-import io.legado.app.help.coroutine.CompositeCoroutine
 import io.legado.app.help.coroutine.Coroutine
 import io.legado.app.model.webBook.WebBook
+import io.legado.app.utils.mapParallelSafe
 import io.legado.app.utils.toastOnUi
 import kotlinx.coroutines.CancellationException
-import kotlinx.coroutines.CoroutineStart
 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.coroutineScope
-import kotlinx.coroutines.ensureActive
 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 kotlinx.coroutines.flow.map
+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.ConcurrentHashMap
 import java.util.concurrent.Executors
@@ -48,9 +56,8 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a
     var author: String = ""
     private var fromReadBookActivity = false
     private var oldBook: Book? = null
-    private var tasks = CompositeCoroutine()
     private var screenKey: String = ""
-    private var bookSourceList = arrayListOf<BookSource>()
+    private var bookSourceParts = arrayListOf<BookSourcePart>()
     private var searchBookList = arrayListOf<SearchBook>()
     private val searchBooks = Collections.synchronizedList(arrayListOf<SearchBook>())
     private val tocMap = ConcurrentHashMap<String, List<BookChapter>>()
@@ -58,7 +65,6 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a
         ContentProcessor.get(oldBook!!)
     }
     private var searchCallback: SourceCallback? = null
-    private val emptyBookSource = BookSource()
     private val chapterNumRegex = "^\\[(\\d+)]".toRegex()
     private val comparatorBase by lazy {
         compareByDescending<SearchBook> { getBookScore(it) }
@@ -73,6 +79,7 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a
             .thenByDescending { it.chapterWordCount }
             .thenBy { it.originOrder }
     }
+    private var task: Job? = null
     val bookMap = ConcurrentHashMap<String, Book>()
     val searchDataFlow = callbackFlow {
 
@@ -120,9 +127,6 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a
         }.getOrDefault(searchBooks)
     }.flowOn(IO)
 
-    @Volatile
-    private var searchIndex = -1
-
     override fun onCleared() {
         super.onCleared()
         searchPool?.close()
@@ -145,7 +149,6 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a
     private fun initSearchPool() {
         searchPool = Executors
             .newFixedThreadPool(min(threadCount, AppConst.MAX_THREAD)).asCoroutineDispatcher()
-        searchIndex = -1
     }
 
     fun refresh(): Boolean {
@@ -160,85 +163,83 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a
     /**
      * 搜索书籍
      */
-    fun startSearch(origin: String? = null) {
+    fun startSearch() {
         execute {
             stopSearch()
-            origin?.let {
-                bookSourceList.clear()
-                bookSourceList.add(appDb.bookSourceDao.getBookSource(origin)!!)
-                searchStateData.postValue(true)
-                searchBooks.removeIf { it.origin == origin }
-                initSearchPool()
-                search()
-                return@execute
-            }
             appDb.searchBookDao.clear(name, author)
             searchBooks.clear()
             searchCallback?.upAdapter()
-            bookSourceList.clear()
+            bookSourceParts.clear()
             val searchGroup = AppConfig.searchGroup
             if (searchGroup.isBlank()) {
-                bookSourceList.addAll(appDb.bookSourceDao.allEnabled)
+                bookSourceParts.addAll(appDb.bookSourceDao.allEnabledPart)
             } else {
-                val sources = appDb.bookSourceDao.getEnabledByGroup(searchGroup)
+                val sources = appDb.bookSourceDao.getEnabledPartByGroup(searchGroup)
                 if (sources.isEmpty()) {
                     AppConfig.searchGroup = ""
-                    bookSourceList.addAll(appDb.bookSourceDao.allEnabled)
+                    bookSourceParts.addAll(appDb.bookSourceDao.allEnabledPart)
                 } else {
-                    bookSourceList.addAll(sources)
+                    bookSourceParts.addAll(sources)
                 }
             }
-            searchStateData.postValue(true)
             initSearchPool()
-            for (i in 0 until threadCount) {
-                search()
-            }
+            search()
         }
     }
 
-    private fun search() {
-        val searchIndex = synchronized(this) {
-            if (searchIndex >= bookSourceList.lastIndex) {
-                return
-            }
-            ++searchIndex
+    fun startSearch(origin: String) {
+        execute {
+            stopSearch()
+            bookSourceParts.clear()
+            bookSourceParts.add(appDb.bookSourceDao.getBookSourcePart(origin)!!)
+            searchBooks.removeIf { it.origin == origin }
+            initSearchPool()
+            search()
         }
-        val source = bookSourceList.getOrNull(searchIndex) ?: return
-        bookSourceList[searchIndex] = emptyBookSource
-        val task = execute(
-            context = searchPool!!,
-            start = CoroutineStart.LAZY,
-            executeContext = searchPool!!
-        ) {
-            val resultBooks = WebBook.searchBookAwait(source, name)
-            resultBooks.forEach { searchBook ->
-                if (searchBook.name != name) {
-                    return@forEach
-                }
-                if (AppConfig.changeSourceCheckAuthor && !searchBook.author.contains(author)) {
-                    return@forEach
-                }
-                when {
-                    AppConfig.changeSourceLoadInfo || AppConfig.changeSourceLoadToc || AppConfig.changeSourceLoadWordCount -> {
-                        loadBookInfo(source, searchBook.toBook())
-                    }
+    }
 
-                    else -> {
-                        searchCallback?.searchSuccess(searchBook)
+    private fun search() {
+        task = viewModelScope.launch(searchPool!!) {
+            flow {
+                for (bs in bookSourceParts) {
+                    bs.getBookSource()?.let {
+                        emit(it)
                     }
                 }
+            }.onStart {
+                searchStateData.postValue(true)
+            }.mapParallelSafe(threadCount) {
+                withTimeout(60000L) {
+                    search(it)
+                }
+            }.onCompletion {
+                searchStateData.postValue(false)
+                searchFinishCallback?.invoke(searchBooks.isEmpty())
+            }.catch {
+                AppLog.put("换源搜索出错\n${it.localizedMessage}", it)
+            }.collect()
+        }
+    }
+
+    private suspend fun search(source: BookSource) {
+        val resultBooks = WebBook.searchBookAwait(source, name)
+        resultBooks.forEach { searchBook ->
+            if (searchBook.name != name) {
+                return@forEach
             }
-        }.timeout(60000L)
-            .onError {
-                ensureActive()
-                nextSearch()
+            if (AppConfig.changeSourceCheckAuthor && !searchBook.author.contains(author)) {
+                return@forEach
             }
-            .onSuccess {
-                ensureActive()
-                nextSearch()
+            when {
+                AppConfig.changeSourceLoadInfo || AppConfig.changeSourceLoadToc || AppConfig.changeSourceLoadWordCount -> {
+                    loadBookInfo(source, searchBook.toBook())
+                }
+
+                else -> {
+                    searchCallback?.searchSuccess(searchBook)
+                }
             }
-        task.start()
-        tasks.add(task)
+        }
     }
 
     private suspend fun loadBookInfo(source: BookSource, book: Book) {
@@ -298,24 +299,6 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a
         searchCallback?.searchSuccess(searchBook)
     }
 
-    @Synchronized
-    private fun nextSearch() {
-        kotlin.runCatching {
-            if (searchIndex < bookSourceList.lastIndex) {
-                search()
-            } else {
-                searchIndex++
-            }
-            if (searchIndex >= bookSourceList.lastIndex + bookSourceList.size
-                || searchIndex >= bookSourceList.lastIndex + threadCount
-            ) {
-                searchStateData.postValue(false)
-                tasks.clear()
-                searchFinishCallback?.invoke(searchBooks.isEmpty())
-            }
-        }
-    }
-
     fun onLoadWordCountChecked(isChecked: Boolean) {
         if (isChecked) {
             startRefreshList(true)
@@ -330,54 +313,38 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a
             stopSearch()
             searchBookList.clear()
             if (onlyRefreshNoWordCountBook) {
-                val noWordCountBook = searchBooks.filter { it.chapterWordCountText == null }
-                searchBookList.addAll(noWordCountBook)
+                searchBooks.filterTo(searchBookList) {
+                    it.chapterWordCountText == null
+                }
                 searchBooks.removeIf { it.chapterWordCountText == null }
             } else {
                 searchBookList.addAll(searchBooks)
                 searchBooks.clear()
             }
             searchCallback?.upAdapter()
-            searchStateData.postValue(true)
             initSearchPool()
-            for (i in 0 until threadCount) {
-                refreshList()
-            }
+            refreshList()
         }
     }
 
     private fun refreshList() {
-        synchronized(this) {
-            if (searchIndex >= searchBookList.lastIndex) {
-                return
-            }
-            searchIndex++
-        }
-        val searchBook = searchBookList[searchIndex]
-        val task = execute(context = searchPool!!, executeContext = searchPool!!) {
-            val source = appDb.bookSourceDao.getBookSource(searchBook.origin) ?: return@execute
-            loadBookInfo(source, searchBook.toBook())
-        }.timeout(60000L)
-            .onError {
-                nextRefreshList()
-            }
-            .onSuccess {
-                nextRefreshList()
-            }
-        tasks.add(task)
-    }
-
-    private fun nextRefreshList() {
-        synchronized(this) {
-            if (searchIndex < searchBookList.lastIndex) {
-                refreshList()
-            } else {
-                searchIndex++
-            }
-            if (searchIndex >= searchBookList.lastIndex + min(searchBookList.size, threadCount)) {
+        task = viewModelScope.launch(searchPool!!) {
+            flow {
+                for (searchBook in searchBookList) {
+                    emit(searchBook)
+                }
+            }.onStart {
+                searchStateData.postValue(true)
+            }.mapParallelSafe(threadCount) {
+                val source = appDb.bookSourceDao.getBookSource(it.origin)!!
+                withTimeout(60000L) {
+                    loadBookInfo(source, it.toBook())
+                }
+            }.onCompletion {
                 searchStateData.postValue(false)
-                tasks.clear()
-            }
+            }.catch {
+                AppLog.put("换源刷新列表出错\n${it.localizedMessage}", it)
+            }.collect()
         }
     }
 
@@ -420,7 +387,7 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a
     }
 
     fun startOrStopSearch() {
-        if (tasks.isEmpty) {
+        if (task == null || !task!!.isActive) {
             startSearch()
         } else {
             stopSearch()
@@ -428,7 +395,7 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a
     }
 
     fun stopSearch() {
-        tasks.clear()
+        task?.cancel()
         searchPool?.close()
         searchStateData.postValue(false)
     }