优化webFile导入

This commit is contained in:
Xwite 2023-03-07 13:51:23 +08:00
parent 6fa331c928
commit ca3f6e26f4
9 changed files with 150 additions and 265 deletions

View File

@ -96,6 +96,7 @@ android {
}
flavorDimensions.add("mode")
productFlavors {
app {
dimension "mode"
@ -107,6 +108,7 @@ android {
manifestPlaceholders.put("APP_CHANNEL_VALUE", "google")
}
}
compileOptions {
// Flag to enable support for the new language APIs
coreLibraryDesugaringEnabled true
@ -115,6 +117,11 @@ android {
targetCompatibility JavaVersion.VERSION_11
}
packagingOptions {
// fix found with path 'META-INF/INDEX.LIST' from inputs
exclude 'META-INF/INDEX.LIST'
}
sourceSets {
// Adds exported schema location as test app assets.
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())

View File

@ -49,9 +49,9 @@ data class Server(
@Parcelize
data class WebDavConfig(
var url: String? = null,
var username: String? = null,
var password: String? = null,
var url: String,
var username: String,
var password: String
) : Parcelable
}

View File

@ -18,49 +18,39 @@ import java.util.concurrent.ConcurrentHashMap
val Book.isAudio: Boolean
get() {
return type and BookType.audio > 0
}
get() = isType(BookType.audio)
val Book.isImage: Boolean
get() {
return type and BookType.image > 0
}
get() = isType(BookType.image)
val Book.isLocal: Boolean
get() {
if (type == 0) {
return origin == BookType.localTag || origin.startsWith(BookType.webDavTag)
}
return type and BookType.local > 0
return isType(BookType.local)
}
val Book.isLocalTxt: Boolean
get() {
return isLocal && originName.endsWith(".txt", true)
}
get() = isLocal && originName.endsWith(".txt", true)
val Book.isEpub: Boolean
get() {
return isLocal && originName.endsWith(".epub", true)
}
get() = isLocal && originName.endsWith(".epub", true)
val Book.isUmd: Boolean
get() {
return isLocal && originName.endsWith(".umd", true)
}
get() = isLocal && originName.endsWith(".umd", true)
val Book.isPdf: Boolean
get() {
return isLocal && originName.endsWith(".pdf", true)
}
get() = isLocal && originName.endsWith(".pdf", true)
val Book.isOnLineTxt: Boolean
get() {
return !isLocal && type and BookType.text > 0
}
get() = !isLocal && isType(BookType.text)
val Book.isWebFile: Boolean
get() = isType(BookType.webFile)
val Book.isUpError: Boolean
get() = type and BookType.updateError > 0
get() = isType(BookType.updateError)
fun Book.contains(word: String?): Boolean {
if (word.isNullOrEmpty()) {
@ -172,6 +162,8 @@ fun Book.clearType() {
type = 0
}
fun Book.isType(@BookType.Type bookType: Int): Boolean = type and bookType > 0
fun Book.upType() {
if (type < 8) {
type = when (type) {

View File

@ -24,7 +24,7 @@ data class Authorization(
constructor(serverID: Long?): this("","") {
serverID ?: throw NoStackTraceException("Unexpected server ID")
appDb.serverDao.get(serverID)?.getWebDavConfig().run {
appDb.serverDao.get(serverID)?.getWebDavConfig()?.run {
data = Credentials.basic(username, password, charset)
} ?: throw WebDavException("Unexpected WebDav Authorization")
}

View File

@ -7,6 +7,7 @@ import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookSource
import io.legado.app.exception.NoStackTraceException
import io.legado.app.help.book.BookHelp
import io.legado.app.help.book.isWebFile
import io.legado.app.model.Debug
import io.legado.app.model.analyzeRule.AnalyzeRule
import io.legado.app.utils.DebugLog
@ -137,7 +138,7 @@ object BookInfo {
Debug.log(bookSource.bookSourceUrl, "${e.localizedMessage}")
DebugLog.e("获取封面出错", e)
}
if (book.type and BookType.webFile == 0) {
if (book.isWebFile) {
coroutineContext.ensureActive()
Debug.log(bookSource.bookSourceUrl, "┌获取目录链接")
book.tocUrl = analyzeRule.getString(infoRule.tocUrl, isUrl = true)

View File

@ -1,131 +0,0 @@
package io.legado.app.ui.association
import android.content.Context
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.LinearLayoutManager
import io.legado.app.R
import io.legado.app.base.BaseDialogFragment
import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.RecyclerAdapter
import io.legado.app.databinding.DialogRecyclerViewBinding
import io.legado.app.databinding.ItemBookFileImportBinding
import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.theme.primaryColor
import io.legado.app.ui.widget.dialog.WaitDialog
import io.legado.app.utils.openFileUri
import io.legado.app.utils.setLayout
import io.legado.app.utils.viewbindingdelegate.viewBinding
import io.legado.app.utils.visible
/**
* 导入在线书籍文件弹出窗口
*/
class ImportOnLineBookFileDialog : BaseDialogFragment(R.layout.dialog_recycler_view) {
private val binding by viewBinding(DialogRecyclerViewBinding::bind)
private val viewModel by viewModels<ImportOnLineBookFileViewModel>()
private val adapter by lazy { BookFileAdapter(requireContext()) }
override fun onStart() {
super.onStart()
setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
}
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
val bookUrl = arguments?.getString("bookUrl")
viewModel.initData(bookUrl)
binding.toolBar.setBackgroundColor(primaryColor)
binding.toolBar.setTitle(R.string.download_and_import_file)
binding.rotateLoading.visible()
binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
binding.recyclerView.adapter = adapter
viewModel.errorLiveData.observe(this) {
binding.rotateLoading.gone()
binding.tvMsg.apply {
text = it
visible()
}
}
viewModel.successLiveData.observe(this) {
binding.rotateLoading.gone()
if (it > 0) {
adapter.setItems(viewModel.allBookFiles)
}
}
viewModel.savedFileUriData.observe(this) {
requireContext().openFileUri(it, "*/*")
}
}
private fun importFileAndUpdate(url: String, fileName: String) {
val waitDialog = WaitDialog(requireContext())
waitDialog.show()
viewModel.importOnLineBookFile(url, fileName) {
waitDialog.dismiss()
dismissAllowingStateLoss()
}
}
private fun downloadFile(url: String, fileName: String) {
val waitDialog = WaitDialog(requireContext())
waitDialog.show()
viewModel.downloadUrl(url, fileName) {
waitDialog.dismiss()
dismissAllowingStateLoss()
}
}
inner class BookFileAdapter(context: Context) :
RecyclerAdapter<Triple<String, String, Boolean>
, ItemBookFileImportBinding>(context) {
override fun getViewBinding(parent: ViewGroup): ItemBookFileImportBinding {
return ItemBookFileImportBinding.inflate(inflater, parent, false)
}
override fun convert(
holder: ItemViewHolder,
binding: ItemBookFileImportBinding,
item: Triple<String, String, Boolean>,
payloads: MutableList<Any>
) {
binding.apply {
cbFileName.text = item.second
}
}
override fun registerListener(
holder: ItemViewHolder,
binding: ItemBookFileImportBinding
) {
binding.apply {
cbFileName.setOnClickListener {
val selectFile = viewModel.allBookFiles[holder.layoutPosition]
if (selectFile.third) {
importFileAndUpdate(selectFile.first, selectFile.second)
} else {
alert(
title = getString(R.string.draw),
message = getString(R.string.file_not_supported, selectFile.second)
) {
yesButton {
importFileAndUpdate(selectFile.first, selectFile.second)
}
neutralButton(R.string.open_fun) {
downloadFile(selectFile.first, selectFile.second)
}
noButton()
}
}
}
}
}
}
}

View File

@ -1,78 +0,0 @@
package io.legado.app.ui.association
import android.app.Application
import android.net.Uri
import androidx.lifecycle.MutableLiveData
import io.legado.app.base.BaseViewModel
import io.legado.app.constant.AppPattern
import io.legado.app.constant.EventBus
import io.legado.app.data.appDb
import io.legado.app.data.entities.BookSource
import io.legado.app.exception.NoStackTraceException
import io.legado.app.model.analyzeRule.AnalyzeRule
import io.legado.app.model.analyzeRule.AnalyzeUrl
import io.legado.app.model.localBook.LocalBook
import io.legado.app.utils.postEvent
import io.legado.app.utils.toastOnUi
class ImportOnLineBookFileViewModel(app: Application) : BaseViewModel(app) {
val allBookFiles = arrayListOf<Triple<String, String, Boolean>>()
val errorLiveData = MutableLiveData<String>()
val successLiveData = MutableLiveData<Int>()
val savedFileUriData = MutableLiveData<Uri>()
var bookSource: BookSource? = null
fun initData(bookUrl: String?) {
execute {
bookUrl ?: throw NoStackTraceException("书籍详情页链接为空")
val book = appDb.searchBookDao.getSearchBook(bookUrl)?.toBook()
?: throw NoStackTraceException("book is null")
bookSource = appDb.bookSourceDao.getBookSource(book.origin)
?: throw NoStackTraceException("bookSource is null")
val ruleDownloadUrls = bookSource?.getBookInfoRule()?.downloadUrls
val content = AnalyzeUrl(bookUrl, source = bookSource).getStrResponse().body
val analyzeRule = AnalyzeRule(book, bookSource)
analyzeRule.setContent(content).setBaseUrl(bookUrl)
val fileName = "${book.name} 作者:${book.author}"
analyzeRule.getStringList(ruleDownloadUrls, isUrl = true)?.let {
it.forEach { url ->
val mFileName = "${fileName}.${LocalBook.parseFileSuffix(url)}"
val isSupportedFile = AppPattern.bookFileRegex.matches(mFileName)
allBookFiles.add(Triple(url, mFileName, isSupportedFile))
}
} ?: throw NoStackTraceException("下载链接规则解析为空")
}.onSuccess {
successLiveData.postValue(allBookFiles.size)
}.onError {
errorLiveData.postValue(it.localizedMessage ?: "")
context.toastOnUi("获取书籍下载链接失败\n${it.localizedMessage}")
}
}
fun downloadUrl(url: String, fileName: String, success: () -> Unit) {
execute {
LocalBook.saveBookFile(url, fileName, bookSource).let {
savedFileUriData.postValue(it)
}
}.onSuccess {
success.invoke()
}.onError {
context.toastOnUi("下载书籍文件失败\n${it.localizedMessage}")
}
}
fun importOnLineBookFile(url: String, fileName: String, success: () -> Unit) {
execute {
LocalBook.importFileOnLine(url, fileName, bookSource).let {
postEvent(EventBus.FILE_SOURCE_DOWNLOAD_DONE, it.bookUrl)
}
}.onSuccess {
success.invoke()
}.onError {
context.toastOnUi("下载书籍文件失败\n${it.localizedMessage}")
}
}
}

View File

@ -21,10 +21,7 @@ import io.legado.app.databinding.ActivityBookInfoBinding
import io.legado.app.databinding.DialogEditTextBinding
import io.legado.app.exception.NoStackTraceException
import io.legado.app.help.AppWebDav
import io.legado.app.help.book.isAudio
import io.legado.app.help.book.isLocal
import io.legado.app.help.book.isLocalTxt
import io.legado.app.help.book.getRemoteUrl
import io.legado.app.help.book.*
import io.legado.app.help.config.AppConfig
import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.theme.backgroundColor
@ -33,7 +30,6 @@ import io.legado.app.lib.theme.getPrimaryTextColor
import io.legado.app.model.BookCover
import io.legado.app.model.remote.RemoteBookWebDav
import io.legado.app.ui.about.AppLogDialog
import io.legado.app.ui.association.ImportOnLineBookFileDialog
import io.legado.app.ui.book.audio.AudioPlayActivity
import io.legado.app.ui.book.changecover.ChangeCoverDialog
import io.legado.app.ui.book.changesource.ChangeBookSourceDialog
@ -231,7 +227,6 @@ class BookInfoActivity :
waitDialog.setText("上传中.....")
waitDialog.show()
try {
bookWebDav
?.upload(book)
?: throw NoStackTraceException("未配置webDav")
@ -253,6 +248,7 @@ class BookInfoActivity :
tvOrigin.text = getString(R.string.origin_show, book.originName)
tvLasted.text = getString(R.string.lasted_show, book.latestChapterTitle)
tvIntro.text = book.getDisplayIntro()
tvToc.visible(!book.isWebFile)
upTvBookshelf()
val kinds = book.getKindList()
if (kinds.isEmpty()) {
@ -278,11 +274,10 @@ class BookInfoActivity :
binding.tvToc.text = getString(R.string.toc_s, getString(R.string.loading))
}
chapterList.isNullOrEmpty() -> {
binding.tvToc.text =
if (viewModel.isImportBookOnLine) getString(R.string.click_read_button_load) else getString(
R.string.toc_s,
getString(R.string.error_load_toc)
)
binding.tvToc.text = getString(
R.string.toc_s,
getString(R.string.error_load_toc)
)
}
else -> {
viewModel.bookData.value?.let {
@ -333,9 +328,9 @@ class BookInfoActivity :
}
tvRead.setOnClickListener {
viewModel.bookData.value?.let { book ->
if (viewModel.isImportBookOnLine) {
showDialogFragment<ImportOnLineBookFileDialog> {
putString("bookUrl", book.bookUrl)
if (book.isWebFile) {
showWebFileDownloadAlert {
readBook(book)
}
} else {
readBook(book)
@ -343,11 +338,21 @@ class BookInfoActivity :
} ?: toastOnUi("Book is null")
}
tvShelf.setOnClickListener {
if (viewModel.inBookshelf) {
deleteBook()
} else {
viewModel.addToBookshelf {
upTvBookshelf()
viewModel.bookData.value?.let { book ->
if (viewModel.inBookshelf) {
deleteBook()
} else {
if (book.isWebFile) {
showWebFileDownloadAlert {
viewModel.addToBookshelf {
upTvBookshelf()
}
}
} else {
viewModel.addToBookshelf {
upTvBookshelf()
}
}
}
}
}
@ -493,6 +498,34 @@ class BookInfoActivity :
}
}
private fun showWebFileDownloadAlert(onClick: (() -> Unit)?) {
viewModel.webFileData ?: return
alert(titleResource = R.string.download_and_import_file) {
val webFileData = viewModel.webFileData
webFileData?.let {
items<BookInfoViewModel.WebFile>(it) { _, webFile, _ ->
if (webFile.isSupported) {
viewModel.importOrDownloadWebFile(webFile) {
onClick?.invoke()
}
} else {
alert(
title = getString(R.string.draw),
message = getString(R.string.file_not_supported, webFile.name)
) {
neutralButton(R.string.open_fun) {
viewModel.importOrDownloadWebFile(webFile) { uri ->
openFileUri(uri!!, "*/*")
}
}
noButton()
}
}
}
}
}
}
private fun readBook(book: Book) {
if (!viewModel.inBookshelf) {
viewModel.saveBook(book) {

View File

@ -1,5 +1,6 @@
package io.legado.app.ui.book.info
import android.net.Uri
import android.app.Application
import android.content.Intent
import androidx.lifecycle.MutableLiveData
@ -7,6 +8,7 @@ import androidx.lifecycle.viewModelScope
import io.legado.app.R
import io.legado.app.base.BaseViewModel
import io.legado.app.constant.AppLog
import io.legado.app.constant.AppPattern
import io.legado.app.constant.BookSourceType
import io.legado.app.constant.BookType
import io.legado.app.constant.EventBus
@ -19,6 +21,8 @@ import io.legado.app.help.AppWebDav
import io.legado.app.help.book.*
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.lib.webdav.ObjectNotFoundException
import io.legado.app.model.analyzeRule.AnalyzeRule
import io.legado.app.model.analyzeRule.AnalyzeUrl
import io.legado.app.model.BookCover
import io.legado.app.model.ReadBook
import io.legado.app.model.localBook.LocalBook
@ -32,11 +36,10 @@ import kotlinx.coroutines.Dispatchers.IO
class BookInfoViewModel(application: Application) : BaseViewModel(application) {
val bookData = MutableLiveData<Book>()
val chapterListData = MutableLiveData<List<BookChapter>>()
val webFileData: MutableList<WebFile>? = null
var inBookshelf = false
var bookSource: BookSource? = null
private var changeSourceCoroutine: Coroutine<*>? = null
val isImportBookOnLine: Boolean
get() = bookSource?.bookSourceType == BookSourceType.file
fun initData(intent: Intent) {
execute {
@ -82,8 +85,6 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
appDb.bookSourceDao.getBookSource(book.origin)
if (book.tocUrl.isEmpty() && !book.isLocal) {
loadBookInfo(book)
} else if (isImportBookOnLine) {
chapterListData.postValue(emptyList())
} else {
val chapterList = appDb.bookChapterDao.getChapterList(book.bookUrl)
if (chapterList.isNotEmpty()) {
@ -156,13 +157,14 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
inBookshelf = true
}
bookData.postValue(book)
if (isImportBookOnLine) {
appDb.searchBookDao.update(book.toSearchBook())
}
if (inBookshelf) {
appDb.bookDao.update(book)
}
loadChapter(it, scope)
if (it.isWebFile) {
loadWebFile(book, bookSource, scope)
} else {
loadChapter(it, scope)
}
}.onError {
AppLog.put("获取数据信息失败\n${it.localizedMessage}", it)
context.toastOnUi(R.string.error_get_book_info)
@ -187,7 +189,7 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
appDb.bookChapterDao.insert(*it.toTypedArray())
chapterListData.postValue(it)
}
} else if (isImportBookOnLine) {
} else if (book.isWebFile) {
chapterListData.postValue(emptyList())
} else {
bookSource?.let { bookSource ->
@ -224,6 +226,7 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
}
}
fun loadGroup(groupId: Long, success: ((groupNames: String?) -> Unit)) {
execute {
appDb.bookGroupDao.getGroupNames(groupId).joinToString(",")
@ -232,6 +235,54 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
}
}
private fun loadWebFile(
book: Book,
bookSource: BookSource,
scope: CoroutineScope = viewModelScope
) {
execute(scope) {
webFileData?.clear()
val fileName = "${book.name} 作者:${book.author}"
if (book.downloadUrls.isNullOrEmpty()) {
val ruleDownloadUrls = bookSource.getBookInfoRule().downloadUrls
val content = AnalyzeUrl(book.bookUrl, source = bookSource).getStrResponse().body
val analyzeRule = AnalyzeRule(book, bookSource)
analyzeRule.setContent(content).setBaseUrl(book.bookUrl)
analyzeRule.getStringList(ruleDownloadUrls, isUrl = true)?.let {
parseDownloadUrls(it, fileName)
} ?: throw NoStackTraceException("Unexpected ruleDownloadUrls")
} else {
parseDownloadUrls(book.downloadUrls, fileName)
}
}.onError {
context.toastOnUi("LoadWebFileError\n${it.localizedMessage}")
}
}
private fun parseDownloadUrls(downloadUrls: List<String>?, fileName: String) {
val urls = downloadUrls
urls?.forEach { url ->
val mFileName = "${fileName}.${LocalBook.parseFileSuffix(url)}"
val isSupportedFile = AppPattern.bookFileRegex.matches(mFileName)
webFileData?.add(WebFile(url, mFileName, isSupportedFile))
}
}
fun importOrDownloadWebFile(webFile: WebFile, success: ((Uri?) -> Unit)?) {
bookSource ?: return
execute {
if (webFile.isSupported) {
LocalBook.importFileOnLine(webFile.url, webFile.name, bookSource)
} else {
LocalBook.saveBookFile(webFile.url, webFile.name, bookSource)
}
}.onSuccess {
success?.invoke(it as? Uri)
}.onError {
context.toastOnUi("ImportWebFileError\n${it.localizedMessage}")
}
}
fun changeTo(source: BookSource, book: Book, toc: List<BookChapter>) {
changeSourceCoroutine?.cancel()
changeSourceCoroutine = execute {
@ -356,4 +407,14 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
}
}
data class WebFile(
val url: String,
val name: String,
val isSupported: Boolean
) {
override fun toString(): String {
return name
}
}
}