书籍匹配URL #282、#203

This commit is contained in:
HapeLee
2025-11-11 00:56:25 +08:00
parent 59cf9b6389
commit 76feb9202a
7 changed files with 99 additions and 45 deletions
@@ -11,6 +11,7 @@ import io.legado.app.data.entities.SearchBook
import io.legado.app.databinding.ActivityExploreShowBinding import io.legado.app.databinding.ActivityExploreShowBinding
import io.legado.app.databinding.ViewLoadMoreBinding import io.legado.app.databinding.ViewLoadMoreBinding
import io.legado.app.ui.book.info.BookInfoActivity import io.legado.app.ui.book.info.BookInfoActivity
import io.legado.app.ui.book.search.BookShelfState
import io.legado.app.ui.widget.recycler.LoadMoreView import io.legado.app.ui.widget.recycler.LoadMoreView
import io.legado.app.utils.applyNavigationBarPadding import io.legado.app.utils.applyNavigationBarPadding
import io.legado.app.utils.startActivity import io.legado.app.utils.startActivity
@@ -81,8 +82,8 @@ class ExploreShowActivity : VMBaseActivity<ActivityExploreShowBinding, ExploreSh
} }
} }
override fun isInBookshelf(name: String, author: String): Boolean { override fun getBookShelfState(name: String, author: String, url: String?): BookShelfState {
return viewModel.isInBookShelf(name, author) return viewModel.isInBookShelf(name, author, url)
} }
override fun showBookInfo(book: Book) { override fun showBookInfo(book: Book) {
@@ -12,6 +12,7 @@ import io.legado.app.data.entities.Book
import io.legado.app.data.entities.SearchBook import io.legado.app.data.entities.SearchBook
import io.legado.app.databinding.ItemSearchBinding import io.legado.app.databinding.ItemSearchBinding
import io.legado.app.help.config.AppConfig import io.legado.app.help.config.AppConfig
import io.legado.app.ui.book.search.BookShelfState
import io.legado.app.ui.widget.text.AccentBgTextView import io.legado.app.ui.widget.text.AccentBgTextView
import io.legado.app.utils.dpToPx import io.legado.app.utils.dpToPx
import io.legado.app.utils.gone import io.legado.app.utils.gone
@@ -46,7 +47,19 @@ class ExploreShowAdapter(context: Context, val callBack: CallBack) :
binding.run { binding.run {
tvName.text = item.name tvName.text = item.name
tvAuthor.text = context.getString(R.string.author_show, item.author) tvAuthor.text = context.getString(R.string.author_show, item.author)
ivInBookshelf.isVisible = callBack.isInBookshelf(item.name, item.author) when (callBack.getBookShelfState(item.name, item.author, item.bookUrl)) {
BookShelfState.IN_SHELF -> {
ivInBookshelf.isVisible = true
tvBookshelf.text = context.getString(R.string.remove_from_bookshelf)
}
BookShelfState.SAME_NAME_AUTHOR -> {
ivInBookshelf.isVisible = true
tvBookshelf.text = context.getString(R.string.same_name_book)
}
BookShelfState.NOT_IN_SHELF -> {
ivInBookshelf.isVisible = false
}
}
if (item.latestChapterTitle.isNullOrEmpty()) { if (item.latestChapterTitle.isNullOrEmpty()) {
tvLasted.gone() tvLasted.gone()
} else { } else {
@@ -90,8 +103,21 @@ class ExploreShowAdapter(context: Context, val callBack: CallBack) :
binding.run { binding.run {
bundle.keySet().forEach { bundle.keySet().forEach {
when (it) { when (it) {
"isInBookshelf" -> ivInBookshelf.isVisible = "isInBookshelf" -> {
callBack.isInBookshelf(item.name, item.author) when (callBack.getBookShelfState(item.name, item.author, item.bookUrl)) {
BookShelfState.IN_SHELF -> {
ivInBookshelf.isVisible = true
tvBookshelf.text = context.getString(R.string.remove_from_bookshelf)
}
BookShelfState.SAME_NAME_AUTHOR -> {
ivInBookshelf.isVisible = true
tvBookshelf.text = context.getString(R.string.same_name_book)
}
BookShelfState.NOT_IN_SHELF -> {
ivInBookshelf.isVisible = false
}
}
}
} }
} }
} }
@@ -109,7 +135,7 @@ class ExploreShowAdapter(context: Context, val callBack: CallBack) :
/** /**
* 是否已经加入书架 * 是否已经加入书架
*/ */
fun isInBookshelf(name: String, author: String): Boolean fun getBookShelfState(name: String, author: String, url: String?): BookShelfState
fun showBookInfo(book: Book) fun showBookInfo(book: Book)
} }
@@ -12,6 +12,8 @@ import io.legado.app.data.entities.BookSource
import io.legado.app.data.entities.SearchBook import io.legado.app.data.entities.SearchBook
import io.legado.app.help.book.isNotShelf import io.legado.app.help.book.isNotShelf
import io.legado.app.model.webBook.WebBook import io.legado.app.model.webBook.WebBook
import io.legado.app.ui.book.search.BookKey
import io.legado.app.ui.book.search.BookShelfState
import io.legado.app.utils.printOnDebug import io.legado.app.utils.printOnDebug
import io.legado.app.utils.stackTraceStr import io.legado.app.utils.stackTraceStr
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
@@ -23,7 +25,7 @@ import java.util.concurrent.ConcurrentHashMap
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
class ExploreShowViewModel(application: Application) : BaseViewModel(application) { class ExploreShowViewModel(application: Application) : BaseViewModel(application) {
val bookshelf: MutableSet<String> = ConcurrentHashMap.newKeySet() val bookshelf: MutableSet<BookKey> = ConcurrentHashMap.newKeySet()
val upAdapterLiveData = MutableLiveData<String>() val upAdapterLiveData = MutableLiveData<String>()
val booksData = MutableLiveData<List<SearchBook>>() val booksData = MutableLiveData<List<SearchBook>>()
val errorLiveData = MutableLiveData<String>() val errorLiveData = MutableLiveData<String>()
@@ -35,18 +37,12 @@ class ExploreShowViewModel(application: Application) : BaseViewModel(application
init { init {
execute { execute {
appDb.bookDao.flowAll().mapLatest { books -> appDb.bookDao.flowAll().mapLatest { books ->
val keys = arrayListOf<String>()
books.filterNot { it.isNotShelf } books.filterNot { it.isNotShelf }
.forEach {
keys.add("${it.name}-${it.author}")
keys.add(it.name)
}
keys
}.catch { }.catch {
AppLog.put("发现列表界面获取书籍数据失败\n${it.localizedMessage}", it) AppLog.put("搜索界面获取书籍列表失败\n${it.localizedMessage}", it)
}.collect { }.collect { books ->
bookshelf.clear() bookshelf.clear()
bookshelf.addAll(it) bookshelf.addAll(books.map { BookKey(it.name, it.author, it.bookUrl) })
upAdapterLiveData.postValue("isInBookshelf") upAdapterLiveData.postValue("isInBookshelf")
} }
}.onError { }.onError {
@@ -82,12 +78,14 @@ class ExploreShowViewModel(application: Application) : BaseViewModel(application
} }
} }
fun isInBookShelf(name: String, author: String): Boolean { fun isInBookShelf(name: String, author: String, url: String?): BookShelfState {
return if (author.isNotBlank()) { val exactMatch = bookshelf.any { it.name == name && it.author == author && it.url == url }
bookshelf.contains("$name-$author") if (exactMatch) return BookShelfState.IN_SHELF
} else {
bookshelf.contains(name) val sameNameAuthor = bookshelf.any { it.name == name && it.author == author && it.url != url }
} if (sameNameAuthor) return BookShelfState.SAME_NAME_AUTHOR
return BookShelfState.NOT_IN_SHELF
} }
} }
@@ -576,8 +576,8 @@ class SearchActivity : VMBaseActivity<ActivityBookSearchBinding, SearchViewModel
/** /**
* 是否已经加入书架 * 是否已经加入书架
*/ */
override fun isInBookshelf(name: String, author: String): Boolean { override fun getBookShelfState(name: String, author: String, url: String?): BookShelfState {
return viewModel.isInBookShelf(name, author) return viewModel.isInBookShelf(name, author, url)
} }
/** /**
@@ -86,8 +86,19 @@ class SearchAdapter(context: Context, val callBack: CallBack) :
binding.run { binding.run {
tvName.text = searchBook.name tvName.text = searchBook.name
tvAuthor.text = context.getString(R.string.author_show, searchBook.author) tvAuthor.text = context.getString(R.string.author_show, searchBook.author)
ivInBookshelf.isVisible = when (callBack.getBookShelfState(searchBook.name, searchBook.author, searchBook.bookUrl)) {
callBack.isInBookshelf(searchBook.name, searchBook.author) BookShelfState.IN_SHELF -> {
ivInBookshelf.isVisible = true
tvBookshelf.text = context.getString(R.string.remove_from_bookshelf)
}
BookShelfState.SAME_NAME_AUTHOR -> {
ivInBookshelf.isVisible = true
tvBookshelf.text = context.getString(R.string.same_name_book)
}
BookShelfState.NOT_IN_SHELF -> {
ivInBookshelf.isVisible = false
}
}
bvOriginCount.text = searchBook.origins.size.toString() bvOriginCount.text = searchBook.origins.size.toString()
upLasted(binding, searchBook.latestChapterTitle) upLasted(binding, searchBook.latestChapterTitle)
tvIntroduce.text = searchBook.trimIntro(context) tvIntroduce.text = searchBook.trimIntro(context)
@@ -110,8 +121,21 @@ class SearchAdapter(context: Context, val callBack: CallBack) :
"last" -> upLasted(binding, searchBook.latestChapterTitle) "last" -> upLasted(binding, searchBook.latestChapterTitle)
"intro" -> tvIntroduce.text = searchBook.trimIntro(context) "intro" -> tvIntroduce.text = searchBook.trimIntro(context)
"kind" -> upKind(binding, searchBook.getKindList()) "kind" -> upKind(binding, searchBook.getKindList())
"isInBookshelf" -> ivInBookshelf.isVisible = "isInBookshelf" -> {
callBack.isInBookshelf(searchBook.name, searchBook.author) when (callBack.getBookShelfState(searchBook.name, searchBook.author, searchBook.bookUrl)) {
BookShelfState.IN_SHELF -> {
ivInBookshelf.isVisible = true
tvBookshelf.text = context.getString(R.string.remove_from_bookshelf)
}
BookShelfState.SAME_NAME_AUTHOR -> {
ivInBookshelf.isVisible = true
tvBookshelf.text = context.getString(R.string.same_name_book)
}
BookShelfState.NOT_IN_SHELF -> {
ivInBookshelf.isVisible = false
}
}
}
"cover" -> ivCover.load( "cover" -> ivCover.load(
searchBook.coverUrl, searchBook.coverUrl,
searchBook.name, searchBook.name,
@@ -165,7 +189,7 @@ class SearchAdapter(context: Context, val callBack: CallBack) :
/** /**
* 是否已经加入书架 * 是否已经加入书架
*/ */
fun isInBookshelf(name: String, author: String): Boolean fun getBookShelfState(name: String, author: String, url: String?): BookShelfState
/** /**
* 显示书籍详情 * 显示书籍详情
@@ -20,10 +20,18 @@ import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.mapLatest
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
enum class BookShelfState {
IN_SHELF,
SAME_NAME_AUTHOR,
NOT_IN_SHELF
}
data class BookKey(val name: String, val author: String, val url: String?)
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
class SearchViewModel(application: Application) : BaseViewModel(application) { class SearchViewModel(application: Application) : BaseViewModel(application) {
val handler = Handler(Looper.getMainLooper()) val handler = Handler(Looper.getMainLooper())
val bookshelf: MutableSet<String> = ConcurrentHashMap.newKeySet() val bookshelf: MutableSet<BookKey> = ConcurrentHashMap.newKeySet()
val upAdapterLiveData = MutableLiveData<String>() val upAdapterLiveData = MutableLiveData<String>()
var searchBookLiveData = ConflateLiveData<List<SearchBook>>(1000) var searchBookLiveData = ConflateLiveData<List<SearchBook>>(1000)
val searchScope: SearchScope = SearchScope(AppConfig.searchScope) val searchScope: SearchScope = SearchScope(AppConfig.searchScope)
@@ -70,18 +78,12 @@ class SearchViewModel(application: Application) : BaseViewModel(application) {
init { init {
execute { execute {
appDb.bookDao.flowAll().mapLatest { books -> appDb.bookDao.flowAll().mapLatest { books ->
val keys = arrayListOf<String>()
books.filterNot { it.isNotShelf } books.filterNot { it.isNotShelf }
.forEach {
keys.add("${it.name}-${it.author}")
keys.add(it.name)
}
keys
}.catch { }.catch {
AppLog.put("搜索界面获取书籍列表失败\n${it.localizedMessage}", it) AppLog.put("搜索界面获取书籍列表失败\n${it.localizedMessage}", it)
}.collect { }.collect { books ->
bookshelf.clear() bookshelf.clear()
bookshelf.addAll(it) bookshelf.addAll(books.map { BookKey(it.name, it.author, it.bookUrl) })
upAdapterLiveData.postValue("isInBookshelf") upAdapterLiveData.postValue("isInBookshelf")
} }
}.onError { }.onError {
@@ -89,12 +91,14 @@ class SearchViewModel(application: Application) : BaseViewModel(application) {
} }
} }
fun isInBookShelf(name: String, author: String): Boolean { fun isInBookShelf(name: String, author: String, url: String?): BookShelfState {
return if (author.isNotBlank()) { val exactMatch = bookshelf.any { it.name == name && it.author == author && it.url == url }
bookshelf.contains("$name-$author") if (exactMatch) return BookShelfState.IN_SHELF
} else {
bookshelf.contains(name) val sameNameAuthor = bookshelf.any { it.name == name && it.author == author && it.url != url }
} if (sameNameAuthor) return BookShelfState.SAME_NAME_AUTHOR
return BookShelfState.NOT_IN_SHELF
} }
/** /**
+2 -1
View File
@@ -49,6 +49,7 @@
app:strokeWidth="0dp"> app:strokeWidth="0dp">
<TextView <TextView
android:id="@+id/tv_bookshelf"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingHorizontal="4dp" android:paddingHorizontal="4dp"
@@ -56,7 +57,7 @@
android:text="@string/remove_from_bookshelf" android:text="@string/remove_from_bookshelf"
android:textColor="?attr/colorOnTertiaryContainer" android:textColor="?attr/colorOnTertiaryContainer"
android:textStyle="bold" android:textStyle="bold"
android:textSize="10sp" android:textSize="9sp"
tools:ignore="SmallSp" /> tools:ignore="SmallSp" />
</com.google.android.material.card.MaterialCardView> </com.google.android.material.card.MaterialCardView>