This commit is contained in:
Horis
2024-11-14 18:13:42 +08:00
parent 9ea26b5eab
commit 8b20f3edbe
5 changed files with 93 additions and 99 deletions
@@ -14,7 +14,6 @@ import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import kotlin.collections.set
/** /**
* 采用md5作为key可以在分类修改后自动重新计算,不需要手动刷新 * 采用md5作为key可以在分类修改后自动重新计算,不需要手动刷新
@@ -91,13 +90,3 @@ suspend fun BookSourcePart.clearExploreKindsCache() {
exploreKindsMap.remove(exploreKindsKey) exploreKindsMap.remove(exploreKindsKey)
} }
} }
fun BookSource.contains(word: String?): Boolean {
if (word.isNullOrEmpty()) {
return true
}
return bookSourceName.contains(word)
|| bookSourceUrl.contains(word)
|| bookSourceGroup?.contains(word) == true
|| bookSourceComment?.contains(word) == true
}
@@ -4,6 +4,7 @@ import io.legado.app.constant.AppLog
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.addType import io.legado.app.help.book.addType
@@ -352,13 +353,14 @@ object WebBook {
*/ */
fun preciseSearch( fun preciseSearch(
scope: CoroutineScope, scope: CoroutineScope,
bookSources: List<BookSource>, bookSourceParts: List<BookSourcePart>,
name: String, name: String,
author: String, author: String,
context: CoroutineContext = Dispatchers.IO, context: CoroutineContext = Dispatchers.IO,
): Coroutine<Pair<Book, BookSource>> { ): Coroutine<Pair<Book, BookSource>> {
return Coroutine.async(scope, context) { return Coroutine.async(scope, context) {
for (source in bookSources) { for (s in bookSourceParts) {
val source = s.getBookSource() ?: continue
val book = preciseSearchAwait(scope, source, name, author).getOrNull() val book = preciseSearchAwait(scope, source, name, author).getOrNull()
if (book != null) { if (book != null) {
return@async Pair(book, source) return@async Pair(book, source)
@@ -6,30 +6,39 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope 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.AppPattern import io.legado.app.constant.AppPattern
import io.legado.app.data.appDb import io.legado.app.data.appDb
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.help.config.AppConfig 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.model.webBook.WebBook
import io.legado.app.utils.mapParallelSafe
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.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 java.util.* 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.Executors import java.util.concurrent.Executors
import kotlin.math.min import kotlin.math.min
class ChangeCoverViewModel(application: Application) : BaseViewModel(application) { class ChangeCoverViewModel(application: Application) : BaseViewModel(application) {
private val threadCount = AppConfig.threadCount private val threadCount = AppConfig.threadCount
private var searchPool: ExecutorCoroutineDispatcher? = null private var searchPool: ExecutorCoroutineDispatcher? = null
private val tasks = CompositeCoroutine()
private var searchSuccess: ((SearchBook) -> Unit)? = null private var searchSuccess: ((SearchBook) -> Unit)? = null
private var upAdapter: (() -> Unit)? = null private var upAdapter: (() -> Unit)? = null
private var bookSourceList = arrayListOf<BookSource>() private var bookSourceParts = arrayListOf<BookSourcePart>()
private val defaultCover by lazy { private val defaultCover by lazy {
listOf( listOf(
SearchBook( SearchBook(
@@ -40,6 +49,7 @@ class ChangeCoverViewModel(application: Application) : BaseViewModel(application
) )
) )
} }
private var task: Job? = null
val searchStateData = MutableLiveData<Boolean>() val searchStateData = MutableLiveData<Boolean>()
var name: String = "" var name: String = ""
var author: String = "" var author: String = ""
@@ -73,9 +83,6 @@ class ChangeCoverViewModel(application: Application) : BaseViewModel(application
} }
}.flowOn(IO) }.flowOn(IO)
@Volatile
private var searchIndex = -1
fun initData(arguments: Bundle?) { fun initData(arguments: Bundle?) {
arguments?.let { bundle -> arguments?.let { bundle ->
bundle.getString("name")?.let { bundle.getString("name")?.let {
@@ -90,7 +97,6 @@ class ChangeCoverViewModel(application: Application) : BaseViewModel(application
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
} }
private fun startSearch() { private fun startSearch() {
@@ -98,71 +104,50 @@ class ChangeCoverViewModel(application: Application) : BaseViewModel(application
stopSearch() stopSearch()
searchBooks.clear() searchBooks.clear()
upAdapter?.invoke() upAdapter?.invoke()
bookSourceList.clear() bookSourceParts.clear()
bookSourceList.addAll(appDb.bookSourceDao.allEnabled) bookSourceParts.addAll(appDb.bookSourceDao.allEnabledPart)
searchStateData.postValue(true)
initSearchPool() initSearchPool()
for (i in 0 until threadCount) { search()
search()
}
} }
} }
@Synchronized
private fun search() { private fun search() {
if (searchIndex >= bookSourceList.lastIndex) { task = viewModelScope.launch(searchPool!!) {
return flow {
} for (bs in bookSourceParts) {
searchIndex++ bs.getBookSource()?.let {
val source = bookSourceList[searchIndex] emit(it)
if (source.getSearchRule().coverUrl.isNullOrBlank()) {
searchNext()
return
}
val task = WebBook
.searchBook(
viewModelScope,
source,
name,
context = searchPool!!,
executeContext = searchPool!!
)
.timeout(60000L)
.onSuccess(IO) {
it.firstOrNull()?.let { searchBook ->
if (searchBook.name == name && searchBook.author == author
&& !searchBook.coverUrl.isNullOrEmpty()
) {
appDb.searchBookDao.insert(searchBook)
searchSuccess?.invoke(searchBook)
} }
} }
} }.onStart {
.onFinally { searchStateData.postValue(true)
searchNext() }.mapParallelSafe(threadCount) {
} withTimeout(60000L) {
tasks.add(task) search(it)
}
}.onCompletion {
searchStateData.postValue(false)
}.catch {
AppLog.put("封面换源搜索出错\n${it.localizedMessage}", it)
}.collect()
}
} }
@Synchronized private suspend fun search(source: BookSource) {
private fun searchNext() { if (source.getSearchRule().coverUrl.isNullOrBlank()) {
if (searchIndex < bookSourceList.lastIndex) { return
search()
} else {
searchIndex++
} }
if (searchIndex >= bookSourceList.lastIndex + min( val searchBook = WebBook.searchBookAwait(source, name).firstOrNull() ?: return
bookSourceList.size, if (searchBook.name == name && searchBook.author == author
threadCount && !searchBook.coverUrl.isNullOrEmpty()
)
) { ) {
searchStateData.postValue(false) appDb.searchBookDao.insert(searchBook)
tasks.clear() searchSuccess?.invoke(searchBook)
} }
} }
fun startOrStopSearch() { fun startOrStopSearch() {
if (tasks.isEmpty) { if (task == null || !task!!.isActive) {
startSearch() startSearch()
} else { } else {
stopSearch() stopSearch()
@@ -170,7 +155,7 @@ class ChangeCoverViewModel(application: Application) : BaseViewModel(application
} }
private fun stopSearch() { private fun stopSearch() {
tasks.clear() task?.cancel()
searchPool?.close() searchPool?.close()
searchStateData.postValue(false) searchStateData.postValue(false)
} }
@@ -10,27 +10,34 @@ import androidx.recyclerview.widget.RecyclerView
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseDialogFragment import io.legado.app.base.BaseDialogFragment
import io.legado.app.base.adapter.ItemViewHolder import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.constant.AppLog
import io.legado.app.data.AppDatabase
import io.legado.app.data.appDb import io.legado.app.data.appDb
import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.BookSourcePart
import io.legado.app.databinding.DialogSearchScopeBinding import io.legado.app.databinding.DialogSearchScopeBinding
import io.legado.app.databinding.ItemCheckBoxBinding import io.legado.app.databinding.ItemCheckBoxBinding
import io.legado.app.databinding.ItemRadioButtonBinding import io.legado.app.databinding.ItemRadioButtonBinding
import io.legado.app.help.source.contains
import io.legado.app.lib.theme.primaryColor import io.legado.app.lib.theme.primaryColor
import io.legado.app.utils.applyTint import io.legado.app.utils.applyTint
import io.legado.app.utils.flowWithLifecycleAndDatabaseChange
import io.legado.app.utils.setLayout import io.legado.app.utils.setLayout
import io.legado.app.utils.viewbindingdelegate.viewBinding import io.legado.app.utils.viewbindingdelegate.viewBinding
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
class SearchScopeDialog : BaseDialogFragment(R.layout.dialog_search_scope) { class SearchScopeDialog : BaseDialogFragment(R.layout.dialog_search_scope) {
private val binding by viewBinding(DialogSearchScopeBinding::bind) private val binding by viewBinding(DialogSearchScopeBinding::bind)
private var sourceFlowJob: Job? = null
val callback: Callback get() = parentFragment as? Callback ?: activity as Callback val callback: Callback get() = parentFragment as? Callback ?: activity as Callback
var groups: List<String> = emptyList() var groups: List<String> = emptyList()
var sources: List<BookSource> = emptyList() val screenSources = arrayListOf<BookSourcePart>()
val screenSources = arrayListOf<BookSource>()
var screenText: String? = null var screenText: String? = null
val adapter by lazy { val adapter by lazy {
@@ -49,7 +56,6 @@ class SearchScopeDialog : BaseDialogFragment(R.layout.dialog_search_scope) {
initSearchView() initSearchView()
initOtherView() initOtherView()
initData() initData()
upData()
} }
private fun initMenu() { private fun initMenu() {
@@ -105,34 +111,49 @@ class SearchScopeDialog : BaseDialogFragment(R.layout.dialog_search_scope) {
groups = withContext(IO) { groups = withContext(IO) {
appDb.bookSourceDao.allEnabledGroups() appDb.bookSourceDao.allEnabledGroups()
} }
sources = withContext(IO) {
appDb.bookSourceDao.allEnabled
}
upData() upData()
} }
} }
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
private fun upData() { private fun upData() {
lifecycleScope.launch { if (binding.rbSource.isChecked) {
withContext(IO) { upBookSource(screenText)
if (binding.rbSource.isChecked) { } else {
sources.filter { source ->
source.contains(screenText)
}.let {
screenSources.clear()
screenSources.addAll(it)
}
}
}
adapter.notifyDataSetChanged() adapter.notifyDataSetChanged()
} }
} }
@SuppressLint("NotifyDataSetChanged")
private fun upBookSource(searchKey: String? = null) {
sourceFlowJob?.cancel()
sourceFlowJob = lifecycleScope.launch {
when {
searchKey.isNullOrEmpty() -> {
appDb.bookSourceDao.flowAll()
}
else -> {
appDb.bookSourceDao.flowSearch(searchKey)
}
}.flowWithLifecycleAndDatabaseChange(
lifecycle,
table = AppDatabase.BOOK_SOURCE_TABLE_NAME
).catch {
AppLog.put("多分组/书源界面更新书源出错", it)
}.flowOn(IO).conflate().collect { data ->
screenSources.clear()
screenSources.addAll(data)
adapter.notifyDataSetChanged()
delay(500)
}
}
}
inner class RecyclerAdapter : RecyclerView.Adapter<ItemViewHolder>() { inner class RecyclerAdapter : RecyclerView.Adapter<ItemViewHolder>() {
val selectGroups = arrayListOf<String>() val selectGroups = arrayListOf<String>()
var selectSource: BookSource? = null var selectSource: BookSourcePart? = null
override fun getItemViewType(position: Int): Int { override fun getItemViewType(position: Int): Int {
return if (binding.rbSource.isChecked) { return if (binding.rbSource.isChecked) {
@@ -165,6 +186,7 @@ class SearchScopeDialog : BaseDialogFragment(R.layout.dialog_search_scope) {
holder.binding.checkBox.text = it holder.binding.checkBox.text = it
} }
} }
is ItemRadioButtonBinding -> { is ItemRadioButtonBinding -> {
screenSources.getOrNull(position)?.let { screenSources.getOrNull(position)?.let {
holder.binding.radioButton.isChecked = selectSource == it holder.binding.radioButton.isChecked = selectSource == it
@@ -195,6 +217,7 @@ class SearchScopeDialog : BaseDialogFragment(R.layout.dialog_search_scope) {
} }
} }
} }
is ItemRadioButtonBinding -> { is ItemRadioButtonBinding -> {
screenSources.getOrNull(position)?.let { screenSources.getOrNull(position)?.let {
holder.binding.radioButton.isChecked = selectSource == it holder.binding.radioButton.isChecked = selectSource == it
@@ -27,11 +27,6 @@ import kotlinx.coroutines.isActive
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.io.OutputStreamWriter import java.io.OutputStreamWriter
import kotlin.collections.List
import kotlin.collections.Map
import kotlin.collections.forEach
import kotlin.collections.hashMapOf
import kotlin.collections.set
class BookshelfViewModel(application: Application) : BaseViewModel(application) { class BookshelfViewModel(application: Application) : BaseViewModel(application) {
val addBookProgressLiveData = MutableLiveData(-1) val addBookProgressLiveData = MutableLiveData(-1)
@@ -156,13 +151,13 @@ class BookshelfViewModel(application: Application) : BaseViewModel(application)
private fun importBookshelfByJson(json: String, groupId: Long) { private fun importBookshelfByJson(json: String, groupId: Long) {
execute { execute {
val bookSources = appDb.bookSourceDao.allEnabled val bookSourceParts = appDb.bookSourceDao.allEnabledPart
GSON.fromJsonArray<Map<String, String?>>(json).getOrThrow().forEach { bookInfo -> GSON.fromJsonArray<Map<String, String?>>(json).getOrThrow().forEach { bookInfo ->
if (!isActive) return@execute if (!isActive) return@execute
val name = bookInfo["name"] ?: "" val name = bookInfo["name"] ?: ""
val author = bookInfo["author"] ?: "" val author = bookInfo["author"] ?: ""
if (name.isNotEmpty() && appDb.bookDao.getBook(name, author) == null) { if (name.isNotEmpty() && appDb.bookDao.getBook(name, author) == null) {
WebBook.preciseSearch(this, bookSources, name, author) WebBook.preciseSearch(this, bookSourceParts, name, author)
.onSuccess { .onSuccess {
val book = it.first val book = it.first
if (groupId > 0) { if (groupId > 0) {