This commit is contained in:
Horis 2024-03-07 22:32:37 +08:00
parent 77711af564
commit 2cda94547e
2 changed files with 92 additions and 123 deletions

View File

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

View File

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