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 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)

View File

@ -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()
}
}
fun startSearch(origin: String) {
execute {
stopSearch()
bookSourceParts.clear()
bookSourceParts.add(appDb.bookSourceDao.getBookSourcePart(origin)!!)
searchBooks.removeIf { it.origin == origin }
initSearchPool()
search()
}
}
private fun search() {
val searchIndex = synchronized(this) {
if (searchIndex >= bookSourceList.lastIndex) {
return
}
++searchIndex
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()
}
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 suspend fun search(source: BookSource) {
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) {
@ -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)
}