pull/1732/head
kunfei 3 years ago
parent 561f1e4e6b
commit 6144de2198
  1. 75
      app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceViewModel.kt
  2. 315
      app/src/main/java/io/legado/app/ui/book/changesource/ChangeChapterSourceViewModel.kt

@ -14,10 +14,11 @@ import io.legado.app.data.entities.BookSource
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.help.coroutine.Coroutine
import io.legado.app.model.webBook.WebBook
import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.getPrefString
import io.legado.app.utils.printOnDebug
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.ExecutorCoroutineDispatcher
import kotlinx.coroutines.asCoroutineDispatcher
@ -26,16 +27,16 @@ import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import splitties.init.appCtx
import java.util.*
import java.util.concurrent.Executors
import kotlin.math.min
@Suppress("MemberVisibilityCanBePrivate")
class ChangeBookSourceViewModel(application: Application) : BaseViewModel(application) {
open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(application) {
private val threadCount = AppConfig.threadCount
private var searchPool: ExecutorCoroutineDispatcher? = null
val searchStateData = MutableLiveData<Boolean>()
var searchFinishCallback: ((isEmpty: Boolean) -> Unit)? = null
var name: String = ""
var author: String = ""
private var tasks = CompositeCoroutine()
@ -70,7 +71,7 @@ class ChangeBookSourceViewModel(application: Application) : BaseViewModel(applic
trySend(arrayOf(searchBooks))
}
if (searchBooks.size <= 1) {
if (searchBooks.isEmpty()) {
startSearch()
}
@ -84,7 +85,7 @@ class ChangeBookSourceViewModel(application: Application) : BaseViewModel(applic
@Volatile
private var searchIndex = -1
fun initData(arguments: Bundle?) {
open fun initData(arguments: Bundle?) {
arguments?.let { bundle ->
bundle.getString("name")?.let {
name = it
@ -141,18 +142,16 @@ class ChangeBookSourceViewModel(application: Application) : BaseViewModel(applic
searchIndex++
}
val source = bookSourceList[searchIndex]
val task = WebBook
.searchBook(viewModelScope, source, name, context = searchPool!!)
.timeout(60000L)
.onSuccess(IO) {
it.forEach { searchBook ->
val task = Coroutine.async(scope = viewModelScope, context = searchPool!!) {
val resultBooks = WebBook.searchBookAwait(this, source, name)
resultBooks.forEach { searchBook ->
if (searchBook.name == name) {
if ((AppConfig.changeSourceCheckAuthor && searchBook.author.contains(author))
|| !AppConfig.changeSourceCheckAuthor
) {
if (searchBook.latestChapterTitle.isNullOrEmpty()) {
if (AppConfig.changeSourceLoadInfo || AppConfig.changeSourceLoadToc) {
loadBookInfo(source, searchBook.toBook())
loadBookInfo(this, source, searchBook.toBook())
} else {
searchCallback?.searchSuccess(searchBook)
}
@ -163,50 +162,46 @@ class ChangeBookSourceViewModel(application: Application) : BaseViewModel(applic
}
}
}
.onFinally(searchPool) {
synchronized(this) {
if (searchIndex < bookSourceList.lastIndex) {
search()
} else {
searchIndex++
}
if (searchIndex >= bookSourceList.lastIndex + bookSourceList.size
|| searchIndex >= bookSourceList.lastIndex + threadCount
) {
searchStateData.postValue(false)
tasks.clear()
}
}
.timeout(60000L)
.onFinally {
nextSearch()
}
tasks.add(task)
}
private fun loadBookInfo(source: BookSource, book: Book) {
WebBook.getBookInfo(viewModelScope, source, book, context = searchPool!!)
.onSuccess(IO) {
private suspend fun loadBookInfo(scope: CoroutineScope, source: BookSource, book: Book) {
val mBook = WebBook.getBookInfoAwait(scope, source, book)
if (context.getPrefBoolean(PreferKey.changeSourceLoadToc)) {
loadBookToc(source, book)
loadBookToc(scope, source, mBook)
} else {
//从详情页里获取最新章节
book.latestChapterTitle = it.latestChapterTitle
book.latestChapterTitle = mBook.latestChapterTitle
val searchBook = book.toSearchBook()
searchCallback?.searchSuccess(searchBook)
}
}.onError(IO) {
it.printOnDebug()
}
}
private fun loadBookToc(source: BookSource, book: Book) {
WebBook.getChapterList(viewModelScope, source, book, context = searchPool!!)
.onSuccess(IO) { chapters ->
private suspend fun loadBookToc(scope: CoroutineScope, source: BookSource, book: Book) {
val chapters = WebBook.getChapterListAwait(scope, source, book)
book.latestChapterTitle = chapters.last().title
val searchBook: SearchBook = book.toSearchBook()
searchCallback?.searchSuccess(searchBook)
}.onError(IO) {
it.printOnDebug()
}
private fun nextSearch() {
synchronized(this) {
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())
}
}
}

@ -2,101 +2,25 @@ package io.legado.app.ui.book.changesource
import android.app.Application
import android.os.Bundle
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.AppPattern
import io.legado.app.constant.PreferKey
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.SearchBook
import io.legado.app.exception.NoStackTraceException
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.getPrefBoolean
import io.legado.app.utils.getPrefString
import io.legado.app.utils.printOnDebug
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.ExecutorCoroutineDispatcher
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import splitties.init.appCtx
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Executors
import kotlin.math.min
@Suppress("MemberVisibilityCanBePrivate")
class ChangeChapterSourceViewModel(application: Application) : BaseViewModel(application) {
private val threadCount = AppConfig.threadCount
private var searchPool: ExecutorCoroutineDispatcher? = null
private val searchGroup get() = appCtx.getPrefString("searchGroup") ?: ""
val searchStateData = MutableLiveData<Boolean>()
var name: String = ""
var author: String = ""
class ChangeChapterSourceViewModel(application: Application) :
ChangeBookSourceViewModel(application) {
var chapterIndex: Int = 0
var chapterTitle: String = ""
private var tasks = CompositeCoroutine()
private var screenKey: String = ""
private var bookSourceList = arrayListOf<BookSource>()
private val searchBooks = Collections.synchronizedList(arrayListOf<SearchBook>())
private val tocMap = ConcurrentHashMap<String, List<BookChapter>>()
private var searchCallback: SourceCallback? = null
val searchDataFlow = callbackFlow {
searchCallback = object : SourceCallback {
override fun searchSuccess(searchBook: SearchBook) {
appDb.searchBookDao.insert(searchBook)
when {
screenKey.isEmpty() -> searchBooks.add(searchBook)
searchBook.name.contains(screenKey) -> searchBooks.add(searchBook)
else -> return
}
trySend(searchBooks)
}
override fun upAdapter() {
trySend(searchBooks)
}
}
getDbSearchBooks().let {
searchBooks.clear()
searchBooks.addAll(it)
trySend(searchBooks)
}
if (searchBooks.size <= 1) {
startSearch()
}
awaitClose {
searchCallback = null
}
}.map {
searchBooks.sortedBy { it.originOrder }
}.flowOn(IO)
@Volatile
private var searchIndex = -1
private val tocMap = ConcurrentHashMap<String, List<BookChapter>>()
fun initData(arguments: Bundle?) {
override fun initData(arguments: Bundle?) {
arguments?.let { bundle ->
bundle.getString("name")?.let {
name = it
}
bundle.getString("author")?.let {
author = it.replace(AppPattern.authorRegex, "")
}
bundle.getString("chapterTitle")?.let {
chapterTitle = it
}
@ -104,171 +28,6 @@ class ChangeChapterSourceViewModel(application: Application) : BaseViewModel(app
}
}
private fun initSearchPool() {
searchPool = Executors
.newFixedThreadPool(min(threadCount, AppConst.MAX_THREAD)).asCoroutineDispatcher()
searchIndex = -1
}
fun refresh() {
getDbSearchBooks().let {
searchBooks.clear()
searchBooks.addAll(it)
searchCallback?.upAdapter()
}
}
fun startSearch() {
execute {
stopSearch()
appDb.searchBookDao.clear(name, author)
searchBooks.clear()
bookSourceList.clear()
if (searchGroup.isBlank()) {
bookSourceList.addAll(appDb.bookSourceDao.allEnabled)
} else {
val sources = appDb.bookSourceDao.getEnabledByGroup(searchGroup)
if (sources.isEmpty()) {
bookSourceList.addAll(appDb.bookSourceDao.allEnabled)
} else {
bookSourceList.addAll(sources)
}
}
searchStateData.postValue(true)
initSearchPool()
for (i in 0 until threadCount) {
search()
}
}
}
private fun search() {
synchronized(this) {
if (searchIndex >= bookSourceList.lastIndex) {
return
}
searchIndex++
}
val source = bookSourceList[searchIndex]
val task = WebBook
.searchBook(viewModelScope, source, name, context = searchPool!!)
.timeout(60000L)
.onSuccess(IO) {
it.forEach { searchBook ->
if (searchBook.name == name) {
if ((AppConfig.changeSourceCheckAuthor && searchBook.author.contains(author))
|| !AppConfig.changeSourceCheckAuthor
) {
if (searchBook.latestChapterTitle.isNullOrEmpty()) {
if (AppConfig.changeSourceLoadInfo || AppConfig.changeSourceLoadToc) {
loadBookInfo(source, searchBook.toBook())
} else {
searchCallback?.searchSuccess(searchBook)
}
} else {
searchCallback?.searchSuccess(searchBook)
}
}
}
}
}
.onFinally(searchPool) {
synchronized(this) {
if (searchIndex < bookSourceList.lastIndex) {
search()
} else {
searchIndex++
}
if (searchIndex >= bookSourceList.lastIndex + bookSourceList.size
|| searchIndex >= bookSourceList.lastIndex + threadCount
) {
searchStateData.postValue(false)
tasks.clear()
}
}
}
tasks.add(task)
}
private fun loadBookInfo(source: BookSource, book: Book) {
WebBook.getBookInfo(viewModelScope, source, book, context = searchPool!!)
.onSuccess(IO) {
if (context.getPrefBoolean(PreferKey.changeSourceLoadToc)) {
loadBookToc(source, book)
} else {
//从详情页里获取最新章节
book.latestChapterTitle = it.latestChapterTitle
val searchBook = book.toSearchBook()
searchCallback?.searchSuccess(searchBook)
}
}.onError(IO) {
it.printOnDebug()
}
}
private fun loadBookToc(source: BookSource, book: Book) {
WebBook.getChapterList(viewModelScope, source, book, context = searchPool!!)
.onSuccess(IO) { chapters ->
tocMap[book.bookUrl] = chapters
book.latestChapterTitle = chapters.last().title
val searchBook: SearchBook = book.toSearchBook()
searchCallback?.searchSuccess(searchBook)
}.onError(IO) {
it.printOnDebug()
}
}
private fun getDbSearchBooks(): List<SearchBook> {
return if (screenKey.isEmpty()) {
if (AppConfig.changeSourceCheckAuthor) {
appDb.searchBookDao.getChangeSourceSearch(name, author, searchGroup)
} else {
appDb.searchBookDao.getChangeSourceSearch(name, "", searchGroup)
}
} else {
if (AppConfig.changeSourceCheckAuthor) {
appDb.searchBookDao.getChangeSourceSearch(name, author, screenKey, searchGroup)
} else {
appDb.searchBookDao.getChangeSourceSearch(name, "", screenKey, searchGroup)
}
}
}
/**
* 筛选
*/
fun screen(key: String?) {
screenKey = key?.trim() ?: ""
execute {
getDbSearchBooks().let {
searchBooks.clear()
searchBooks.addAll(it)
searchCallback?.upAdapter()
}
}
}
fun startOrStopSearch() {
if (tasks.isEmpty) {
startSearch()
} else {
stopSearch()
}
}
fun stopSearch() {
tasks.clear()
searchPool?.close()
searchStateData.postValue(false)
}
override fun onCleared() {
super.onCleared()
searchPool?.close()
}
fun getToc(
searchBook: SearchBook,
success: (toc: List<BookChapter>) -> Unit,
@ -312,68 +71,4 @@ class ChangeChapterSourceViewModel(application: Application) : BaseViewModel(app
}
}
fun disableSource(searchBook: SearchBook) {
execute {
appDb.bookSourceDao.getBookSource(searchBook.origin)?.let { source ->
source.enabled = false
appDb.bookSourceDao.update(source)
}
searchBooks.remove(searchBook)
searchCallback?.upAdapter()
}
}
fun topSource(searchBook: SearchBook) {
execute {
appDb.bookSourceDao.getBookSource(searchBook.origin)?.let { source ->
val minOrder = appDb.bookSourceDao.minOrder - 1
source.customOrder = minOrder
searchBook.originOrder = source.customOrder
appDb.bookSourceDao.update(source)
updateSource(searchBook)
}
searchCallback?.upAdapter()
}
}
fun bottomSource(searchBook: SearchBook) {
execute {
appDb.bookSourceDao.getBookSource(searchBook.origin)?.let { source ->
val maxOrder = appDb.bookSourceDao.maxOrder + 1
source.customOrder = maxOrder
searchBook.originOrder = source.customOrder
appDb.bookSourceDao.update(source)
updateSource(searchBook)
}
searchCallback?.upAdapter()
}
}
fun updateSource(searchBook: SearchBook) {
appDb.searchBookDao.update(searchBook)
}
fun del(searchBook: SearchBook) {
execute {
appDb.bookSourceDao.getBookSource(searchBook.origin)?.let { source ->
appDb.bookSourceDao.delete(source)
appDb.searchBookDao.delete(searchBook)
}
}
searchBooks.remove(searchBook)
searchCallback?.upAdapter()
}
fun firstSourceOrNull(searchBook: SearchBook): SearchBook? {
return searchBooks.firstOrNull { it.bookUrl != searchBook.bookUrl }
}
interface SourceCallback {
fun searchSuccess(searchBook: SearchBook)
fun upAdapter()
}
}
Loading…
Cancel
Save