|
@@ -1,7 +1,11 @@
|
|
|
package io.legado.app.ui.book.changesource
|
|
package io.legado.app.ui.book.changesource
|
|
|
|
|
|
|
|
|
|
+import androidx.annotation.StringRes
|
|
|
import androidx.lifecycle.ViewModel
|
|
import androidx.lifecycle.ViewModel
|
|
|
import androidx.lifecycle.viewModelScope
|
|
import androidx.lifecycle.viewModelScope
|
|
|
|
|
+import io.legado.app.R
|
|
|
|
|
+import io.legado.app.constant.AppLog
|
|
|
|
|
+import io.legado.app.constant.AppPattern
|
|
|
import io.legado.app.data.dao.BookDao
|
|
import io.legado.app.data.dao.BookDao
|
|
|
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
|
|
@@ -15,13 +19,25 @@ import io.legado.app.domain.usecase.GetChapterContentUseCase
|
|
|
import io.legado.app.help.book.isWebFile
|
|
import io.legado.app.help.book.isWebFile
|
|
|
import io.legado.app.help.book.primaryStr
|
|
import io.legado.app.help.book.primaryStr
|
|
|
import io.legado.app.ui.book.search.SearchScope
|
|
import io.legado.app.ui.book.search.SearchScope
|
|
|
|
|
+import kotlinx.coroutines.CancellationException
|
|
|
import kotlinx.coroutines.Dispatchers.IO
|
|
import kotlinx.coroutines.Dispatchers.IO
|
|
|
|
|
+import kotlinx.coroutines.Dispatchers.Main
|
|
|
import kotlinx.coroutines.Job
|
|
import kotlinx.coroutines.Job
|
|
|
|
|
+import kotlinx.coroutines.flow.Flow
|
|
|
|
|
+import kotlinx.coroutines.flow.MutableSharedFlow
|
|
|
import kotlinx.coroutines.flow.MutableStateFlow
|
|
import kotlinx.coroutines.flow.MutableStateFlow
|
|
|
import kotlinx.coroutines.flow.StateFlow
|
|
import kotlinx.coroutines.flow.StateFlow
|
|
|
|
|
+import kotlinx.coroutines.flow.asSharedFlow
|
|
|
import kotlinx.coroutines.flow.asStateFlow
|
|
import kotlinx.coroutines.flow.asStateFlow
|
|
|
import kotlinx.coroutines.launch
|
|
import kotlinx.coroutines.launch
|
|
|
import kotlinx.coroutines.withContext
|
|
import kotlinx.coroutines.withContext
|
|
|
|
|
+import java.util.Collections
|
|
|
|
|
+import java.util.concurrent.ConcurrentHashMap
|
|
|
|
|
+
|
|
|
|
|
+sealed interface ChangeBookSourceEffect {
|
|
|
|
|
+ data class ShowMessage(val message: String) : ChangeBookSourceEffect
|
|
|
|
|
+ data class ShowMessageResource(@StringRes val messageRes: Int) : ChangeBookSourceEffect
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
class ChangeBookSourceComposeViewModel(
|
|
class ChangeBookSourceComposeViewModel(
|
|
|
private val changeSourceSearchUseCase: ChangeSourceSearchUseCase,
|
|
private val changeSourceSearchUseCase: ChangeSourceSearchUseCase,
|
|
@@ -62,17 +78,23 @@ class ChangeBookSourceComposeViewModel(
|
|
|
private val _searchDataFlow = MutableStateFlow<List<SearchBook>>(emptyList())
|
|
private val _searchDataFlow = MutableStateFlow<List<SearchBook>>(emptyList())
|
|
|
val searchDataFlow: StateFlow<List<SearchBook>> = _searchDataFlow.asStateFlow()
|
|
val searchDataFlow: StateFlow<List<SearchBook>> = _searchDataFlow.asStateFlow()
|
|
|
|
|
|
|
|
|
|
+ private val _emptyScopeName = MutableStateFlow<String?>(null)
|
|
|
|
|
+ val emptyScopeName = _emptyScopeName.asStateFlow()
|
|
|
|
|
+
|
|
|
|
|
+ private val _effects =
|
|
|
|
|
+ MutableSharedFlow<ChangeBookSourceEffect>(extraBufferCapacity = 16)
|
|
|
|
|
+ val effects = _effects.asSharedFlow()
|
|
|
|
|
+
|
|
|
|
|
+ @Volatile
|
|
|
var totalSourceCount: Int = 0
|
|
var totalSourceCount: Int = 0
|
|
|
private set
|
|
private set
|
|
|
|
|
|
|
|
fun getBookFromMap(key: String): Book? = bookMap[key]
|
|
fun getBookFromMap(key: String): Book? = bookMap[key]
|
|
|
|
|
|
|
|
fun findShelfConflict(book: Book, onResult: (Book?) -> Unit) {
|
|
fun findShelfConflict(book: Book, onResult: (Book?) -> Unit) {
|
|
|
- viewModelScope.launch {
|
|
|
|
|
- val conflict = withContext(IO) {
|
|
|
|
|
- bookDao.getShelfBookConflict(book.name, book.author)
|
|
|
|
|
- }
|
|
|
|
|
- onResult(conflict)
|
|
|
|
|
|
|
+ viewModelScope.launch(IO) {
|
|
|
|
|
+ val conflict = bookDao.getShelfBookConflict(book.name, book.author)
|
|
|
|
|
+ onMain { onResult(conflict) }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -89,53 +111,28 @@ class ChangeBookSourceComposeViewModel(
|
|
|
private var bookName: String = ""
|
|
private var bookName: String = ""
|
|
|
private var bookAuthor: String = ""
|
|
private var bookAuthor: String = ""
|
|
|
private var fromReadBookActivity: Boolean = false
|
|
private var fromReadBookActivity: Boolean = false
|
|
|
|
|
+ @Volatile
|
|
|
private var screenKey: String = ""
|
|
private var screenKey: String = ""
|
|
|
- private var knownEnabledSourceUrls: Set<String>? = null
|
|
|
|
|
private var cachedTocChapterCount = 0
|
|
private var cachedTocChapterCount = 0
|
|
|
- private val searchResults = mutableListOf<SearchBook>()
|
|
|
|
|
- private val bookMap = mutableMapOf<String, Book>()
|
|
|
|
|
- private val tocMap = mutableMapOf<String, List<BookChapter>>()
|
|
|
|
|
-
|
|
|
|
|
- init {
|
|
|
|
|
- viewModelScope.launch {
|
|
|
|
|
- searchRepository.enabledSources.collect { sources ->
|
|
|
|
|
- val sourceUrls = sources.mapTo(linkedSetOf()) { it.bookSourceUrl }
|
|
|
|
|
- val previousSourceUrls = knownEnabledSourceUrls
|
|
|
|
|
- knownEnabledSourceUrls = sourceUrls
|
|
|
|
|
- if (oldBook != null && previousSourceUrls != null) {
|
|
|
|
|
- val addedSources = sources.filter {
|
|
|
|
|
- it.bookSourceUrl !in previousSourceUrls
|
|
|
|
|
- }
|
|
|
|
|
- if (addedSources.isNotEmpty()) {
|
|
|
|
|
- startSearch(addedSources)
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ private val searchResults = Collections.synchronizedList(mutableListOf<SearchBook>())
|
|
|
|
|
+ private val bookMap = ConcurrentHashMap<String, Book>()
|
|
|
|
|
+ private val tocMap = ConcurrentHashMap<String, List<BookChapter>>()
|
|
|
|
|
|
|
|
fun initData(name: String, author: String, book: Book, fromReadBookActivity: Boolean) {
|
|
fun initData(name: String, author: String, book: Book, fromReadBookActivity: Boolean) {
|
|
|
this.oldBook = book
|
|
this.oldBook = book
|
|
|
this.bookName = name
|
|
this.bookName = name
|
|
|
- this.bookAuthor = author
|
|
|
|
|
|
|
+ this.bookAuthor = author.replace(AppPattern.authorRegex, "")
|
|
|
this.fromReadBookActivity = fromReadBookActivity
|
|
this.fromReadBookActivity = fromReadBookActivity
|
|
|
if (searchJob?.isActive != true) {
|
|
if (searchJob?.isActive != true) {
|
|
|
- viewModelScope.launch {
|
|
|
|
|
- val dbBooks = withContext(IO) { getDbSearchBooks() }
|
|
|
|
|
|
|
+ viewModelScope.launch(IO) {
|
|
|
|
|
+ val dbBooks = getDbSearchBooks()
|
|
|
if (dbBooks.isNotEmpty()) {
|
|
if (dbBooks.isNotEmpty()) {
|
|
|
- searchResults.clear()
|
|
|
|
|
- searchResults.addAll(dbBooks)
|
|
|
|
|
- searchResults.forEach { bookMap[it.primaryStr()] = it.toBook() }
|
|
|
|
|
- filterResults()
|
|
|
|
|
- val cachedOrigins = dbBooks.mapTo(hashSetOf()) { it.origin }
|
|
|
|
|
- val missingSources = withContext(IO) {
|
|
|
|
|
- SearchScope(ChangeSourceConfig.searchScope)
|
|
|
|
|
- .getBookSourceParts()
|
|
|
|
|
- .filter { it.bookSourceUrl !in cachedOrigins }
|
|
|
|
|
- }
|
|
|
|
|
- if (missingSources.isNotEmpty()) {
|
|
|
|
|
- startSearch(missingSources)
|
|
|
|
|
|
|
+ synchronized(searchResults) {
|
|
|
|
|
+ searchResults.clear()
|
|
|
|
|
+ searchResults.addAll(dbBooks)
|
|
|
}
|
|
}
|
|
|
|
|
+ dbBooks.forEach { bookMap[it.primaryStr()] = it.toBook() }
|
|
|
|
|
+ filterResults()
|
|
|
} else {
|
|
} else {
|
|
|
startSearch()
|
|
startSearch()
|
|
|
}
|
|
}
|
|
@@ -144,24 +141,16 @@ class ChangeBookSourceComposeViewModel(
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private fun getDbSearchBooks(): List<SearchBook> {
|
|
private fun getDbSearchBooks(): List<SearchBook> {
|
|
|
- val searchScope = SearchScope(ChangeSourceConfig.searchScope)
|
|
|
|
|
- val group = when {
|
|
|
|
|
- searchScope.isAll() || searchScope.isSource() -> ""
|
|
|
|
|
- else -> {
|
|
|
|
|
- val names = searchScope.displayNames
|
|
|
|
|
- if (names.size == 1) names.first() else ""
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- val bookAuthor = if (checkAuthor) bookAuthor else ""
|
|
|
|
|
- return if (screenKey.isEmpty()) {
|
|
|
|
|
- io.legado.app.data.appDb.searchBookDao.changeSourceByGroup(
|
|
|
|
|
- bookName, bookAuthor, group
|
|
|
|
|
- )
|
|
|
|
|
- } else {
|
|
|
|
|
- io.legado.app.data.appDb.searchBookDao.changeSourceSearch(
|
|
|
|
|
- bookName, bookAuthor, screenKey, group
|
|
|
|
|
- )
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ val scope = SearchScope(ChangeSourceConfig.searchScope)
|
|
|
|
|
+ val author = if (checkAuthor) bookAuthor else ""
|
|
|
|
|
+ val dbBooks = io.legado.app.data.appDb.searchBookDao.changeSourceByGroup(
|
|
|
|
|
+ bookName, author, ""
|
|
|
|
|
+ )
|
|
|
|
|
+ if (scope.isAll()) return dbBooks
|
|
|
|
|
+
|
|
|
|
|
+ val allowedOrigins = scope.getBookSourceParts()
|
|
|
|
|
+ .mapTo(hashSetOf()) { it.bookSourceUrl }
|
|
|
|
|
+ return dbBooks.filter { it.origin in allowedOrigins }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
fun startSearch() {
|
|
fun startSearch() {
|
|
@@ -172,10 +161,9 @@ class ChangeBookSourceComposeViewModel(
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
fun startSearch(origin: String) {
|
|
fun startSearch(origin: String) {
|
|
|
- viewModelScope.launch {
|
|
|
|
|
- val source = withContext(IO) {
|
|
|
|
|
- io.legado.app.data.appDb.bookSourceDao.getBookSourcePart(origin)
|
|
|
|
|
- } ?: return@launch
|
|
|
|
|
|
|
+ viewModelScope.launch(IO) {
|
|
|
|
|
+ val source = io.legado.app.data.appDb.bookSourceDao
|
|
|
|
|
+ .getBookSourcePart(origin) ?: return@launch
|
|
|
startSearch(listOf(source))
|
|
startSearch(listOf(source))
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -194,13 +182,16 @@ class ChangeBookSourceComposeViewModel(
|
|
|
) {
|
|
) {
|
|
|
val book = oldBook ?: return
|
|
val book = oldBook ?: return
|
|
|
stopSearch()
|
|
stopSearch()
|
|
|
- val removedResults = if (replacedOrigins == null) {
|
|
|
|
|
- searchResults.toList()
|
|
|
|
|
- } else {
|
|
|
|
|
- searchResults.filter { it.origin in replacedOrigins }
|
|
|
|
|
|
|
+ val removedResults = synchronized(searchResults) {
|
|
|
|
|
+ val results = if (replacedOrigins == null) {
|
|
|
|
|
+ searchResults.toList()
|
|
|
|
|
+ } else {
|
|
|
|
|
+ searchResults.filter { it.origin in replacedOrigins }
|
|
|
|
|
+ }
|
|
|
|
|
+ searchResults.removeAll(results.toSet())
|
|
|
|
|
+ results
|
|
|
}
|
|
}
|
|
|
val removedKeys = removedResults.mapTo(hashSetOf()) { it.primaryStr() }
|
|
val removedKeys = removedResults.mapTo(hashSetOf()) { it.primaryStr() }
|
|
|
- searchResults.removeAll(removedResults.toSet())
|
|
|
|
|
removedKeys.forEach {
|
|
removedKeys.forEach {
|
|
|
bookMap.remove(it)
|
|
bookMap.remove(it)
|
|
|
tocMap.remove(it)
|
|
tocMap.remove(it)
|
|
@@ -211,65 +202,108 @@ class ChangeBookSourceComposeViewModel(
|
|
|
_changeSourceProgress.value = 0 to ""
|
|
_changeSourceProgress.value = 0 to ""
|
|
|
filterResults()
|
|
filterResults()
|
|
|
|
|
|
|
|
- searchJob = viewModelScope.launch {
|
|
|
|
|
- if (removedResults.isNotEmpty()) {
|
|
|
|
|
- withContext(IO) {
|
|
|
|
|
|
|
+ searchJob = viewModelScope.launch(IO) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (removedResults.isNotEmpty()) {
|
|
|
io.legado.app.data.appDb.searchBookDao.delete(*removedResults.toTypedArray())
|
|
io.legado.app.data.appDb.searchBookDao.delete(*removedResults.toTypedArray())
|
|
|
}
|
|
}
|
|
|
|
|
+ collectSearchEvents(
|
|
|
|
|
+ events = changeSourceSearchUseCase.search(
|
|
|
|
|
+ name = bookName,
|
|
|
|
|
+ author = bookAuthor,
|
|
|
|
|
+ scope = scope,
|
|
|
|
|
+ oldBook = book,
|
|
|
|
|
+ fromReadBookActivity = fromReadBookActivity,
|
|
|
|
|
+ ),
|
|
|
|
|
+ emptyScope = scope,
|
|
|
|
|
+ )
|
|
|
|
|
+ } catch (error: CancellationException) {
|
|
|
|
|
+ throw error
|
|
|
|
|
+ } catch (error: Throwable) {
|
|
|
|
|
+ reportSearchError(error)
|
|
|
}
|
|
}
|
|
|
- val pendingBooks = arrayListOf<SearchBook>()
|
|
|
|
|
- try {
|
|
|
|
|
- changeSourceSearchUseCase.search(
|
|
|
|
|
- name = book.name,
|
|
|
|
|
- author = book.author,
|
|
|
|
|
- scope = scope,
|
|
|
|
|
- oldBook = book,
|
|
|
|
|
- fromReadBookActivity = fromReadBookActivity,
|
|
|
|
|
- ).collect { event ->
|
|
|
|
|
- when (event) {
|
|
|
|
|
- is ChangeSourceSearchEvent.Started -> {
|
|
|
|
|
- _isSearching.value = true
|
|
|
|
|
- totalSourceCount = event.totalSources
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- is ChangeSourceSearchEvent.Progress -> {
|
|
|
|
|
- totalSourceCount = event.totalSources
|
|
|
|
|
- _changeSourceProgress.value =
|
|
|
|
|
- event.processedSources to event.sourceName
|
|
|
|
|
- filterResults()
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ private suspend fun collectSearchEvents(
|
|
|
|
|
+ events: Flow<ChangeSourceSearchEvent>,
|
|
|
|
|
+ emptyScope: SearchScope? = null,
|
|
|
|
|
+ ) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ events.collect { event ->
|
|
|
|
|
+ when (event) {
|
|
|
|
|
+ is ChangeSourceSearchEvent.Started -> {
|
|
|
|
|
+ _isSearching.value = true
|
|
|
|
|
+ totalSourceCount = event.totalSources
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ is ChangeSourceSearchEvent.Progress -> {
|
|
|
|
|
+ totalSourceCount = event.totalSources
|
|
|
|
|
+ _changeSourceProgress.value =
|
|
|
|
|
+ event.processedSources to event.sourceName
|
|
|
|
|
+ filterResults()
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- is ChangeSourceSearchEvent.Result -> {
|
|
|
|
|
- val key = event.searchBook.primaryStr()
|
|
|
|
|
- searchResults.add(event.searchBook)
|
|
|
|
|
- pendingBooks.add(event.searchBook)
|
|
|
|
|
- bookMap[key] = event.book
|
|
|
|
|
- event.toc?.let { toc ->
|
|
|
|
|
- if (cachedTocChapterCount < MAX_CACHED_CHAPTERS) {
|
|
|
|
|
- cachedTocChapterCount += toc.size
|
|
|
|
|
- tocMap[key] = toc
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ is ChangeSourceSearchEvent.Result -> {
|
|
|
|
|
+ event.searchBook.releaseHtmlData()
|
|
|
|
|
+ searchRepository.saveSearchBook(event.searchBook)
|
|
|
|
|
+ val key = event.searchBook.primaryStr()
|
|
|
|
|
+ searchResults.add(event.searchBook)
|
|
|
|
|
+ bookMap[key] = event.book
|
|
|
|
|
+ event.toc?.let { toc ->
|
|
|
|
|
+ if (cachedTocChapterCount < MAX_CACHED_CHAPTERS) {
|
|
|
|
|
+ cachedTocChapterCount += toc.size
|
|
|
|
|
+ tocMap[key] = toc
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- is ChangeSourceSearchEvent.Finished -> {
|
|
|
|
|
- filterResults()
|
|
|
|
|
- searchRepository.saveSearchBooks(pendingBooks)
|
|
|
|
|
|
|
+ is ChangeSourceSearchEvent.Finished -> {
|
|
|
|
|
+ filterResults()
|
|
|
|
|
+ val isResultEmpty = synchronized(searchResults) {
|
|
|
|
|
+ searchResults.isEmpty()
|
|
|
|
|
+ }
|
|
|
|
|
+ if (event.isEmpty && isResultEmpty && emptyScope?.isAll() == false) {
|
|
|
|
|
+ _emptyScopeName.value = emptyScope.display
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- } finally {
|
|
|
|
|
- _isSearching.value = false
|
|
|
|
|
}
|
|
}
|
|
|
|
|
+ } catch (error: CancellationException) {
|
|
|
|
|
+ throw error
|
|
|
|
|
+ } catch (error: Throwable) {
|
|
|
|
|
+ reportSearchError(error)
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ _isSearching.value = false
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private fun reportSearchError(error: Throwable) {
|
|
|
|
|
+ AppLog.put("换源搜索出错\n${error.localizedMessage}", error)
|
|
|
|
|
+ _effects.tryEmit(
|
|
|
|
|
+ ChangeBookSourceEffect.ShowMessage(
|
|
|
|
|
+ error.localizedMessage?.takeIf { it.isNotBlank() } ?: error.toString()
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
fun stopSearch() {
|
|
fun stopSearch() {
|
|
|
searchJob?.cancel()
|
|
searchJob?.cancel()
|
|
|
searchJob = null
|
|
searchJob = null
|
|
|
_isSearching.value = false
|
|
_isSearching.value = false
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ fun confirmEmptyScopeSearch() {
|
|
|
|
|
+ _emptyScopeName.value = null
|
|
|
|
|
+ searchScope.update("")
|
|
|
|
|
+ updateScopeState()
|
|
|
|
|
+ startSearch()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ fun dismissEmptyScopeSearch() {
|
|
|
|
|
+ _emptyScopeName.value = null
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
fun screen(key: String?) {
|
|
fun screen(key: String?) {
|
|
|
screenKey = key?.trim() ?: ""
|
|
screenKey = key?.trim() ?: ""
|
|
|
filterResults()
|
|
filterResults()
|
|
@@ -288,11 +322,15 @@ class ChangeBookSourceComposeViewModel(
|
|
|
fun resume() = Unit
|
|
fun resume() = Unit
|
|
|
|
|
|
|
|
private fun filterResults() {
|
|
private fun filterResults() {
|
|
|
- val filtered = if (screenKey.isEmpty()) {
|
|
|
|
|
- searchResults.toList()
|
|
|
|
|
- } else {
|
|
|
|
|
- searchResults.filter {
|
|
|
|
|
- it.name.contains(screenKey) || it.originName.contains(screenKey)
|
|
|
|
|
|
|
+ val key = screenKey
|
|
|
|
|
+ val filtered = synchronized(searchResults) {
|
|
|
|
|
+ if (key.isEmpty()) {
|
|
|
|
|
+ searchResults.toList()
|
|
|
|
|
+ } else {
|
|
|
|
|
+ searchResults.filter {
|
|
|
|
|
+ it.originName.contains(key) ||
|
|
|
|
|
+ it.latestChapterTitle?.contains(key) == true
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
val comparator = if (ChangeSourceConfig.loadWordCount) {
|
|
val comparator = if (ChangeSourceConfig.loadWordCount) {
|
|
@@ -311,8 +349,8 @@ class ChangeBookSourceComposeViewModel(
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private fun getChapterNum(text: String?): Int {
|
|
private fun getChapterNum(text: String?): Int {
|
|
|
- if (text.isNullOrBlank()) return 0
|
|
|
|
|
- return chapterNumRegex.find(text)?.groupValues?.get(1)?.toIntOrNull() ?: 0
|
|
|
|
|
|
|
+ if (text.isNullOrBlank()) return -1
|
|
|
|
|
+ return chapterNumRegex.find(text)?.groupValues?.get(1)?.toIntOrNull() ?: -1
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
fun getToc(
|
|
fun getToc(
|
|
@@ -321,31 +359,29 @@ class ChangeBookSourceComposeViewModel(
|
|
|
onError: (e: Throwable) -> Unit,
|
|
onError: (e: Throwable) -> Unit,
|
|
|
) {
|
|
) {
|
|
|
stopSearch()
|
|
stopSearch()
|
|
|
- viewModelScope.launch {
|
|
|
|
|
|
|
+ viewModelScope.launch(IO) {
|
|
|
try {
|
|
try {
|
|
|
val cachedToc = tocMap[book.primaryStr()]
|
|
val cachedToc = tocMap[book.primaryStr()]
|
|
|
if (cachedToc != null) {
|
|
if (cachedToc != null) {
|
|
|
- val source = withContext(IO) {
|
|
|
|
|
|
|
+ val source =
|
|
|
io.legado.app.data.appDb.bookSourceDao.getBookSource(book.origin)
|
|
io.legado.app.data.appDb.bookSourceDao.getBookSource(book.origin)
|
|
|
- }
|
|
|
|
|
if (source != null) {
|
|
if (source != null) {
|
|
|
- onSuccess(cachedToc, source)
|
|
|
|
|
|
|
+ onMain { onSuccess(cachedToc, source) }
|
|
|
return@launch
|
|
return@launch
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
if (book.isWebFile) {
|
|
if (book.isWebFile) {
|
|
|
- val source = withContext(IO) {
|
|
|
|
|
|
|
+ val source =
|
|
|
io.legado.app.data.appDb.bookSourceDao.getBookSource(book.origin)
|
|
io.legado.app.data.appDb.bookSourceDao.getBookSource(book.origin)
|
|
|
- }
|
|
|
|
|
?: throw io.legado.app.exception.NoStackTraceException("书源不存在")
|
|
?: throw io.legado.app.exception.NoStackTraceException("书源不存在")
|
|
|
- onSuccess(emptyList(), source)
|
|
|
|
|
|
|
+ onMain { onSuccess(emptyList(), source) }
|
|
|
return@launch
|
|
return@launch
|
|
|
}
|
|
}
|
|
|
val (toc, source) = getChapterContentUseCase.getToc(book)
|
|
val (toc, source) = getChapterContentUseCase.getToc(book)
|
|
|
tocMap[book.primaryStr()] = toc
|
|
tocMap[book.primaryStr()] = toc
|
|
|
- onSuccess(toc, source)
|
|
|
|
|
|
|
+ onMain { onSuccess(toc, source) }
|
|
|
} catch (e: Exception) {
|
|
} catch (e: Exception) {
|
|
|
- onError(e)
|
|
|
|
|
|
|
+ onMain { onError(e) }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -371,24 +407,52 @@ class ChangeBookSourceComposeViewModel(
|
|
|
if (ChangeSourceConfig.loadWordCount == enabled) return
|
|
if (ChangeSourceConfig.loadWordCount == enabled) return
|
|
|
ChangeSourceConfig.loadWordCount = enabled
|
|
ChangeSourceConfig.loadWordCount = enabled
|
|
|
if (enabled) {
|
|
if (enabled) {
|
|
|
- startSearch()
|
|
|
|
|
|
|
+ refreshMissingWordCounts()
|
|
|
} else {
|
|
} else {
|
|
|
refresh()
|
|
refresh()
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- fun refresh() {
|
|
|
|
|
- viewModelScope.launch {
|
|
|
|
|
- val dbBooks = withContext(IO) { getDbSearchBooks() }
|
|
|
|
|
- searchResults.clear()
|
|
|
|
|
|
|
+ private fun refreshMissingWordCounts() {
|
|
|
|
|
+ val book = oldBook ?: return
|
|
|
|
|
+ stopSearch()
|
|
|
|
|
+ val refreshBooks = synchronized(searchResults) {
|
|
|
|
|
+ searchResults
|
|
|
|
|
+ .filter { it.chapterWordCountText == null }
|
|
|
|
|
+ .also { searchResults.removeAll(it.toSet()) }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (refreshBooks.isEmpty()) {
|
|
|
|
|
+ filterResults()
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ filterResults()
|
|
|
|
|
+ _changeSourceProgress.value = 0 to ""
|
|
|
|
|
+ searchJob = viewModelScope.launch(IO) {
|
|
|
|
|
+ collectSearchEvents(
|
|
|
|
|
+ changeSourceSearchUseCase.refresh(
|
|
|
|
|
+ books = refreshBooks,
|
|
|
|
|
+ oldBook = book,
|
|
|
|
|
+ fromReadBookActivity = fromReadBookActivity,
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ fun refresh(startSearchIfEmpty: Boolean = false) {
|
|
|
|
|
+ viewModelScope.launch(IO) {
|
|
|
|
|
+ val dbBooks = getDbSearchBooks()
|
|
|
|
|
+ synchronized(searchResults) {
|
|
|
|
|
+ searchResults.clear()
|
|
|
|
|
+ searchResults.addAll(dbBooks)
|
|
|
|
|
+ }
|
|
|
bookMap.clear()
|
|
bookMap.clear()
|
|
|
tocMap.clear()
|
|
tocMap.clear()
|
|
|
cachedTocChapterCount = 0
|
|
cachedTocChapterCount = 0
|
|
|
if (dbBooks.isNotEmpty()) {
|
|
if (dbBooks.isNotEmpty()) {
|
|
|
- searchResults.addAll(dbBooks)
|
|
|
|
|
- searchResults.forEach { bookMap[it.primaryStr()] = it.toBook() }
|
|
|
|
|
- filterResults()
|
|
|
|
|
- } else {
|
|
|
|
|
|
|
+ dbBooks.forEach { bookMap[it.primaryStr()] = it.toBook() }
|
|
|
|
|
+ }
|
|
|
|
|
+ filterResults()
|
|
|
|
|
+ if (dbBooks.isEmpty() && startSearchIfEmpty) {
|
|
|
startSearch()
|
|
startSearch()
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -406,30 +470,41 @@ class ChangeBookSourceComposeViewModel(
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
fun disableSource(searchBook: SearchBook) {
|
|
fun disableSource(searchBook: SearchBook) {
|
|
|
- changeSourceSearchUseCase.disableSource(searchBook)
|
|
|
|
|
searchResults.remove(searchBook)
|
|
searchResults.remove(searchBook)
|
|
|
filterResults()
|
|
filterResults()
|
|
|
|
|
+ viewModelScope.launch(IO) {
|
|
|
|
|
+ changeSourceSearchUseCase.disableSource(searchBook)
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
fun del(searchBook: SearchBook) {
|
|
fun del(searchBook: SearchBook) {
|
|
|
- changeSourceSearchUseCase.deleteSource(searchBook)
|
|
|
|
|
searchResults.remove(searchBook)
|
|
searchResults.remove(searchBook)
|
|
|
filterResults()
|
|
filterResults()
|
|
|
|
|
+ viewModelScope.launch(IO) {
|
|
|
|
|
+ changeSourceSearchUseCase.deleteSource(searchBook)
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
fun autoChangeSource(
|
|
fun autoChangeSource(
|
|
|
bookType: Int?,
|
|
bookType: Int?,
|
|
|
onSuccess: (book: Book, toc: List<BookChapter>, source: BookSource) -> Unit,
|
|
onSuccess: (book: Book, toc: List<BookChapter>, source: BookSource) -> Unit,
|
|
|
) {
|
|
) {
|
|
|
- viewModelScope.launch {
|
|
|
|
|
- val found = searchResults.firstOrNull { it.type == bookType }
|
|
|
|
|
- if (found != null) {
|
|
|
|
|
|
|
+ viewModelScope.launch(IO) {
|
|
|
|
|
+ val matchingBooks = synchronized(searchResults) {
|
|
|
|
|
+ searchResults.filter { it.type == bookType }
|
|
|
|
|
+ }
|
|
|
|
|
+ for (found in matchingBooks) {
|
|
|
try {
|
|
try {
|
|
|
- val (toc, source) = getChapterContentUseCase.getToc(found.toBook())
|
|
|
|
|
- onSuccess(found.toBook(), toc, source)
|
|
|
|
|
|
|
+ val book = found.toBook()
|
|
|
|
|
+ val (toc, source) = getChapterContentUseCase.getToc(book)
|
|
|
|
|
+ onMain { onSuccess(book, toc, source) }
|
|
|
|
|
+ return@launch
|
|
|
} catch (_: Exception) {
|
|
} catch (_: Exception) {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+ _effects.tryEmit(
|
|
|
|
|
+ ChangeBookSourceEffect.ShowMessageResource(R.string.auto_change_source_failed)
|
|
|
|
|
+ )
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -502,6 +577,11 @@ class ChangeBookSourceComposeViewModel(
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private fun saveScope() {
|
|
private fun saveScope() {
|
|
|
|
|
+ updateScopeState()
|
|
|
|
|
+ refresh(startSearchIfEmpty = true)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private fun updateScopeState() {
|
|
|
ChangeSourceConfig.searchScope = searchScope.toString()
|
|
ChangeSourceConfig.searchScope = searchScope.toString()
|
|
|
_scopeUiState.value = ScopeUiState(
|
|
_scopeUiState.value = ScopeUiState(
|
|
|
isAll = searchScope.isAll(),
|
|
isAll = searchScope.isAll(),
|
|
@@ -509,7 +589,12 @@ class ChangeBookSourceComposeViewModel(
|
|
|
displayNames = searchScope.displayNames,
|
|
displayNames = searchScope.displayNames,
|
|
|
sourceUrls = searchScope.sourceUrls
|
|
sourceUrls = searchScope.sourceUrls
|
|
|
)
|
|
)
|
|
|
- refresh()
|
|
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private suspend fun onMain(block: () -> Unit) {
|
|
|
|
|
+ withContext(Main.immediate) {
|
|
|
|
|
+ block()
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private companion object {
|
|
private companion object {
|