Xwite 3 лет назад
Родитель
Сommit
ec9057be9b

+ 7 - 0
app/build.gradle

@@ -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())

+ 3 - 3
app/src/main/java/io/legado/app/data/entities/Server.kt

@@ -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
 
 }

+ 15 - 23
app/src/main/java/io/legado/app/help/book/BookExtensions.kt

@@ -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) {

+ 1 - 1
app/src/main/java/io/legado/app/lib/webdav/Authorization.kt

@@ -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")
     }

+ 2 - 1
app/src/main/java/io/legado/app/model/webBook/BookInfo.kt

@@ -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)

+ 0 - 131
app/src/main/java/io/legado/app/ui/association/ImportOnLineBookFileDialog.kt

@@ -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()
-                        }
-                    }
-                }
-            }
-        }
-
-    }
-
-}

+ 0 - 78
app/src/main/java/io/legado/app/ui/association/ImportOnLineBookFileViewModel.kt

@@ -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}")
-        }
-    }
-
-}

+ 52 - 19
app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt

@@ -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) {

+ 70 - 9
app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt

@@ -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
+        }
+    }
+
 }