Просмотр исходного кода

Merge pull request #1884 from Xwite/master

在线文件导入对话框
kunfei 4 лет назад
Родитель
Сommit
cd9bb019d8
23 измененных файлов с 366 добавлено и 53 удалено
  1. 3 0
      app/src/main/assets/help/ruleHelp.md
  2. 1 1
      app/src/main/java/io/legado/app/api/controller/BookController.kt
  3. 1 0
      app/src/main/java/io/legado/app/constant/EventBus.kt
  4. 4 0
      app/src/main/java/io/legado/app/data/entities/Book.kt
  5. 2 1
      app/src/main/java/io/legado/app/data/entities/rule/BookInfoRule.kt
  6. 6 1
      app/src/main/java/io/legado/app/model/Debug.kt
  7. 47 19
      app/src/main/java/io/legado/app/model/localBook/LocalBook.kt
  8. 2 2
      app/src/main/java/io/legado/app/model/localBook/README.md
  9. 24 7
      app/src/main/java/io/legado/app/model/webBook/BookInfo.kt
  10. 4 1
      app/src/main/java/io/legado/app/service/CheckSourceService.kt
  11. 132 0
      app/src/main/java/io/legado/app/ui/association/ImportOnLineBookFileDialog.kt
  12. 80 0
      app/src/main/java/io/legado/app/ui/association/ImportOnLineBookFileViewModel.kt
  13. 12 3
      app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt
  14. 10 18
      app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt
  15. 2 0
      app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditActivity.kt
  16. 23 0
      app/src/main/res/layout/item_book_file_import.xml
  17. 2 0
      app/src/main/res/values-es-rES/strings.xml
  18. 2 0
      app/src/main/res/values-ja-rJP/strings.xml
  19. 2 0
      app/src/main/res/values-pt-rBR/strings.xml
  20. 2 0
      app/src/main/res/values-zh-rHK/strings.xml
  21. 2 0
      app/src/main/res/values-zh-rTW/strings.xml
  22. 1 0
      app/src/main/res/values-zh/strings.xml
  23. 2 0
      app/src/main/res/values/strings.xml

+ 3 - 0
app/src/main/assets/help/ruleHelp.md

@@ -10,6 +10,9 @@
 @Json: json规则,直接写时以$.开头可省略@Json
 : regex规则,不可省略,只可以用在书籍列表和目录列表
 ```
+* 书源类型: 文件
+> 对于类似知轩藏书提供文件整合下载的网站,可以'在书源详情的下载URL规则获取文件链接,支持多个链接,阅读会自动下载并导入
+
 * CookieJar
 > 启用后会自动保存每次返回头中的Set-Cookie中的值,适用于验证码图片一类需要session的网站
 * 登录UI

+ 1 - 1
app/src/main/java/io/legado/app/api/controller/BookController.kt

@@ -244,7 +244,7 @@ object BookController {
         val fileData = parameters["fileData"]?.firstOrNull()
             ?: return returnData.setErrorMsg("fileData 不能为空")
         kotlin.runCatching {
-           LocalBook.importFile(fileData, fileName)
+           LocalBook.importFileOnLine(fileData, fileName)
         }.onFailure {
             return when (it) {
                 is SecurityException -> returnData.setErrorMsg("需重新设置书籍保存位置!")

+ 1 - 0
app/src/main/java/io/legado/app/constant/EventBus.kt

@@ -28,4 +28,5 @@ object EventBus {
     const val TIP_COLOR = "tipColor"
     const val SOURCE_CHANGED = "sourceChanged"
     const val SEARCH_RESULT = "searchResult"
+    const val BOOK_URL_CHANGED = "bookUrlChanged"
 }

+ 4 - 0
app/src/main/java/io/legado/app/data/entities/Book.kt

@@ -154,6 +154,10 @@ data class Book(
     @IgnoredOnParcel
     override var tocHtml: String? = null
 
+    @Ignore
+    @IgnoredOnParcel
+    var downloadUrls: List<String>? = null
+
     fun getRealAuthor() = author.replace(AppPattern.authorRegex, "")
 
     fun getUnreadChapterNum() = max(totalChapterNum - durChapterIndex - 1, 0)

+ 2 - 1
app/src/main/java/io/legado/app/data/entities/rule/BookInfoRule.kt

@@ -16,5 +16,6 @@ data class BookInfoRule(
     var coverUrl: String? = null,
     var tocUrl: String? = null,
     var wordCount: String? = null,
-    var canReName: String? = null
+    var canReName: String? = null,
+    var downloadUrls: String? = null
 ) : Parcelable

+ 6 - 1
app/src/main/java/io/legado/app/model/Debug.kt

@@ -2,6 +2,7 @@ package io.legado.app.model
 
 import android.annotation.SuppressLint
 import io.legado.app.constant.AppPattern
+import io.legado.app.constant.BookType
 import io.legado.app.data.entities.*
 import io.legado.app.help.coroutine.CompositeCoroutine
 import io.legado.app.model.rss.Rss
@@ -238,7 +239,11 @@ object Debug {
             .onSuccess {
                 log(debugSource, "︽详情页解析完成")
                 log(debugSource, showTime = false)
-                tocDebug(scope, bookSource, book)
+                if (book.type != BookType.file) {
+                    tocDebug(scope, bookSource, book)
+                } else {
+                    log(debugSource, "≡文件类书源跳过解析目录", state = 1000)
+                }
             }
             .onError {
                 log(debugSource, it.msg, state = -1)

+ 47 - 19
app/src/main/java/io/legado/app/model/localBook/LocalBook.kt

@@ -24,6 +24,10 @@ import java.io.FileNotFoundException
 import java.io.InputStream
 import java.util.regex.Pattern
 
+/**
+ * 书籍文件导入 目录正文解析
+ * 支持在线文件(txt epub umd 压缩文件需要用户解压) 本地文件
+ */
 object LocalBook {
 
     private val nameAuthorPatterns = arrayOf(
@@ -84,32 +88,23 @@ object LocalBook {
         }
     }
 
-    //导入在线的文件
-    fun importFile(
+    /**
+     * 下载在线的文件并自动导入到阅读(txt umd epub)
+     * 压缩文件请先提示用户解压
+     */
+    fun importFileOnLine(
         str: String,
         fileName: String,
         source: BaseSource? = null,
-        onLineBook: Book? = null
     ): Book {
-        val bytes = when {
-            str.isAbsUrl() -> AnalyzeUrl(str, source = source).getByteArray()
-            str.isDataUrl() -> Base64.decode(str.substringAfter("base64,"), Base64.DEFAULT)
-            else -> throw NoStackTraceException("在线导入书籍支持http/https/DataURL")
-        }
-        val localBook = importFile(bytes, fileName)
-        return mergeBook(localBook, onLineBook)
-    }
-
-    fun importFile(
-        bytes: ByteArray,
-        fileName: String
-    ): Book {
-        return saveBookFile(bytes, fileName).let {
+        return saveBookFile(str, fileName, source).let {
             importFile(it)
         }
     }
 
-    //导入本地文件
+    /**
+     * 导入本地文件
+     */
     fun importFile(uri: Uri): Book {
         val bookUrl: String
         val updateTime: Long
@@ -150,6 +145,9 @@ object LocalBook {
         return book
     }
 
+    /**
+     * 从文件分析书籍必要信息(书名 作者等)
+     */
     private fun analyzeNameAuthor(fileName: String): Pair<String, String> {
         val tempFileName = fileName.substringBeforeLast(".")
         var name: String
@@ -203,6 +201,36 @@ object LocalBook {
         }
     }
 
+    /**
+     * 下载在线的文件
+     */
+    fun saveBookFile(
+        str: String,
+        fileName: String,
+        source: BaseSource? = null,
+    ): Uri {
+        val bytes = when {
+            str.isAbsUrl() -> AnalyzeUrl(str, source = source).getByteArray()
+            str.isDataUrl() -> Base64.decode(str.substringAfter("base64,"), Base64.DEFAULT)
+            else -> throw NoStackTraceException("在线导入书籍支持http/https/DataURL")
+        }
+        return saveBookFile(bytes, fileName)
+    }
+
+    /**
+     * 分析下载文件类书源的下载链接的文件后缀
+     * https://www.example.com/download/{fileName}.{type} 含有文件名和后缀
+     * https://www.example.com/download/?fileid=1234, {type: "txt"} 规则设置
+     */
+    fun parseFileSuffix(url: String): String {
+        val analyzeUrl = AnalyzeUrl(url)
+        val urlNoOption = analyzeUrl.url
+        val lastPath = urlNoOption.substringAfterLast("/")
+        val fileType = lastPath.substringAfterLast(".")
+        val type = analyzeUrl.type
+        return type ?: fileType ?: "unknown"
+    }
+
     private fun saveBookFile(
         bytes: ByteArray,
         fileName: String
@@ -232,7 +260,7 @@ object LocalBook {
     }
 
     //文件类书源 合并在线书籍信息 在线 > 本地
-    private fun mergeBook(localBook: Book, onLineBook: Book?): Book {
+    fun mergeBook(localBook: Book, onLineBook: Book?): Book {
         onLineBook ?: return localBook
         val mergeBook = localBook
         mergeBook.name = if (onLineBook.name.isBlank()) localBook.name else onLineBook.name

+ 2 - 2
app/src/main/java/io/legado/app/model/localBook/README.md

@@ -1,7 +1,7 @@
-# 本地书籍解析
+# 书籍文件导入解析
 
 * BaseLocalBookParse.kt 本地书籍解析接口
-* LocalBook.kt 总入口
+* LocalBook.kt 导入解析总入口
 * TextFile.kt 解析txt
 * EpubFile.kt 解析epub
 * UmdFile.kt 解析umd

+ 24 - 7
app/src/main/java/io/legado/app/model/webBook/BookInfo.kt

@@ -1,6 +1,8 @@
 package io.legado.app.model.webBook
 
+import android.text.TextUtils
 import io.legado.app.R
+import io.legado.app.constant.BookType
 import io.legado.app.data.entities.Book
 import io.legado.app.data.entities.BookSource
 import io.legado.app.exception.NoStackTraceException
@@ -137,14 +139,29 @@ object BookInfo {
             Debug.log(bookSource.bookSourceUrl, "└${e.localizedMessage}")
             DebugLog.e("获取封面出错", e)
         }
-        scope.ensureActive()
-        Debug.log(bookSource.bookSourceUrl, "┌获取目录链接")
-        book.tocUrl = analyzeRule.getString(infoRule.tocUrl, isUrl = true)
-        if (book.tocUrl.isEmpty()) book.tocUrl = baseUrl
-        if (book.tocUrl == baseUrl) {
-            book.tocHtml = body
+        if (book.type != BookType.file) {
+            scope.ensureActive()
+            Debug.log(bookSource.bookSourceUrl, "┌获取目录链接")
+            book.tocUrl = analyzeRule.getString(infoRule.tocUrl, isUrl = true)
+            if (book.tocUrl.isEmpty()) book.tocUrl = baseUrl
+            if (book.tocUrl == baseUrl) {
+                book.tocHtml = body
+            }
+            Debug.log(bookSource.bookSourceUrl, "└${book.tocUrl}")
+        } else {
+            scope.ensureActive()
+            Debug.log(bookSource.bookSourceUrl, "┌获取文件下载链接")
+            book.downloadUrls = analyzeRule.getStringList(infoRule.downloadUrls, isUrl = true)
+            if (book.downloadUrls == null) {
+                Debug.log(bookSource.bookSourceUrl, "└")
+                throw NoStackTraceException("下载链接为空")
+            } else {
+                Debug.log(
+                    bookSource.bookSourceUrl,
+                    "└" + TextUtils.join(",\n", book.downloadUrls!!)
+                )
+            }
         }
-        Debug.log(bookSource.bookSourceUrl, "└${book.tocUrl}")
     }
 
 }

+ 4 - 1
app/src/main/java/io/legado/app/service/CheckSourceService.kt

@@ -6,6 +6,7 @@ import com.script.ScriptException
 import io.legado.app.R
 import io.legado.app.base.BaseService
 import io.legado.app.constant.AppConst
+import io.legado.app.constant.BookType
 import io.legado.app.constant.EventBus
 import io.legado.app.constant.IntentAction
 import io.legado.app.data.appDb
@@ -208,7 +209,9 @@ class CheckSourceService : BaseService() {
                     mBook = WebBook.getBookInfoAwait(this, source, mBook)
                 }
                 //校验目录
-                if (CheckSource.checkCategory) {
+                if (CheckSource.checkCategory &&
+                    source.bookSourceType != BookType.file
+                ) {
                     val toc = WebBook.getChapterListAwait(this, source, mBook).getOrThrow()
                     val nextChapterUrl = toc.getOrNull(1)?.url ?: toc.first().url
                     //校验正文

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

@@ -0,0 +1,132 @@
+package io.legado.app.ui.association
+
+import android.content.Context
+import android.content.DialogInterface
+import android.os.Bundle
+import android.view.View
+import android.view.ViewGroup
+import android.net.Uri
+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.help.config.AppConfig
+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.*
+import io.legado.app.utils.viewbindingdelegate.viewBinding
+
+
+/**
+ * 导入在线书籍文件弹出窗口
+ */
+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.show()
+        binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
+        binding.recyclerView.adapter = adapter
+        viewModel.errorLiveData.observe(this) {
+            binding.rotateLoading.hide()
+            binding.tvMsg.apply {
+                text = it
+                visible()
+            }
+        }
+        viewModel.successLiveData.observe(this) {
+            binding.rotateLoading.hide()
+            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)
+                        ) {
+                            okButton {
+                                importFileAndUpdate(selectFile.first, selectFile.second)
+                            }
+                            neutralButton(R.string.open_fun) {
+                                downloadFile(selectFile.first, selectFile.second)
+                            }
+                            cancelButton()
+                        }
+                    }
+                }
+            }
+        }
+
+    }
+
+}

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

@@ -0,0 +1,80 @@
+package io.legado.app.ui.association
+
+import android.app.Application
+import android.net.Uri
+import androidx.lifecycle.MutableLiveData
+import io.legado.app.R
+import io.legado.app.base.BaseViewModel
+import io.legado.app.constant.AppPattern
+import io.legado.app.constant.AppLog
+import io.legado.app.constant.EventBus
+import io.legado.app.data.appDb
+import io.legado.app.data.entities.Book
+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.*
+
+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.BOOK_URL_CHANGED, it.bookUrl)
+            }
+        }.onSuccess {
+           success.invoke()
+        }.onError {
+            context.toastOnUi("下载书籍文件失败\n${it.localizedMessage}")
+        }
+    }
+
+}

+ 12 - 3
app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt

@@ -12,6 +12,7 @@ import androidx.activity.viewModels
 import io.legado.app.R
 import io.legado.app.base.VMBaseActivity
 import io.legado.app.constant.BookType
+import io.legado.app.constant.EventBus
 import io.legado.app.constant.Theme
 import io.legado.app.data.appDb
 import io.legado.app.data.entities.Book
@@ -25,6 +26,7 @@ import io.legado.app.lib.theme.bottomBackground
 import io.legado.app.lib.theme.getPrimaryTextColor
 import io.legado.app.model.BookCover
 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
@@ -278,11 +280,13 @@ class BookInfoActivity :
             true
         }
         tvRead.setOnClickListener {
-            viewModel.bookData.value?.let {
+            viewModel.bookData.value?.let { book ->
                 if (viewModel.isImportBookOnLine) {
-                    viewModel.importBookFileOnLine()
+                    showDialogFragment<ImportOnLineBookFileDialog> {
+                        putString("bookUrl", book.bookUrl)
+                    }
                 } else {
-                    readBook(it)
+                    readBook(book)
                 }
             } ?: toastOnUi("Book is null")
         }
@@ -486,4 +490,9 @@ class BookInfoActivity :
         }
     }
 
+    override fun observeLiveBus() {
+        observeEvent<String>(EventBus.BOOK_URL_CHANGED) {
+            viewModel.changeToLocalBook(it)
+        }
+    }
 }

+ 10 - 18
app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt

@@ -2,6 +2,7 @@ package io.legado.app.ui.book.info
 
 import android.app.Application
 import android.content.Intent
+import android.net.Uri
 import androidx.lifecycle.MutableLiveData
 import androidx.lifecycle.viewModelScope
 import io.legado.app.R
@@ -118,6 +119,9 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
                     WebBook.getBookInfo(this, bookSource, book, canReName = canReName)
                         .onSuccess(IO) {
                             bookData.postValue(book)
+                            if (isImportBookOnLine) {
+                                appDb.searchBookDao.update(book.toSearchBook())
+                            }
                             if (inBookshelf) {
                                 appDb.bookDao.update(book)
                             }
@@ -290,26 +294,14 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
         }
     }
 
-    fun importBookFileOnLine() {
-        execute {
-            //下载类书源的目录链接视为文件链接
-            val book = bookData.value!!
-            val fileUrl = book.tocUrl
-            //切下载链接获取文件名
-            var fileName = fileUrl.substringAfterLast("/")
-            if (fileName.isEmpty()) {
-                fileName = book.name
-            }
-            LocalBook.importFile(fileUrl, fileName, bookSource, book)
-        }.onSuccess {
-            bookData.postValue(it)
-            LocalBook.getChapterList(it).let { toc ->
-                chapterListData.postValue(toc)
-            }
+    fun changeToLocalBook(bookUrl: String) {
+        appDb.bookDao.getBook(bookUrl)?.let { localBook ->
             isImportBookOnLine = false
             inBookshelf = true
-        }.onError {
-            context.toastOnUi("自动导入出错\n${it.localizedMessage}")
+            LocalBook.mergeBook(localBook, bookData.value).let {
+                bookData.postValue(it)
+                loadChapter(it)
+            }
         }
     }
 

+ 2 - 0
app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditActivity.kt

@@ -266,6 +266,7 @@ class BookSourceEditActivity :
             add(EditEntity("coverUrl", ir?.coverUrl, R.string.rule_cover_url))
             add(EditEntity("tocUrl", ir?.tocUrl, R.string.rule_toc_url))
             add(EditEntity("canReName", ir?.canReName, R.string.rule_can_re_name))
+            add(EditEntity("downloadUrls", ir?.downloadUrls, R.string.download_url_rule))
         }
         //目录页
         val tr = source?.getTocRule()
@@ -396,6 +397,7 @@ class BookSourceEditActivity :
                 "tocUrl" -> bookInfoRule.tocUrl =
                     viewModel.ruleComplete(it.value, bookInfoRule.init, 2)
                 "canReName" -> bookInfoRule.canReName = it.value
+                "downloadUrls" -> bookInfoRule.downloadUrls = viewModel.ruleComplete(it.value, bookInfoRule.init)
             }
         }
         tocEntities.forEach {

+ 23 - 0
app/src/main/res/layout/item_book_file_import.xml

@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:padding="8dp">
+
+    <TextView
+        android:id="@+id/cb_file_name"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textColor="@color/primaryText"
+        android:textSize="14sp"
+        app:layout_constrainedWidth="true"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintHorizontal_bias="0"
+        app:layout_constraintHorizontal_chainStyle="packed"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        tools:ignore="TouchTargetSizeCheck" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 2 - 0
app/src/main/res/values-es-rES/strings.xml

@@ -981,5 +981,7 @@
     <string name="auto_save_cookie">CookieJar</string>
     <string name="click_read_button_load">点击阅读加载目录</string>
     <string name="cookie">清除cookie</string>
+    <string name="download_and_import_file">导入在线书籍文件</string>
+
     <!-- string end -->
 </resources>

+ 2 - 0
app/src/main/res/values-ja-rJP/strings.xml

@@ -984,5 +984,7 @@
     <string name="auto_save_cookie">CookieJar</string>
     <string name="click_read_button_load">点击阅读加载目录</string>
     <string name="cookie">清除cookie</string>
+    <string name="download_and_import_file">导入在线书籍文件</string>
+
     <!-- string end -->
 </resources>

+ 2 - 0
app/src/main/res/values-pt-rBR/strings.xml

@@ -984,5 +984,7 @@
     <string name="auto_save_cookie">CookieJar</string>
     <string name="click_read_button_load">点击阅读加载目录</string>
     <string name="cookie">清除cookie</string>
+    <string name="download_and_import_file">导入在线书籍文件</string>
+
     <!-- string end -->
 </resources>

+ 2 - 0
app/src/main/res/values-zh-rHK/strings.xml

@@ -981,5 +981,7 @@
     <string name="auto_save_cookie">CookieJar</string>
     <string name="click_read_button_load">点击阅读加载目录</string>
     <string name="cookie">清除cookie</string>
+    <string name="download_and_import_file">导入在线书籍文件</string>
+
     <!-- string end -->
 </resources>

+ 2 - 0
app/src/main/res/values-zh-rTW/strings.xml

@@ -983,5 +983,7 @@
     <string name="auto_save_cookie">CookieJar</string>
     <string name="click_read_button_load">点击阅读加载目录</string>
     <string name="cookie">清除cookie</string>
+    <string name="download_and_import_file">导入在线书籍文件</string>
+
     <!-- string end -->
 </resources>

+ 1 - 0
app/src/main/res/values-zh/strings.xml

@@ -983,5 +983,6 @@
     <string name="auto_save_cookie">CookieJar</string>
     <string name="click_read_button_load">点击阅读加载目录</string>
     <string name="cookie">清除cookie</string>
+    <string name="download_and_import_file">导入在线书籍文件</string>
     <!-- string end -->
 </resources>

+ 2 - 0
app/src/main/res/values/strings.xml

@@ -984,5 +984,7 @@
     <string name="auto_save_cookie">CookieJar</string>
     <string name="click_read_button_load">点击阅读加载目录</string>
     <string name="cookie">清除cookie</string>
+    <string name="download_and_import_file">导入在线书籍文件</string>
+
     <!-- string end -->
 </resources>