Fix import book slow

This commit is contained in:
Horis
2025-01-21 20:30:26 +08:00
parent 17a860896e
commit c37c5d4085
6 changed files with 80 additions and 57 deletions
@@ -132,8 +132,11 @@ interface BookDao {
@Query("select 1 from books where bookUrl = :bookUrl") @Query("select 1 from books where bookUrl = :bookUrl")
fun has(bookUrl: String): Boolean? fun has(bookUrl: String): Boolean?
@Query("select 1 from books where originName = :fileName or origin like '%' || :fileName") @Query(
fun hasFile(fileName: String): Boolean? """select exists(select 1 from books where type & ${BookType.local} > 0
and (originName = :fileName or (origin != '${BookType.localTag}' and origin like '%' || :fileName)))"""
)
fun hasFile(fileName: String): Boolean
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg book: Book) fun insert(vararg book: Book)
@@ -16,7 +16,6 @@ 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.book.isEpub import io.legado.app.help.book.isEpub
import io.legado.app.help.book.isImage import io.legado.app.help.book.isImage
import io.legado.app.help.book.isPdf
import io.legado.app.help.book.simulatedTotalChapterNum import io.legado.app.help.book.simulatedTotalChapterNum
import io.legado.app.help.config.AppConfig import io.legado.app.help.config.AppConfig
import io.legado.app.help.config.ReadBookConfig import io.legado.app.help.config.ReadBookConfig
@@ -0,0 +1,14 @@
package io.legado.app.ui.book.import.local
import io.legado.app.model.localBook.LocalBook
import io.legado.app.utils.FileDoc
data class ImportBook(
val file: FileDoc,
var isOnBookShelf: Boolean = LocalBook.isOnBookShelf(file.name)
) {
val name get() = file.name
val isDir get() = file.isDir
val size get() = file.size
val lastModified get() = file.lastModified
}
@@ -101,7 +101,7 @@ class ImportBookActivity : BaseImportBookActivity<ImportBookViewModel>(),
override fun onMenuItemClick(item: MenuItem?): Boolean { override fun onMenuItemClick(item: MenuItem?): Boolean {
when (item?.itemId) { when (item?.itemId) {
R.id.menu_del_selection -> viewModel.deleteDoc(adapter.selectedUris) { R.id.menu_del_selection -> viewModel.deleteDoc(adapter.selected) {
adapter.removeSelection() adapter.removeSelection()
} }
} }
@@ -118,8 +118,11 @@ class ImportBookActivity : BaseImportBookActivity<ImportBookViewModel>(),
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
override fun onClickSelectBarMainAction() { override fun onClickSelectBarMainAction() {
viewModel.addToBookshelf(adapter.selectedUris) { viewModel.addToBookshelf(adapter.selected) {
adapter.selectedUris.clear() adapter.selected.forEach {
it.isOnBookShelf = true
}
adapter.selected.clear()
adapter.notifyDataSetChanged() adapter.notifyDataSetChanged()
} }
} }
@@ -129,6 +132,7 @@ class ImportBookActivity : BaseImportBookActivity<ImportBookViewModel>(),
binding.tvEmptyMsg.setText(R.string.empty_msg_import_book) binding.tvEmptyMsg.setText(R.string.empty_msg_import_book)
binding.recyclerView.layoutManager = LinearLayoutManager(this) binding.recyclerView.layoutManager = LinearLayoutManager(this)
binding.recyclerView.adapter = adapter binding.recyclerView.adapter = adapter
binding.recyclerView.recycledViewPool.setMaxRecycledViews(0, 15)
binding.selectActionBar.setMainActionText(R.string.add_to_bookshelf) binding.selectActionBar.setMainActionText(R.string.add_to_bookshelf)
binding.selectActionBar.inflateMenu(R.menu.import_book_sel) binding.selectActionBar.inflateMenu(R.menu.import_book_sel)
binding.selectActionBar.setOnMenuItemClickListener(this) binding.selectActionBar.setOnMenuItemClickListener(this)
@@ -212,7 +216,7 @@ class ImportBookActivity : BaseImportBookActivity<ImportBookViewModel>(),
viewModel.sort = sort viewModel.sort = sort
putPrefInt(PreferKey.localBookImportSort, sort) putPrefInt(PreferKey.localBookImportSort, sort)
if (scanDocJob?.isActive != true) { if (scanDocJob?.isActive != true) {
viewModel.dataCallback?.setItems(adapter.getItems()) viewModel.dataCallback?.upAdapter()
} }
} }
@@ -234,7 +238,7 @@ class ImportBookActivity : BaseImportBookActivity<ImportBookViewModel>(),
path = path + doc.name + File.separator path = path + doc.name + File.separator
} }
binding.tvPath.text = path binding.tvPath.text = path
adapter.selectedUris.clear() adapter.selected.clear()
adapter.clearItems() adapter.clearItems()
viewModel.loadDoc(lastDoc) viewModel.loadDoc(lastDoc)
} }
@@ -295,7 +299,7 @@ class ImportBookActivity : BaseImportBookActivity<ImportBookViewModel>(),
} }
override fun upCountView() { override fun upCountView() {
binding.selectActionBar.upCountView(adapter.selectedUris.size, adapter.checkableCount) binding.selectActionBar.upCountView(adapter.selected.size, adapter.checkableCount)
} }
override fun startRead(fileDoc: FileDoc) { override fun startRead(fileDoc: FileDoc) {
@@ -9,12 +9,18 @@ import io.legado.app.base.adapter.RecyclerAdapter
import io.legado.app.constant.AppConst import io.legado.app.constant.AppConst
import io.legado.app.databinding.ItemImportBookBinding import io.legado.app.databinding.ItemImportBookBinding
import io.legado.app.model.localBook.LocalBook import io.legado.app.model.localBook.LocalBook
import io.legado.app.utils.* import io.legado.app.utils.ConvertUtils
import io.legado.app.utils.FileDoc
import io.legado.app.utils.gone
import io.legado.app.utils.invisible
import io.legado.app.utils.visible
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
class ImportBookAdapter(context: Context, val callBack: CallBack) : class ImportBookAdapter(context: Context, val callBack: CallBack) :
RecyclerAdapter<FileDoc, ItemImportBookBinding>(context) { RecyclerAdapter<ImportBook, ItemImportBookBinding>(context) {
val selectedUris = hashSetOf<String>() val selected = hashSetOf<ImportBook>()
var checkableCount = 0 var checkableCount = 0
override fun getViewBinding(parent: ViewGroup): ItemImportBookBinding { override fun getViewBinding(parent: ViewGroup): ItemImportBookBinding {
@@ -28,7 +34,7 @@ class ImportBookAdapter(context: Context, val callBack: CallBack) :
override fun convert( override fun convert(
holder: ItemViewHolder, holder: ItemViewHolder,
binding: ItemImportBookBinding, binding: ItemImportBookBinding,
item: FileDoc, item: ImportBook,
payloads: MutableList<Any> payloads: MutableList<Any>
) { ) {
binding.run { binding.run {
@@ -40,7 +46,7 @@ class ImportBookAdapter(context: Context, val callBack: CallBack) :
llBrief.gone() llBrief.gone()
cbSelect.isChecked = false cbSelect.isChecked = false
} else { } else {
if (isOnBookShelf(item)) { if (item.isOnBookShelf) {
ivIcon.setImageResource(R.drawable.ic_book_has) ivIcon.setImageResource(R.drawable.ic_book_has)
ivIcon.visible() ivIcon.visible()
cbSelect.invisible() cbSelect.invisible()
@@ -52,11 +58,11 @@ class ImportBookAdapter(context: Context, val callBack: CallBack) :
tvTag.text = item.name.substringAfterLast(".") tvTag.text = item.name.substringAfterLast(".")
tvSize.text = ConvertUtils.formatFileSize(item.size) tvSize.text = ConvertUtils.formatFileSize(item.size)
tvDate.text = AppConst.dateFormat.format(item.lastModified) tvDate.text = AppConst.dateFormat.format(item.lastModified)
cbSelect.isChecked = selectedUris.contains(item.toString()) cbSelect.isChecked = selected.contains(item)
} }
tvName.text = item.name tvName.text = item.name
} else { } else {
cbSelect.isChecked = selectedUris.contains(item.toString()) cbSelect.isChecked = selected.contains(item)
} }
} }
} }
@@ -65,18 +71,18 @@ class ImportBookAdapter(context: Context, val callBack: CallBack) :
holder.itemView.setOnClickListener { holder.itemView.setOnClickListener {
getItem(holder.layoutPosition)?.let { getItem(holder.layoutPosition)?.let {
if (it.isDir) { if (it.isDir) {
callBack.nextDoc(it) callBack.nextDoc(it.file)
} else if (!isOnBookShelf(it)) { } else if (!it.isOnBookShelf) {
if (!selectedUris.contains(it.toString())) { if (!selected.contains(it)) {
selectedUris.add(it.toString()) selected.add(it)
} else { } else {
selectedUris.remove(it.toString()) selected.remove(it)
} }
notifyItemChanged(holder.layoutPosition, true) notifyItemChanged(holder.layoutPosition, true)
callBack.upCountView() callBack.upCountView()
} else { } else {
/* 点击开始阅读 */ /* 点击开始阅读 */
callBack.startRead(it) callBack.startRead(it.file)
} }
} }
} }
@@ -89,7 +95,7 @@ class ImportBookAdapter(context: Context, val callBack: CallBack) :
private fun upCheckableCount() { private fun upCheckableCount() {
checkableCount = 0 checkableCount = 0
getItems().forEach { getItems().forEach {
if (!it.isDir && !isOnBookShelf(it)) { if (!it.isDir && !it.isOnBookShelf) {
checkableCount++ checkableCount++
} }
} }
@@ -100,12 +106,12 @@ class ImportBookAdapter(context: Context, val callBack: CallBack) :
fun selectAll(selectAll: Boolean) { fun selectAll(selectAll: Boolean) {
if (selectAll) { if (selectAll) {
getItems().forEach { getItems().forEach {
if (!it.isDir && !isOnBookShelf(it)) { if (!it.isDir && !it.isOnBookShelf) {
selectedUris.add(it.toString()) selected.add(it)
} }
} }
} else { } else {
selectedUris.clear() selected.clear()
} }
notifyDataSetChanged() notifyDataSetChanged()
callBack.upCountView() callBack.upCountView()
@@ -113,11 +119,11 @@ class ImportBookAdapter(context: Context, val callBack: CallBack) :
fun revertSelection() { fun revertSelection() {
getItems().forEach { getItems().forEach {
if (!it.isDir && !isOnBookShelf(it)) { if (!it.isDir && !it.isOnBookShelf) {
if (selectedUris.contains(it.toString())) { if (selected.contains(it)) {
selectedUris.remove(it.toString()) selected.remove(it)
} else { } else {
selectedUris.add(it.toString()) selected.add(it)
} }
} }
} }
@@ -127,7 +133,7 @@ class ImportBookAdapter(context: Context, val callBack: CallBack) :
fun removeSelection() { fun removeSelection() {
for (i in getItems().lastIndex downTo 0) { for (i in getItems().lastIndex downTo 0) {
if (getItem(i)?.toString() in selectedUris) { if (getItem(i) in selected) {
removeItem(i) removeItem(i)
} }
} }
@@ -1,8 +1,6 @@
package io.legado.app.ui.book.import.local package io.legado.app.ui.book.import.local
import android.app.Application import android.app.Application
import android.net.Uri
import androidx.documentfile.provider.DocumentFile
import io.legado.app.base.BaseViewModel import io.legado.app.base.BaseViewModel
import io.legado.app.constant.AppLog import io.legado.app.constant.AppLog
import io.legado.app.constant.AppPattern.archiveFileRegex import io.legado.app.constant.AppPattern.archiveFileRegex
@@ -11,19 +9,19 @@ import io.legado.app.constant.PreferKey
import io.legado.app.model.localBook.LocalBook import io.legado.app.model.localBook.LocalBook
import io.legado.app.utils.AlphanumComparator import io.legado.app.utils.AlphanumComparator
import io.legado.app.utils.FileDoc import io.legado.app.utils.FileDoc
import io.legado.app.utils.delete
import io.legado.app.utils.getPrefInt import io.legado.app.utils.getPrefInt
import io.legado.app.utils.isContentScheme
import io.legado.app.utils.list import io.legado.app.utils.list
import io.legado.app.utils.toastOnUi import io.legado.app.utils.toastOnUi
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File
import java.util.Collections import java.util.Collections
import kotlin.coroutines.coroutineContext import kotlin.coroutines.coroutineContext
@@ -33,20 +31,24 @@ class ImportBookViewModel(application: Application) : BaseViewModel(application)
var sort = context.getPrefInt(PreferKey.localBookImportSort) var sort = context.getPrefInt(PreferKey.localBookImportSort)
var dataCallback: DataCallback? = null var dataCallback: DataCallback? = null
var dataFlowStart: (() -> Unit)? = null var dataFlowStart: (() -> Unit)? = null
val dataFlow = callbackFlow<List<FileDoc>> { val dataFlow = callbackFlow<List<ImportBook>> {
val list = Collections.synchronizedList(ArrayList<FileDoc>()) val list = Collections.synchronizedList(ArrayList<ImportBook>())
dataCallback = object : DataCallback { dataCallback = object : DataCallback {
override fun setItems(fileDocs: List<FileDoc>) { override fun setItems(fileDocs: List<FileDoc>) {
list.clear() list.clear()
list.addAll(fileDocs) fileDocs.mapTo(list) {
ImportBook(it)
}
trySend(list) trySend(list)
} }
override fun addItems(fileDocs: List<FileDoc>) { override fun addItems(fileDocs: List<FileDoc>) {
list.addAll(fileDocs) fileDocs.mapTo(list) {
ImportBook(it)
}
trySend(list) trySend(list)
} }
@@ -64,6 +66,10 @@ class ImportBookViewModel(application: Application) : BaseViewModel(application)
) )
} }
} }
override fun upAdapter() {
trySend(list)
}
} }
withContext(Main) { withContext(Main) {
@@ -76,21 +82,17 @@ class ImportBookViewModel(application: Application) : BaseViewModel(application)
}.map { docList -> }.map { docList ->
val comparator = when (sort) { val comparator = when (sort) {
2 -> compareBy<FileDoc>({ !it.isDir }, { -it.lastModified }) 2 -> compareBy<ImportBook>({ !it.isDir }, { -it.lastModified })
1 -> compareBy({ !it.isDir }, { -it.size }) 1 -> compareBy({ !it.isDir }, { -it.size })
else -> compareBy { !it.isDir } else -> compareBy { !it.isDir }
} then compareBy(AlphanumComparator) { it.name } } then compareBy(AlphanumComparator) { it.name }
docList.sortedWith(comparator) docList.sortedWith(comparator)
}.flowOn(IO) }.flowOn(IO)
fun addToBookshelf(uriList: HashSet<String>, finally: () -> Unit) { fun addToBookshelf(bookList: HashSet<ImportBook>, finally: () -> Unit) {
execute { execute {
val fileUris = uriList.map { val fileUris = bookList.map {
if (it.isContentScheme()) { it.file.uri
Uri.parse(it)
} else {
Uri.fromFile(File(it))
}
} }
LocalBook.importFiles(fileUris) LocalBook.importFiles(fileUris)
}.onError { }.onError {
@@ -101,17 +103,10 @@ class ImportBookViewModel(application: Application) : BaseViewModel(application)
} }
} }
fun deleteDoc(uriList: HashSet<String>, finally: () -> Unit) { fun deleteDoc(bookList: HashSet<ImportBook>, finally: () -> Unit) {
execute { execute {
uriList.forEach { bookList.forEach {
val uri = Uri.parse(it) it.file.delete()
if (uri.isContentScheme()) {
DocumentFile.fromSingleUri(context, uri)?.delete()
} else {
uri.path?.let { path ->
File(path).delete()
}
}
} }
}.onFinally { }.onFinally {
finally.invoke() finally.invoke()
@@ -190,6 +185,8 @@ class ImportBookViewModel(application: Application) : BaseViewModel(application)
fun screen(key: String?) fun screen(key: String?)
fun upAdapter()
} }
} }