|
@@ -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()
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- 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) {
|
|
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 }
|
|
|
|
|
- searchBookList.addAll(noWordCountBook)
|
|
|
|
|
|
|
+ searchBooks.filterTo(searchBookList) {
|
|
|
|
|
+ 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) {
|
|
|
|
|
- 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)
|
|
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)
|
|
|
}
|
|
}
|