Quellcode durchsuchen

Merge pull request #3118 from Discut/custom_export_dev

Custom export dev
kunfei vor 3 Jahren
Ursprung
Commit
64dc2f1be8

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

@@ -54,6 +54,7 @@ object PreferKey {
     const val webDavAccount = "web_dav_account"
     const val webDavAccount = "web_dav_account"
     const val webDavPassword = "web_dav_password"
     const val webDavPassword = "web_dav_password"
     const val webDavDir = "webDavDir"
     const val webDavDir = "webDavDir"
+    const val enableCustomExport = "enableCustomExport"
     const val exportToWebDav = "webDavCacheBackup"
     const val exportToWebDav = "webDavCacheBackup"
     const val exportNoChapterName = "exportNoChapterName"
     const val exportNoChapterName = "exportNoChapterName"
     const val exportType = "exportType"
     const val exportType = "exportType"

+ 21 - 0
app/src/main/java/io/legado/app/help/book/BookExtensions.kt

@@ -243,4 +243,25 @@ fun Book.getExportFileName(suffix: String): String {
     }.onFailure {
     }.onFailure {
         AppLog.put("导出书名规则错误,使用默认规则\n${it.localizedMessage}", it)
         AppLog.put("导出书名规则错误,使用默认规则\n${it.localizedMessage}", it)
     }.getOrDefault("${name} 作者:${getRealAuthor()}.$suffix")
     }.getOrDefault("${name} 作者:${getRealAuthor()}.$suffix")
+}
+
+/**
+ * 获取分割文件后的文件名
+ */
+fun Book.getExportFileName(suffix: String, epubIndex: Int): String {
+    val jsStr = AppConfig.bookExportFileName
+    // 默认规则
+    val default = "$name 作者:${getRealAuthor()} [${epubIndex}].$suffix"
+    if (jsStr.isNullOrBlank()) {
+        return default
+    }
+    val bindings = SimpleBindings()
+    bindings["name"] = name
+    bindings["author"] = getRealAuthor()
+    bindings["epubIndex"] = epubIndex
+    return kotlin.runCatching {
+        RhinoScriptEngine.eval(jsStr, bindings).toString() + "." + suffix
+    }.onFailure {
+        AppLog.put("导出书名规则错误,使用默认规则\n${it.localizedMessage}", it)
+    }.getOrDefault(default)
 }
 }

+ 6 - 0
app/src/main/java/io/legado/app/help/config/AppConfig.kt

@@ -268,6 +268,12 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
         set(value) {
         set(value) {
             appCtx.putPrefBoolean(PreferKey.exportNoChapterName, value)
             appCtx.putPrefBoolean(PreferKey.exportNoChapterName, value)
         }
         }
+    var enableCustomExport: Boolean
+        get() = appCtx.getPrefBoolean(PreferKey.enableCustomExport)
+        set(value) {
+            appCtx.putPrefBoolean(PreferKey.enableCustomExport, value)
+        }
+
     var exportType: Int
     var exportType: Int
         get() = appCtx.getPrefInt(PreferKey.exportType)
         get() = appCtx.getPrefInt(PreferKey.exportType)
         set(value) {
         set(value) {

+ 119 - 1
app/src/main/java/io/legado/app/ui/book/cache/CacheActivity.kt

@@ -4,7 +4,9 @@ import android.annotation.SuppressLint
 import android.os.Bundle
 import android.os.Bundle
 import android.view.Menu
 import android.view.Menu
 import android.view.MenuItem
 import android.view.MenuItem
+import android.view.View
 import androidx.activity.viewModels
 import androidx.activity.viewModels
+import androidx.appcompat.app.AlertDialog
 import androidx.recyclerview.widget.LinearLayoutManager
 import androidx.recyclerview.widget.LinearLayoutManager
 import io.legado.app.R
 import io.legado.app.R
 import io.legado.app.base.VMBaseActivity
 import io.legado.app.base.VMBaseActivity
@@ -17,6 +19,7 @@ import io.legado.app.data.entities.BookChapter
 import io.legado.app.data.entities.BookGroup
 import io.legado.app.data.entities.BookGroup
 import io.legado.app.databinding.ActivityCacheBookBinding
 import io.legado.app.databinding.ActivityCacheBookBinding
 import io.legado.app.databinding.DialogEditTextBinding
 import io.legado.app.databinding.DialogEditTextBinding
+import io.legado.app.databinding.DialogSelectSectionExportBinding
 import io.legado.app.help.book.isAudio
 import io.legado.app.help.book.isAudio
 import io.legado.app.help.config.AppConfig
 import io.legado.app.help.config.AppConfig
 import io.legado.app.lib.dialogs.SelectItem
 import io.legado.app.lib.dialogs.SelectItem
@@ -33,6 +36,9 @@ import kotlinx.coroutines.flow.*
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 import kotlinx.coroutines.withContext
 
 
+/**
+ * cache/download 缓存界面
+ */
 class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>(),
 class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>(),
     CacheAdapter.CallBack {
     CacheAdapter.CallBack {
 
 
@@ -88,6 +94,8 @@ class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>()
 
 
     override fun onMenuOpened(featureId: Int, menu: Menu): Boolean {
     override fun onMenuOpened(featureId: Int, menu: Menu): Boolean {
         menu.findItem(R.id.menu_enable_replace)?.isChecked = AppConfig.exportUseReplace
         menu.findItem(R.id.menu_enable_replace)?.isChecked = AppConfig.exportUseReplace
+        // 菜单打开时读取状态[enableCustomExport]
+        menu.findItem(R.id.menu_enable_custom_export)?.isChecked = AppConfig.enableCustomExport
         menu.findItem(R.id.menu_export_no_chapter_name)?.isChecked = AppConfig.exportNoChapterName
         menu.findItem(R.id.menu_export_no_chapter_name)?.isChecked = AppConfig.exportNoChapterName
         menu.findItem(R.id.menu_export_web_dav)?.isChecked = AppConfig.exportToWebDav
         menu.findItem(R.id.menu_export_web_dav)?.isChecked = AppConfig.exportToWebDav
         menu.findItem(R.id.menu_export_pics_file)?.isChecked = AppConfig.exportPictureFile
         menu.findItem(R.id.menu_export_pics_file)?.isChecked = AppConfig.exportPictureFile
@@ -108,6 +116,9 @@ class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>()
         }
         }
     }
     }
 
 
+    /**
+     * 菜单按下回调
+     */
     override fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
     override fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
         when (item.itemId) {
         when (item.itemId) {
             R.id.menu_download -> {
             R.id.menu_download -> {
@@ -124,8 +135,11 @@ class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>()
                     CacheBook.stop(this@CacheActivity)
                     CacheBook.stop(this@CacheActivity)
                 }
                 }
             }
             }
+
             R.id.menu_export_all -> exportAll()
             R.id.menu_export_all -> exportAll()
             R.id.menu_enable_replace -> AppConfig.exportUseReplace = !item.isChecked
             R.id.menu_enable_replace -> AppConfig.exportUseReplace = !item.isChecked
+            // 更改菜单状态[enableCustomExport]
+            R.id.menu_enable_custom_export -> AppConfig.enableCustomExport = !item.isChecked
             R.id.menu_export_no_chapter_name -> AppConfig.exportNoChapterName = !item.isChecked
             R.id.menu_export_no_chapter_name -> AppConfig.exportNoChapterName = !item.isChecked
             R.id.menu_export_web_dav -> AppConfig.exportToWebDav = !item.isChecked
             R.id.menu_export_web_dav -> AppConfig.exportToWebDav = !item.isChecked
             R.id.menu_export_pics_file -> AppConfig.exportPictureFile = !item.isChecked
             R.id.menu_export_pics_file -> AppConfig.exportPictureFile = !item.isChecked
@@ -133,6 +147,7 @@ class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>()
             R.id.menu_export_folder -> {
             R.id.menu_export_folder -> {
                 selectExportFolder(-1)
                 selectExportFolder(-1)
             }
             }
+
             R.id.menu_export_file_name -> alertExportFileName()
             R.id.menu_export_file_name -> alertExportFileName()
             R.id.menu_export_type -> showExportTypeConfig()
             R.id.menu_export_type -> showExportTypeConfig()
             R.id.menu_export_charset -> showCharsetConfig()
             R.id.menu_export_charset -> showCharsetConfig()
@@ -170,6 +185,7 @@ class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>()
                     2 -> booksDownload.sortedWith { o1, o2 ->
                     2 -> booksDownload.sortedWith { o1, o2 ->
                         o1.name.cnCompare(o2.name)
                         o1.name.cnCompare(o2.name)
                     }
                     }
+
                     3 -> booksDownload.sortedBy { it.order }
                     3 -> booksDownload.sortedBy { it.order }
                     else -> booksDownload.sortedByDescending { it.durChapterTime }
                     else -> booksDownload.sortedByDescending { it.durChapterTime }
                 }
                 }
@@ -233,6 +249,8 @@ class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>()
         val path = ACache.get().getAsString(exportBookPathKey)
         val path = ACache.get().getAsString(exportBookPathKey)
         if (path.isNullOrEmpty()) {
         if (path.isNullOrEmpty()) {
             selectExportFolder(position)
             selectExportFolder(position)
+        } else if (AppConfig.enableCustomExport && AppConfig.exportType == 1) {// 启用自定义导出 and 导出类型为Epub
+            configExportSection(path, position)
         } else {
         } else {
             startExport(path, position)
             startExport(path, position)
         }
         }
@@ -247,6 +265,89 @@ class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>()
         }
         }
     }
     }
 
 
+    /**
+     * 配置自定义导出对话框
+     *
+     * @param path  导出路径
+     * @param position  book位置
+     * @author Discut
+     * @since 1.0.0
+     */
+    private fun configExportSection(path: String, position: Int) {
+        val alertBinding = DialogSelectSectionExportBinding.inflate(layoutInflater)
+            .apply {
+                etEpubSize.setText("1")
+                tvAllExport.setOnClickListener {
+                    cbAllExport.callOnClick()
+                }
+                tvSelectExport.setOnClickListener {
+                    cbSelectExport.callOnClick()
+                }
+                cbSelectExport.onCheckedChangeListener = { _, isChecked ->
+                    if (isChecked) {
+                        etEpubSize.isEnabled = true
+                        etInputScope.isEnabled = true
+                        cbAllExport.isChecked = false
+                    }
+                }
+                cbAllExport.onCheckedChangeListener = { _, isChecked ->
+                    if (isChecked) {
+                        etEpubSize.isEnabled = false
+                        etInputScope.isEnabled = false
+                        cbSelectExport.isChecked = false
+                    }
+                }
+
+                etInputScope.onFocusChangeListener =
+                    View.OnFocusChangeListener { _, hasFocus ->
+                        if (hasFocus) {
+                            etInputScope.hint = "1-5,8,10-18"
+                        } else {
+                            etInputScope.hint = ""
+                        }
+                    }
+                cbAllExport.callOnClick()
+            }
+        val alertDialog = alert(titleResource = R.string.select_section_export) {
+            customView { alertBinding.root }
+            positiveButton(R.string.ok)
+            cancelButton()
+        }
+        alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
+            alertBinding.apply {
+                if (cbAllExport.isChecked) {
+                    startExport(path, position)
+                    alertDialog.hide()
+                    return@apply
+                }
+                val text = etInputScope.text
+                if (!verificationField(text.toString())) {
+                    etInputScope.error =
+                        applicationContext.getString(R.string.error_scope_input)//"请输入正确的范围"
+                    return@apply
+                }
+                etInputScope.error = null
+                val toInt = etEpubSize.text.toString().toInt()
+                startExport(path, position, toInt, text.toString())
+                alertDialog.hide()
+
+            }
+
+        }
+    }
+
+    /**
+     * 验证 输入的范围 是否正确
+     *
+     * @since 1.0.0
+     * @author Discut
+     * @param text 输入的范围 字符串
+     * @return 是否正确
+     */
+    private fun verificationField(text: String): Boolean {
+        return text.matches(Regex("\\d+(-\\d+)?(,\\d+(-\\d+)?)*"))
+    }
+
     private fun selectExportFolder(exportPosition: Int) {
     private fun selectExportFolder(exportPosition: Int) {
         val default = arrayListOf<SelectItem<Int>>()
         val default = arrayListOf<SelectItem<Int>>()
         val path = ACache.get().getAsString(exportBookPathKey)
         val path = ACache.get().getAsString(exportBookPathKey)
@@ -259,6 +360,18 @@ class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>()
         }
         }
     }
     }
 
 
+    private fun startExport(path: String, exportPosition: Int, size: Int, scope: String) {
+        if (exportPosition >= 0) {
+            adapter.getItem(exportPosition)?.let { book ->
+                when (AppConfig.exportType) {
+                    1 -> viewModel.exportEPUBs(path, book, size, scope)
+                    // 目前仅支持 epub
+                    //else -> viewModel.export(path, book)
+                }
+            }
+        }
+    }
+
     private fun startExport(path: String, exportPosition: Int) {
     private fun startExport(path: String, exportPosition: Int) {
         if (exportPosition == -10) {
         if (exportPosition == -10) {
             if (adapter.getItems().isNotEmpty()) {
             if (adapter.getItems().isNotEmpty()) {
@@ -284,7 +397,12 @@ class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>()
     @SuppressLint("SetTextI18n")
     @SuppressLint("SetTextI18n")
     private fun alertExportFileName() {
     private fun alertExportFileName() {
         alert(R.string.export_file_name) {
         alert(R.string.export_file_name) {
-            setMessage("js内有name和author变量,返回书名")
+            var message =
+                "js内有name和author变量,返回书名\n启用自定义epub导出章节时包含额外变量[epubIndex]"
+            if (AppConfig.bookExportFileName.isNullOrBlank()) {
+                message += "\n例如:\nname+\"-\"+author+(epubIndex?\"(\"+epubIndex+\")\":\"\")"
+            }
+            setMessage(message)
             val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
             val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
                 editView.hint = "file name js"
                 editView.hint = "file name js"
                 editView.setText(AppConfig.bookExportFileName)
                 editView.setText(AppConfig.bookExportFileName)

+ 330 - 0
app/src/main/java/io/legado/app/ui/book/cache/CacheViewModel.kt

@@ -4,6 +4,7 @@ import android.app.Application
 import android.graphics.Bitmap
 import android.graphics.Bitmap
 import android.graphics.drawable.Drawable
 import android.graphics.drawable.Drawable
 import android.net.Uri
 import android.net.Uri
+import android.util.ArraySet
 import androidx.documentfile.provider.DocumentFile
 import androidx.documentfile.provider.DocumentFile
 import androidx.lifecycle.MutableLiveData
 import androidx.lifecycle.MutableLiveData
 import com.bumptech.glide.Glide
 import com.bumptech.glide.Glide
@@ -41,6 +42,7 @@ import java.nio.charset.Charset
 import java.nio.file.*
 import java.nio.file.*
 import java.util.*
 import java.util.*
 import java.util.concurrent.ConcurrentHashMap
 import java.util.concurrent.ConcurrentHashMap
+import kotlin.collections.ArrayList
 import kotlin.coroutines.coroutineContext
 import kotlin.coroutines.coroutineContext
 
 
 
 
@@ -248,7 +250,56 @@ class CacheViewModel(application: Application) : BaseViewModel(application) {
         }
         }
     }
     }
 
 
+    /**
+     * 解析范围字符串
+     *
+     * @param scope 范围字符串
+     * @return 范围
+     *
+     * @since 2023/5/22
+     * @author Discut
+     */
+    private fun paresScope(scope: String): IntArray {
+        val split = scope.split(",")
+        val result = ArraySet<Int>()
+        for (s in split) {
+            val v = s.split("-")
+            if (v.size != 2) {
+                result.add(s.toInt() - 1)
+                continue
+            }
+            val left = v[0].toInt()
+            val right = v[1].toInt()
+            if (left > right){
+                AppLog.put("Error expression : $s; left > right")
+                continue
+            }
+            for (i in left..right)
+                result.add(i - 1)
+        }
+        return result.toIntArray()
+    }
+
+
     //////////////////Start EPUB
     //////////////////Start EPUB
+    /**
+     * 导出Epub 根据自定义导出范围
+     *
+     * @param path  导出路径
+     * @param book  书籍
+     * @param size  每本Epub包含的章节
+     * @param scope  导出范围
+     * @since 2023/5/22
+     */
+    fun exportEPUBs(path: String, book: Book, size: Int = 1, scope: String) {
+        if (exportProgress.contains(book.bookUrl)) return
+        CustomExporter(this).let {
+            it.scope = paresScope(scope)
+            it.size = size
+            it.export(path, book)
+        }
+    }
+
     /**
     /**
      * 导出Epub
      * 导出Epub
      */
      */
@@ -373,6 +424,7 @@ class CacheViewModel(application: Application) : BaseViewModel(application) {
                                             )
                                             )
                                         )
                                         )
                                     }
                                     }
+
                                     else -> {
                                     else -> {
                                         //其他格式文件当做资源文件
                                         //其他格式文件当做资源文件
                                         folder.listFiles().forEach {
                                         folder.listFiles().forEach {
@@ -575,4 +627,282 @@ class CacheViewModel(application: Application) : BaseViewModel(application) {
     }
     }
 
 
     //////end of EPUB
     //////end of EPUB
+
+    //////start of custom exporter
+    /**
+     * 自定义Exporter
+     *
+     * @since 2023/5/23
+     */
+    class CustomExporter(private val context: CacheViewModel) {
+        var scope: IntArray = IntArray(0)
+        var size: Int = 1
+
+        /**
+         * 导出Epub
+         *
+         * from [io.legado.app.ui.book.cache.CacheViewModel.exportEPUB]
+         * @param path 导出的路径
+         * @param book 书籍
+         */
+        fun export(
+            path: String,
+            book: Book
+        ) {
+            context.exportProgress[book.bookUrl] = 0
+            context.exportMsg.remove(book.bookUrl)
+            context.upAdapterLiveData.sendValue(book.bookUrl)
+            context.execute {
+                context.mutex.withLock {
+                    while (context.exportNumber > 0) {
+                        delay(1000)
+                    }
+                    context.exportNumber++
+                }
+                if (path.isContentScheme()) {
+                    val uri = Uri.parse(path)
+                    val doc = DocumentFile.fromTreeUri(context.context, uri)
+                        ?: throw NoStackTraceException("获取导出文档失败")
+                    exportEpub(doc, book)
+                } else {
+                    exportEpub(File(path).createFolderIfNotExist(), book)
+                }
+            }.onError {
+                context.exportProgress.remove(book.bookUrl)
+                context.exportMsg[book.bookUrl] = it.localizedMessage ?: "ERROR"
+                context.upAdapterLiveData.postValue(book.bookUrl)
+                it.printStackTrace()
+                AppLog.put("导出epub书籍<${book.name}>出错\n${it.localizedMessage}", it)
+            }.onSuccess {
+                context.exportProgress.remove(book.bookUrl)
+                context.exportMsg[book.bookUrl] = context.context.getString(R.string.export_success)
+                context.upAdapterLiveData.postValue(book.bookUrl)
+            }.onFinally {
+                context.exportNumber--
+            }
+        }
+
+        /**
+         * 导出 epub
+         *
+         * from [io.legado.app.ui.book.cache.CacheViewModel.exportEpub]
+         */
+        private suspend fun exportEpub(file: File, book: Book) {
+            val (contentModel, epubList) = createEpubs(book)
+            epubList.forEachIndexed { index, ep ->
+                val (filename, epubBook) = ep
+                //设置正文
+                this.setEpubContent(contentModel, book, epubBook, index)
+                save2Drive(filename, epubBook, file)
+            }
+
+        }
+
+        /**
+         * 导出 epub
+         *
+         * from [io.legado.app.ui.book.cache.CacheViewModel.exportEpub]
+         */
+        private suspend fun exportEpub(doc: DocumentFile, book: Book) {
+            val (contentModel, epubList) = createEpubs(doc, book)
+            epubList.forEachIndexed { index, ep ->
+                val (filename, epubBook) = ep
+                //设置正文
+                this.setEpubContent(contentModel, book, epubBook, index)
+                save2Drive(filename, epubBook, doc)
+            }
+
+        }
+
+
+        /**
+         * 设置epub正文
+         *
+         * from [io.legado.app.ui.book.cache.CacheViewModel.setEpubContent]
+         *
+         * @param contentModel 正文模板
+         * @param book 书籍
+         * @param epubBook 分割后的epub
+         * @param epubBookIndex 分割后的epub序号
+         */
+        private suspend fun setEpubContent(
+            contentModel: String,
+            book: Book,
+            epubBook: EpubBook,
+            epubBookIndex: Int
+        ) {
+            //正文
+            val useReplace = AppConfig.exportUseReplace && book.getUseReplaceRule()
+            val contentProcessor = ContentProcessor.get(book.name, book.origin)
+            var chapterList: MutableList<BookChapter> = ArrayList()
+            appDb.bookChapterDao.getChapterList(book.bookUrl).forEachIndexed { index, chapter ->
+                if (scope.indexOf(index) >= 0) {
+                    chapterList.add(chapter)
+                }
+                if (scope.size == chapterList.size) {
+                    return@forEachIndexed
+                }
+            }
+            val totalChapterNum = book.totalChapterNum / scope.size
+            if (chapterList.size == 0) {
+                throw RuntimeException("书籍<${book.name}>(${epubBookIndex + 1})未找到章节信息")
+            }
+            chapterList = chapterList.subList(
+                epubBookIndex * size,
+                if ((epubBookIndex + 1) * size > scope.size) scope.size else (epubBookIndex + 1) * size
+            )
+            chapterList.forEachIndexed { index, chapter ->
+                coroutineContext.ensureActive()
+                context.upAdapterLiveData.postValue(book.bookUrl)
+                context.exportProgress[book.bookUrl] =
+                    totalChapterNum * (epubBookIndex * size + index)
+                BookHelp.getContent(book, chapter).let { content ->
+                    var content1 = context.fixPic(
+                        epubBook,
+                        book,
+                        content ?: if (chapter.isVolume) "" else "null",
+                        chapter
+                    )
+                    content1 = contentProcessor
+                        .getContent(
+                            book,
+                            chapter,
+                            content1,
+                            includeTitle = false,
+                            useReplace = useReplace,
+                            chineseConvert = false,
+                            reSegment = false
+                        ).toString()
+                    val title = chapter.run {
+                        // 不导出vip标识
+                        isVip = false
+                        getDisplayTitle(
+                            contentProcessor.getTitleReplaceRules(),
+                            useReplace = useReplace
+                        )
+                    }
+                    epubBook.addSection(
+                        title,
+                        ResourceUtil.createChapterResource(
+                            title.replace("\uD83D\uDD12", ""),
+                            content1,
+                            contentModel,
+                            "Text/chapter_${index}.html"
+                        )
+                    )
+                }
+            }
+        }
+
+        /**
+         * 创建多个epub 对象
+         *
+         * 分割epub时,一个书籍需要创建多个epub对象
+         *
+         * @param doc 导出文档
+         * @param book 书籍
+         *
+         * @return <内容模板字符串, <epub文件名, epub对象>>
+         */
+        private fun createEpubs(
+            doc: DocumentFile,
+            book: Book
+        ): Pair<String, List<Pair<String, EpubBook>>> {
+            val paresNumOfEpub = paresNumOfEpub(scope.size, size)
+            val result: MutableList<Pair<String, EpubBook>> = ArrayList(paresNumOfEpub)
+            var contentModel = ""
+            for (i in 1..paresNumOfEpub) {
+                val filename = book.getExportFileName("epub", i)
+                DocumentUtils.delete(doc, filename)
+                val epubBook = EpubBook()
+                epubBook.version = "2.0"
+                //set metadata
+                context.setEpubMetadata(book, epubBook)
+                //set cover
+                context.setCover(book, epubBook)
+                //set css
+                contentModel = context.setAssets(doc, book, epubBook)
+
+                // add epubBook
+                result.add(Pair(filename, epubBook))
+            }
+            return Pair(contentModel, result)
+        }
+
+        /**
+         * 创建多个epub 对象
+         *
+         * 分割epub时,一个书籍需要创建多个epub对象
+         *
+         * @param book 书籍
+         *
+         * @return <内容模板字符串, <epub文件名, epub对象>>
+         */
+        private fun createEpubs(
+            book: Book
+        ): Pair<String, List<Pair<String, EpubBook>>> {
+            val paresNumOfEpub = paresNumOfEpub(scope.size, size)
+            val result: MutableList<Pair<String, EpubBook>> = ArrayList(paresNumOfEpub)
+            var contentModel = ""
+            for (i in 1..paresNumOfEpub) {
+                val filename = book.getExportFileName("epub", i)
+                val epubBook = EpubBook()
+                epubBook.version = "2.0"
+                //set metadata
+                context.setEpubMetadata(book, epubBook)
+                //set cover
+                context.setCover(book, epubBook)
+                //set css
+                contentModel = context.setAssets(book, epubBook)
+
+                // add epubBook
+                result.add(Pair(filename, epubBook))
+            }
+            return Pair(contentModel, result)
+        }
+
+        /**
+         * 保存文件到 设备
+         */
+        private suspend fun save2Drive(filename: String, epubBook: EpubBook, doc: DocumentFile) {
+            DocumentUtils.createFileIfNotExist(doc, filename)?.let { bookDoc ->
+                context.context.contentResolver.openOutputStream(bookDoc.uri, "wa")?.use { bookOs ->
+                    EpubWriter().write(epubBook, bookOs)
+                }
+                if (AppConfig.exportToWebDav) {
+                    // 导出到webdav
+                    AppWebDav.exportWebDav(bookDoc.uri, filename)
+                }
+            }
+        }
+
+        /**
+         * 保存文件到 设备
+         */
+        private suspend fun save2Drive(filename: String, epubBook: EpubBook, file: File) {
+            val bookPath = FileUtils.getPath(file, filename)
+            val bookFile = FileUtils.createFileWithReplace(bookPath)
+            @Suppress("BlockingMethodInNonBlockingContext")
+            EpubWriter().write(epubBook, FileOutputStream(bookFile))
+            if (AppConfig.exportToWebDav) {
+                // 导出到webdav
+                AppWebDav.exportWebDav(Uri.fromFile(bookFile), filename)
+            }
+        }
+
+        /**
+         * 解析 分割epub后的数量
+         *
+         * @param total 章节总数
+         * @param size 每个epub文件包含多少章节
+         */
+        private fun paresNumOfEpub(total: Int, size: Int): Int {
+            val i = total % size
+            var result = total / size
+            if (i > 0) {
+                result++
+            }
+            return result
+        }
+    }
 }
 }

+ 90 - 0
app/src/main/res/layout/dialog_select_section_export.xml

@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:padding="16dp">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="vertical">
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="12dp"
+            android:orientation="horizontal">
+
+            <io.legado.app.ui.widget.checkbox.SmoothCheckBox
+                android:id="@+id/cb_all_export"
+                android:layout_width="20dp"
+                android:layout_height="20dp"
+                android:layout_marginStart="6dp"
+                android:layout_marginEnd="16dp" />
+
+            <TextView
+                android:id="@+id/tv_all_export"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="@string/export_all"
+                android:textSize="18sp" />
+
+        </LinearLayout>
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="12dp"
+            android:orientation="horizontal">
+
+            <io.legado.app.ui.widget.checkbox.SmoothCheckBox
+                android:id="@+id/cb_select_export"
+                android:layout_width="20dp"
+                android:layout_height="20dp"
+                android:layout_marginStart="6dp"
+                android:layout_marginEnd="16dp" />
+
+            <TextView
+                android:id="@+id/tv_select_export"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="@string/custom_export"
+                android:textSize="18sp" />
+
+        </LinearLayout>
+
+        <io.legado.app.ui.widget.text.TextInputLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="12dp"
+            android:hint="@string/file_contains_number">
+
+            <io.legado.app.lib.theme.view.ThemeEditText
+                android:id="@+id/et_epub_size"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:maxLength="6"
+                android:inputType="number"
+                tools:ignore="SpeakableTextPresentCheck,TouchTargetSizeCheck" />
+
+        </io.legado.app.ui.widget.text.TextInputLayout>
+
+        <io.legado.app.ui.widget.text.TextInputLayout
+            android:id="@+id/ly_et_input_scope"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:hint="@string/export_chapter_index">
+
+            <io.legado.app.lib.theme.view.ThemeEditText
+                android:id="@+id/et_input_scope"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                tools:ignore="SpeakableTextPresentCheck,TouchTargetSizeCheck" />
+
+        </io.legado.app.ui.widget.text.TextInputLayout>
+
+    </LinearLayout>
+
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 7 - 0
app/src/main/res/menu/book_cache.xml

@@ -31,6 +31,13 @@
         android:checkable="true"
         android:checkable="true"
         app:showAsAction="never" />
         app:showAsAction="never" />
 
 
+    <!--自定义导出章节 选择菜单-->
+    <item
+        android:id="@+id/menu_enable_custom_export"
+        android:title="@string/custom_export_section"
+        android:checkable="true"
+        app:showAsAction="never" />
+
     <item
     <item
         android:id="@+id/menu_export_web_dav"
         android:id="@+id/menu_export_web_dav"
         android:title="@string/export_to_web_dav"
         android:title="@string/export_to_web_dav"

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

@@ -33,6 +33,7 @@
     <string name="replace">Reemplazar</string>
     <string name="replace">Reemplazar</string>
     <string name="replace_purify">Reemplazo</string>
     <string name="replace_purify">Reemplazo</string>
     <string name="replace_purify_desc">Configurar reglas de reemplazo</string>
     <string name="replace_purify_desc">Configurar reglas de reemplazo</string>
+    <string name="custom_export_section">Custom export chapter of epub</string>
     <string name="not_available">No disponible</string>
     <string name="not_available">No disponible</string>
     <string name="enable">Activar</string>
     <string name="enable">Activar</string>
     <string name="replace_purify_search">Buscar reemplazo</string>
     <string name="replace_purify_search">Buscar reemplazo</string>
@@ -1104,4 +1105,9 @@
     <string name="replace_exclude_scope">排除范围,选填书名或者书源 URL</string>
     <string name="replace_exclude_scope">排除范围,选填书名或者书源 URL</string>
     <string name="format_js_rule">格式化规则(formatJs)</string>
     <string name="format_js_rule">格式化规则(formatJs)</string>
     <string name="adjust_pos">调整位置</string>
     <string name="adjust_pos">调整位置</string>
+    <string name="select_section_export">Choose some chapters to be exported</string>
+    <string name="error_scope_input">Please enter the correct range</string>
+    <string name="custom_export">Custom Export</string>
+    <string name="file_contains_number">The number of chapters contained in each file</string>
+    <string name="export_chapter_index">The section index that needs to be exported</string>
 </resources>
 </resources>

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

@@ -44,6 +44,7 @@
     <string name="rss">Subscription</string>
     <string name="rss">Subscription</string>
     <string name="all">All</string>
     <string name="all">All</string>
     <string name="recent_reading">Recent reading</string>
     <string name="recent_reading">Recent reading</string>
+    <string name="custom_export_section">カスタムEPUBエクスポートの章</string>
     <string name="last_read">Last reading</string>
     <string name="last_read">Last reading</string>
     <string name="update_log">What\'s new</string>
     <string name="update_log">What\'s new</string>
     <string name="bookshelf_empty">The bookshelf is still empty. Search for books or add them from discovery! \n if you use it for the first time, please open legado.top get help!</string>
     <string name="bookshelf_empty">The bookshelf is still empty. Search for books or add them from discovery! \n if you use it for the first time, please open legado.top get help!</string>
@@ -1107,4 +1108,9 @@
     <string name="replace_exclude_scope">排除范围,选填书名或者书源 URL</string>
     <string name="replace_exclude_scope">排除范围,选填书名或者书源 URL</string>
     <string name="format_js_rule">格式化规则(formatJs)</string>
     <string name="format_js_rule">格式化规则(formatJs)</string>
     <string name="adjust_pos">调整位置</string>
     <string name="adjust_pos">调整位置</string>
+    <string name="select_section_export">Choose some chapters to be exported</string>
+    <string name="error_scope_input">Please enter the correct range</string>
+    <string name="custom_export">Custom Export</string>
+    <string name="file_contains_number">The number of chapters contained in each file</string>
+    <string name="export_chapter_index">The section index that needs to be exported</string>
 </resources>
 </resources>

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

@@ -33,6 +33,7 @@
     <string name="replace">Substituir</string>
     <string name="replace">Substituir</string>
     <string name="replace_purify">Substituição</string>
     <string name="replace_purify">Substituição</string>
     <string name="replace_purify_desc">Configurar regras de substituição</string>
     <string name="replace_purify_desc">Configurar regras de substituição</string>
+    <string name="custom_export_section">Custom export chapter of epub</string>
     <string name="not_available">Indisponível</string>
     <string name="not_available">Indisponível</string>
     <string name="enable">Ativar</string>
     <string name="enable">Ativar</string>
     <string name="replace_purify_search">Procurar o substituto</string>
     <string name="replace_purify_search">Procurar o substituto</string>
@@ -1107,4 +1108,9 @@
     <string name="replace_exclude_scope">排除范围,选填书名或者书源 URL</string>
     <string name="replace_exclude_scope">排除范围,选填书名或者书源 URL</string>
     <string name="format_js_rule">格式化规则(formatJs)</string>
     <string name="format_js_rule">格式化规则(formatJs)</string>
     <string name="adjust_pos">调整位置</string>
     <string name="adjust_pos">调整位置</string>
+    <string name="select_section_export">Choose some chapters to be exported</string>
+    <string name="error_scope_input">Please enter the correct range</string>
+    <string name="custom_export">Custom Export</string>
+    <string name="file_contains_number">The index of chapters contained in each file</string>
+    <string name="export_chapter_index">The section index that needs to be exported</string>
 </resources>
 </resources>

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

@@ -33,6 +33,7 @@
     <string name="not_available">暫無</string>
     <string name="not_available">暫無</string>
     <string name="enable">啟用</string>
     <string name="enable">啟用</string>
     <string name="replace_purify_search">替換淨化-搜尋</string>
     <string name="replace_purify_search">替換淨化-搜尋</string>
+    <string name="custom_export_section">自定義Epub導出章節</string>
     <string name="bookshelf">書架</string>
     <string name="bookshelf">書架</string>
     <string name="favorites">收藏夾</string>
     <string name="favorites">收藏夾</string>
     <string name="favorite">收藏</string>
     <string name="favorite">收藏</string>
@@ -1104,4 +1105,9 @@
     <string name="replace_exclude_scope">排除范围,选填书名或者书源 URL</string>
     <string name="replace_exclude_scope">排除范围,选填书名或者书源 URL</string>
     <string name="format_js_rule">格式化规则(formatJs)</string>
     <string name="format_js_rule">格式化规则(formatJs)</string>
     <string name="adjust_pos">调整位置</string>
     <string name="adjust_pos">调整位置</string>
+    <string name="select_section_export">選擇待導出章節</string>
+    <string name="error_scope_input">請輸入正確的範圍</string>
+    <string name="custom_export">自定義導出</string>
+    <string name="file_contains_number">每個文件包含的章節數量</string>
+    <string name="export_chapter_index">需要輸出的章節</string>
 </resources>
 </resources>

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

@@ -32,6 +32,7 @@
     <string name="replace">取代</string>
     <string name="replace">取代</string>
     <string name="replace_purify">取代淨化</string>
     <string name="replace_purify">取代淨化</string>
     <string name="replace_purify_desc">配置取代淨化規則</string>
     <string name="replace_purify_desc">配置取代淨化規則</string>
+    <string name="custom_export_section">自定義Epub導出章節</string>
     <string name="not_available">暫無</string>
     <string name="not_available">暫無</string>
     <string name="enable">啟用</string>
     <string name="enable">啟用</string>
     <string name="replace_purify_search">取代淨化-搜尋</string>
     <string name="replace_purify_search">取代淨化-搜尋</string>
@@ -1106,4 +1107,9 @@
     <string name="replace_exclude_scope">排除范围,选填书名或者书源 URL</string>
     <string name="replace_exclude_scope">排除范围,选填书名或者书源 URL</string>
     <string name="format_js_rule">格式化规则(formatJs)</string>
     <string name="format_js_rule">格式化规则(formatJs)</string>
     <string name="adjust_pos">调整位置</string>
     <string name="adjust_pos">调整位置</string>
+    <string name="select_section_export">選擇待導出章節</string>
+    <string name="error_scope_input">請輸入正確的範圍</string>
+    <string name="custom_export">自定義導出</string>
+    <string name="file_contains_number">每個文件包含的章節數量</string>
+    <string name="export_chapter_index">需要輸出的章節</string>
 </resources>
 </resources>

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

@@ -31,6 +31,7 @@
     <string name="delete_all">删除所有</string>
     <string name="delete_all">删除所有</string>
     <string name="replace">替换</string>
     <string name="replace">替换</string>
     <string name="replace_purify">替换净化</string>
     <string name="replace_purify">替换净化</string>
+    <string name="custom_export_section">自定义Epub导出章节</string>
     <string name="replace_purify_desc">配置替换净化规则</string>
     <string name="replace_purify_desc">配置替换净化规则</string>
     <string name="not_available">暂无</string>
     <string name="not_available">暂无</string>
     <string name="enable">启用</string>
     <string name="enable">启用</string>
@@ -346,6 +347,7 @@
     <string name="ps_show_all_find">关闭则只显示勾选源的发现</string>
     <string name="ps_show_all_find">关闭则只显示勾选源的发现</string>
     <string name="update_toc">更新目录</string>
     <string name="update_toc">更新目录</string>
     <string name="txt_toc_rule">TXT 目录规则</string>
     <string name="txt_toc_rule">TXT 目录规则</string>
+    <string name="select_section_export">选择待导出章节</string>
     <string name="set_charset">设置编码</string>
     <string name="set_charset">设置编码</string>
     <string name="swap_sort">倒序-顺序</string>
     <string name="swap_sort">倒序-顺序</string>
     <string name="sort">排序</string>
     <string name="sort">排序</string>
@@ -1106,4 +1108,8 @@
     <string name="replace_exclude_scope">排除范围,选填书名或者书源 URL</string>
     <string name="replace_exclude_scope">排除范围,选填书名或者书源 URL</string>
     <string name="format_js_rule">格式化规则(formatJs)</string>
     <string name="format_js_rule">格式化规则(formatJs)</string>
     <string name="adjust_pos">调整位置</string>
     <string name="adjust_pos">调整位置</string>
+    <string name="error_scope_input">请输入正确的范围</string>
+    <string name="custom_export">自定义导出</string>
+    <string name="file_contains_number">每个文件包含的章节数量</string>
+    <string name="export_chapter_index">需要输出的章节</string>
 </resources>
 </resources>

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

@@ -30,6 +30,7 @@
     <string name="edit">Edit</string>
     <string name="edit">Edit</string>
     <string name="delete">Delete</string>
     <string name="delete">Delete</string>
     <string name="delete_all">Delete all</string>
     <string name="delete_all">Delete all</string>
+    <string name="custom_export_section">Custom export chapter of epub</string>
     <string name="replace">Replace</string>
     <string name="replace">Replace</string>
     <string name="replace_purify">Replacement</string>
     <string name="replace_purify">Replacement</string>
     <string name="replace_purify_desc">Configure replacement rules</string>
     <string name="replace_purify_desc">Configure replacement rules</string>
@@ -347,6 +348,7 @@
     <string name="ps_show_all_find">Display the selected origin\'s Discovery if closed</string>
     <string name="ps_show_all_find">Display the selected origin\'s Discovery if closed</string>
     <string name="update_toc">Update chapters</string>
     <string name="update_toc">Update chapters</string>
     <string name="txt_toc_rule">Txt Chapters Rule</string>
     <string name="txt_toc_rule">Txt Chapters Rule</string>
+    <string name="select_section_export">Choose some chapters to be exported</string>
     <string name="set_charset">Text encoding</string>
     <string name="set_charset">Text encoding</string>
     <string name="swap_sort">Ascending/Descending order</string>
     <string name="swap_sort">Ascending/Descending order</string>
     <string name="sort">Sort</string>
     <string name="sort">Sort</string>
@@ -1107,4 +1109,8 @@
     <string name="replace_exclude_scope">排除范围,选填书名或者书源 URL</string>
     <string name="replace_exclude_scope">排除范围,选填书名或者书源 URL</string>
     <string name="format_js_rule">格式化规则(formatJs)</string>
     <string name="format_js_rule">格式化规则(formatJs)</string>
     <string name="adjust_pos">调整位置</string>
     <string name="adjust_pos">调整位置</string>
+    <string name="error_scope_input">Please enter the correct range</string>
+    <string name="custom_export">Custom Export</string>
+    <string name="file_contains_number">The number of chapters contained in each file</string>
+    <string name="export_chapter_index">The section index that needs to be exported</string>
 </resources>
 </resources>