diff --git a/app/src/main/java/io/legado/app/constant/PreferKey.kt b/app/src/main/java/io/legado/app/constant/PreferKey.kt index a58c23b77..53ba82c0b 100644 --- a/app/src/main/java/io/legado/app/constant/PreferKey.kt +++ b/app/src/main/java/io/legado/app/constant/PreferKey.kt @@ -39,6 +39,7 @@ object PreferKey { const val bookshelfSort = "bookshelfSort" const val bookExportFileName = "bookExportFileName" const val bookImportFileName = "bookImportFileName" + const val episodeExportFileName = "episodeExportFileName" const val recordLog = "recordLog" const val processText = "process_text" const val cleanCache = "cleanCache" diff --git a/app/src/main/java/io/legado/app/help/book/BookExtensions.kt b/app/src/main/java/io/legado/app/help/book/BookExtensions.kt index a5a95db19..db1d15b9e 100644 --- a/app/src/main/java/io/legado/app/help/book/BookExtensions.kt +++ b/app/src/main/java/io/legado/app/help/book/BookExtensions.kt @@ -236,6 +236,7 @@ fun Book.getExportFileName(suffix: String): String { return "$name 作者:${getRealAuthor()}.$suffix" } val bindings = SimpleBindings() + bindings["epubIndex"] = ""// 兼容老版本,修复可能存在的错误 bindings["name"] = name bindings["author"] = getRealAuthor() return kotlin.runCatching { @@ -248,8 +249,11 @@ fun Book.getExportFileName(suffix: String): String { /** * 获取分割文件后的文件名 */ -fun Book.getExportFileName(suffix: String, epubIndex: Int): String { - val jsStr = AppConfig.bookExportFileName +fun Book.getExportFileName( + suffix: String, + epubIndex: Int, + jsStr: String? = AppConfig.episodeExportFileName +): String { // 默认规则 val default = "$name 作者:${getRealAuthor()} [${epubIndex}].$suffix" if (jsStr.isNullOrBlank()) { @@ -264,4 +268,15 @@ fun Book.getExportFileName(suffix: String, epubIndex: Int): String { }.onFailure { AppLog.put("导出书名规则错误,使用默认规则\n${it.localizedMessage}", it) }.getOrDefault(default) +} + +fun tryParesExportFileName(jsStr: String): Boolean { + val bindings = SimpleBindings() + bindings["name"] = "name" + bindings["author"] = "author" + bindings["epubIndex"] = "epubIndex" + return runCatching { + RhinoScriptEngine.eval(jsStr, bindings) + true + }.getOrDefault(false) } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/config/AppConfig.kt b/app/src/main/java/io/legado/app/help/config/AppConfig.kt index 53b832e6d..ad3b596dd 100644 --- a/app/src/main/java/io/legado/app/help/config/AppConfig.kt +++ b/app/src/main/java/io/legado/app/help/config/AppConfig.kt @@ -31,26 +31,37 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener { PreferKey.themeMode -> isEInkMode = appCtx.getPrefString(PreferKey.themeMode) == "3" PreferKey.clickActionTL -> clickActionTL = appCtx.getPrefInt(PreferKey.clickActionTL, 2) + PreferKey.clickActionTC -> clickActionTC = appCtx.getPrefInt(PreferKey.clickActionTC, 2) + PreferKey.clickActionTR -> clickActionTR = appCtx.getPrefInt(PreferKey.clickActionTR, 1) + PreferKey.clickActionML -> clickActionML = appCtx.getPrefInt(PreferKey.clickActionML, 2) + PreferKey.clickActionMC -> clickActionMC = appCtx.getPrefInt(PreferKey.clickActionMC, 0) + PreferKey.clickActionMR -> clickActionMR = appCtx.getPrefInt(PreferKey.clickActionMR, 1) + PreferKey.clickActionBL -> clickActionBL = appCtx.getPrefInt(PreferKey.clickActionBL, 2) + PreferKey.clickActionBC -> clickActionBC = appCtx.getPrefInt(PreferKey.clickActionBC, 1) + PreferKey.clickActionBR -> clickActionBR = appCtx.getPrefInt(PreferKey.clickActionBR, 1) + PreferKey.readBodyToLh -> ReadBookConfig.readBodyToLh = appCtx.getPrefBoolean(PreferKey.readBodyToLh, true) + PreferKey.useZhLayout -> ReadBookConfig.useZhLayout = appCtx.getPrefBoolean(PreferKey.useZhLayout) + PreferKey.userAgent -> userAgent = getPrefUserAgent() } } @@ -132,6 +143,13 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener { appCtx.putPrefString(PreferKey.bookExportFileName, value) } + // 保存 自定义导出章节模式 文件名js表达式 + var episodeExportFileName: String? + get() = appCtx.getPrefString(PreferKey.episodeExportFileName, "") + set(value) { + appCtx.putPrefString(PreferKey.episodeExportFileName, value) + } + var bookImportFileName: String? get() = appCtx.getPrefString(PreferKey.bookImportFileName) set(value) { @@ -274,8 +292,10 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener { set(value) { appCtx.putPrefBoolean(PreferKey.exportNoChapterName, value) } + + // 是否启用自定义导出 default->false var enableCustomExport: Boolean - get() = appCtx.getPrefBoolean(PreferKey.enableCustomExport) + get() = appCtx.getPrefBoolean(PreferKey.enableCustomExport, false) set(value) { appCtx.putPrefBoolean(PreferKey.enableCustomExport, value) } diff --git a/app/src/main/java/io/legado/app/ui/book/cache/CacheActivity.kt b/app/src/main/java/io/legado/app/ui/book/cache/CacheActivity.kt index 5f67bbd3d..993c0f311 100644 --- a/app/src/main/java/io/legado/app/ui/book/cache/CacheActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/cache/CacheActivity.kt @@ -8,10 +8,12 @@ import android.view.View import androidx.activity.viewModels import androidx.appcompat.app.AlertDialog import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.textfield.TextInputLayout import io.legado.app.R import io.legado.app.base.VMBaseActivity import io.legado.app.constant.AppConst import io.legado.app.constant.AppConst.charsets +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 @@ -20,7 +22,9 @@ import io.legado.app.data.entities.BookGroup import io.legado.app.databinding.ActivityCacheBookBinding import io.legado.app.databinding.DialogEditTextBinding import io.legado.app.databinding.DialogSelectSectionExportBinding +import io.legado.app.help.book.getExportFileName import io.legado.app.help.book.isAudio +import io.legado.app.help.book.tryParesExportFileName import io.legado.app.help.config.AppConfig import io.legado.app.lib.dialogs.SelectItem import io.legado.app.lib.dialogs.alert @@ -55,17 +59,29 @@ class CacheActivity : VMBaseActivity() private var groupId: Long = -1 private val exportDir = registerForActivityResult(HandleFileContract()) { result -> + var isReadyPath = false + var dirPath = "" result.uri?.let { uri -> if (uri.isContentScheme()) { ACache.get().put(exportBookPathKey, uri.toString()) - startExport(uri.toString(), result.requestCode) + dirPath = uri.toString() + isReadyPath = true } else { uri.path?.let { path -> ACache.get().put(exportBookPathKey, path) - startExport(path, result.requestCode) + dirPath = path + isReadyPath = true } } } + if (!isReadyPath) { + return@registerForActivityResult + } + if (enableCustomExport()) {// 启用自定义导出 and 导出类型为Epub + configExportSection(dirPath, result.requestCode) + } else { + startExport(dirPath, result.requestCode) + } } override fun onActivityCreated(savedInstanceState: Bundle?) { @@ -249,7 +265,7 @@ class CacheActivity : VMBaseActivity() val path = ACache.get().getAsString(exportBookPathKey) if (path.isNullOrEmpty()) { selectExportFolder(position) - } else if (AppConfig.enableCustomExport && AppConfig.exportType == 1) {// 启用自定义导出 and 导出类型为Epub + } else if (enableCustomExport()) {// 启用自定义导出 and 导出类型为Epub configExportSection(path, position) } else { startExport(path, position) @@ -274,9 +290,50 @@ class CacheActivity : VMBaseActivity() * @since 1.0.0 */ private fun configExportSection(path: String, position: Int) { + + val alertBinding = DialogSelectSectionExportBinding.inflate(layoutInflater) .apply { + fun verifyExportFileNameJsStr(js: String): Boolean { + return tryParesExportFileName(js) && etEpubFilename.text.toString() + .isNotEmpty() + } + + fun enableLyEtEpubFilenameIcon() { + lyEtEpubFilename.endIconMode = TextInputLayout.END_ICON_CUSTOM + lyEtEpubFilename.setEndIconOnClickListener { + adapter.getItem(position)?.run { + lyEtEpubFilename.helperText = + if (verifyExportFileNameJsStr(etEpubFilename.text.toString())) + "${resources.getString(R.string.result_analyzed)}: ${ + getExportFileName( + "epub", + 1, + etEpubFilename.text.toString() + ) + }" + else "Error" + } ?: run { + lyEtEpubFilename.helperText = "Error" + AppLog.put("未找到书籍,position is $position") + } + } + } etEpubSize.setText("1") + // lyEtEpubFilename.endIconMode = TextInputLayout.END_ICON_NONE + etEpubFilename.text?.append(AppConfig.episodeExportFileName) + // 存储解析文件名的jsStr + etEpubFilename.let { + it.setOnFocusChangeListener { _, hasFocus -> + if (hasFocus) + return@setOnFocusChangeListener + it.text?.run { + if (verifyExportFileNameJsStr(toString())) { + AppConfig.episodeExportFileName = toString() + } + } + } + } tvAllExport.setOnClickListener { cbAllExport.callOnClick() } @@ -287,6 +344,8 @@ class CacheActivity : VMBaseActivity() if (isChecked) { etEpubSize.isEnabled = true etInputScope.isEnabled = true + etEpubFilename.isEnabled = true + enableLyEtEpubFilenameIcon() cbAllExport.isChecked = false } } @@ -294,6 +353,8 @@ class CacheActivity : VMBaseActivity() if (isChecked) { etEpubSize.isEnabled = false etInputScope.isEnabled = false + etEpubFilename.isEnabled = false + lyEtEpubFilename.endIconMode = TextInputLayout.END_ICON_NONE cbSelectExport.isChecked = false } } @@ -306,7 +367,9 @@ class CacheActivity : VMBaseActivity() etInputScope.hint = "" } } - cbAllExport.callOnClick() + + // 默认选择自定义导出 + cbSelectExport.callOnClick() } val alertDialog = alert(titleResource = R.string.select_section_export) { customView { alertBinding.root } @@ -336,17 +399,7 @@ class CacheActivity : VMBaseActivity() } } - /** - * 验证 输入的范围 是否正确 - * - * @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) { val default = arrayListOf>() @@ -397,11 +450,11 @@ class CacheActivity : VMBaseActivity() @SuppressLint("SetTextI18n") private fun alertExportFileName() { alert(R.string.export_file_name) { - var message = - "js内有name和author变量,返回书名\n启用自定义epub导出章节时包含额外变量[epubIndex]" - if (AppConfig.bookExportFileName.isNullOrBlank()) { - message += "\n例如:\nname+\"-\"+author+(epubIndex?\"(\"+epubIndex+\")\":\"\")" - } + val message = + "Variable: name, author." +// if (AppConfig.bookExportFileName.isNullOrBlank()) { +// message += "\n例如:\nname+\"-\"+author+(epubIndex?\"(\"+epubIndex+\")\":\"\")" +// } setMessage(message) val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { editView.hint = "file name js" diff --git a/app/src/main/java/io/legado/app/ui/book/cache/CacheViewModel.kt b/app/src/main/java/io/legado/app/ui/book/cache/CacheViewModel.kt index 7d4a26759..97c719c2f 100644 --- a/app/src/main/java/io/legado/app/ui/book/cache/CacheViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/cache/CacheViewModel.kt @@ -33,8 +33,10 @@ import kotlinx.coroutines.sync.withLock import me.ag2s.epublib.domain.* import me.ag2s.epublib.domain.Date import me.ag2s.epublib.epub.EpubWriter +import me.ag2s.epublib.epub.EpubWriterProcessor import me.ag2s.epublib.util.ResourceUtil import splitties.init.appCtx +import java.io.BufferedOutputStream import java.io.ByteArrayOutputStream import java.io.File import java.io.FileOutputStream @@ -42,7 +44,6 @@ import java.nio.charset.Charset import java.nio.file.* import java.util.* import java.util.concurrent.ConcurrentHashMap -import kotlin.collections.ArrayList import kotlin.coroutines.coroutineContext @@ -270,7 +271,7 @@ class CacheViewModel(application: Application) : BaseViewModel(application) { } val left = v[0].toInt() val right = v[1].toInt() - if (left > right){ + if (left > right) { AppLog.put("Error expression : $s; left > right") continue } @@ -354,7 +355,7 @@ class CacheViewModel(application: Application) : BaseViewModel(application) { setEpubContent(contentModel, book, epubBook) DocumentUtils.createFileIfNotExist(doc, filename)?.let { bookDoc -> context.contentResolver.openOutputStream(bookDoc.uri, "wa")?.use { bookOs -> - EpubWriter().write(epubBook, bookOs) + EpubWriter().write(epubBook, BufferedOutputStream(bookOs)) } if (AppConfig.exportToWebDav) { // 导出到webdav @@ -380,7 +381,7 @@ class CacheViewModel(application: Application) : BaseViewModel(application) { //设置正文 setEpubContent(contentModel, book, epubBook) @Suppress("BlockingMethodInNonBlockingContext") - EpubWriter().write(epubBook, FileOutputStream(bookFile)) + EpubWriter().write(epubBook, BufferedOutputStream(FileOutputStream(bookFile))) if (AppConfig.exportToWebDav) { // 导出到webdav AppWebDav.exportWebDav(Uri.fromFile(bookFile), filename) @@ -636,6 +637,10 @@ class CacheViewModel(application: Application) : BaseViewModel(application) { */ class CustomExporter(private val context: CacheViewModel) { var scope: IntArray = IntArray(0) + + /** + * epub 文件包含最大章节数 + */ var size: Int = 1 /** @@ -659,14 +664,75 @@ class CacheViewModel(application: Application) : BaseViewModel(application) { } 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) + val currentTimeMillis = System.currentTimeMillis() + when (path.isContentScheme()) { + true -> { + val uri = Uri.parse(path) + val doc = DocumentFile.fromTreeUri(context.context, uri) + ?: throw NoStackTraceException("获取导出文档失败") + val (contentModel, epubList) = createEpubs(book, doc) + val asyncBlocks = ArrayList>(epubList.size) + var progressBar = 0.0 + epubList.forEachIndexed { index, ep -> + val (filename, epubBook) = ep + val asyncBlock = async { + //设置正文 + setEpubContent( + contentModel, + book, + epubBook, + index + ) { _, _ -> + // 将章节写入内存时更新进度条 + context.upAdapterLiveData.postValue(book.bookUrl) + progressBar += book.totalChapterNum.toDouble() / scope.size / 2 + context.exportProgress[book.bookUrl] = progressBar.toInt() + } + save2Drive(filename, epubBook, doc) { total, progress -> + //写入硬盘时更新进度条 + progressBar += book.totalChapterNum.toDouble() / epubList.size / total / 2 + context.upAdapterLiveData.postValue(book.bookUrl) + context.exportProgress[book.bookUrl] = progressBar.toInt() + } + } + asyncBlocks.add(asyncBlock) + } + asyncBlocks.forEach { it.await() } + } + + false -> { + val file = File(path).createFolderIfNotExist() + val (contentModel, epubList) = createEpubs(book, null) + val asyncBlocks = ArrayList>(epubList.size) + var progressBar = 0.0 + epubList.forEachIndexed { index, ep -> + val (filename, epubBook) = ep + val asyncBlock = async { + //设置正文 + setEpubContent( + contentModel, + book, + epubBook, + index + ) { _, _ -> + context.upAdapterLiveData.postValue(book.bookUrl) + context.exportProgress[book.bookUrl] = + context.exportProgress[book.bookUrl]?.plus(book.totalChapterNum / scope.size) + ?: 1 + } + save2Drive(filename, epubBook, file) { total, progress -> + //设置进度 + progressBar += book.totalChapterNum.toDouble() / epubList.size / total / 2 + context.upAdapterLiveData.postValue(book.bookUrl) + context.exportProgress[book.bookUrl] = progressBar.toInt() + } + } + asyncBlocks.add(asyncBlock) + } + asyncBlocks.forEach { it.await() } + } } + AppLog.put("分割导出书籍 ${book.name} 一共耗时 ${System.currentTimeMillis() - currentTimeMillis}") }.onError { context.exportProgress.remove(book.bookUrl) context.exportMsg[book.bookUrl] = it.localizedMessage ?: "ERROR" @@ -682,38 +748,6 @@ class CacheViewModel(application: Application) : BaseViewModel(application) { } } - /** - * 导出 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正文 @@ -729,7 +763,8 @@ class CacheViewModel(application: Application) : BaseViewModel(application) { contentModel: String, book: Book, epubBook: EpubBook, - epubBookIndex: Int + epubBookIndex: Int, + updateProgress: (chapterList: MutableList, index: Int) -> Unit ) { //正文 val useReplace = AppConfig.exportUseReplace && book.getUseReplaceRule() @@ -743,7 +778,7 @@ class CacheViewModel(application: Application) : BaseViewModel(application) { return@forEachIndexed } } - val totalChapterNum = book.totalChapterNum / scope.size + // val totalChapterNum = book.totalChapterNum / scope.size if (chapterList.size == 0) { throw RuntimeException("书籍<${book.name}>(${epubBookIndex + 1})未找到章节信息") } @@ -753,9 +788,7 @@ class CacheViewModel(application: Application) : BaseViewModel(application) { ) chapterList.forEachIndexed { index, chapter -> coroutineContext.ensureActive() - context.upAdapterLiveData.postValue(book.bookUrl) - context.exportProgress[book.bookUrl] = - totalChapterNum * (epubBookIndex * size + index) + updateProgress(chapterList, index) BookHelp.getContent(book, chapter).let { content -> var content1 = context.fixPic( epubBook, @@ -798,22 +831,23 @@ class CacheViewModel(application: Application) : BaseViewModel(application) { * 创建多个epub 对象 * * 分割epub时,一个书籍需要创建多个epub对象 - * * @param doc 导出文档 * @param book 书籍 * * @return <内容模板字符串, > */ private fun createEpubs( - doc: DocumentFile, - book: Book + book: Book, + doc: DocumentFile?, ): Pair>> { val paresNumOfEpub = paresNumOfEpub(scope.size, size) val result: MutableList> = ArrayList(paresNumOfEpub) var contentModel = "" for (i in 1..paresNumOfEpub) { val filename = book.getExportFileName("epub", i) - DocumentUtils.delete(doc, filename) + doc?.let { + DocumentUtils.delete(it, filename) + } val epubBook = EpubBook() epubBook.version = "2.0" //set metadata @@ -821,39 +855,9 @@ class CacheViewModel(application: Application) : BaseViewModel(application) { //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 <内容模板字符串, > - */ - private fun createEpubs( - book: Book - ): Pair>> { - val paresNumOfEpub = paresNumOfEpub(scope.size, size) - val result: MutableList> = 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) + contentModel = doc?.let { + context.setAssets(it, book, epubBook) + } ?: context.setAssets(book, epubBook) // add epubBook result.add(Pair(filename, epubBook)) @@ -864,10 +868,21 @@ class CacheViewModel(application: Application) : BaseViewModel(application) { /** * 保存文件到 设备 */ - private suspend fun save2Drive(filename: String, epubBook: EpubBook, doc: DocumentFile) { + private suspend fun save2Drive( + filename: String, + epubBook: EpubBook, + doc: DocumentFile, + callback: (total: Int, progress: Int) -> Unit + ) { DocumentUtils.createFileIfNotExist(doc, filename)?.let { bookDoc -> context.context.contentResolver.openOutputStream(bookDoc.uri, "wa")?.use { bookOs -> - EpubWriter().write(epubBook, bookOs) + EpubWriter() + .setCallback(object : EpubWriterProcessor.Callback { + override fun onProgressing(total: Int, progress: Int) { + callback(total, progress) + } + }) + .write(epubBook, BufferedOutputStream(bookOs)) } if (AppConfig.exportToWebDav) { // 导出到webdav @@ -879,11 +894,22 @@ class CacheViewModel(application: Application) : BaseViewModel(application) { /** * 保存文件到 设备 */ - private suspend fun save2Drive(filename: String, epubBook: EpubBook, file: File) { + private suspend fun save2Drive( + filename: String, + epubBook: EpubBook, + file: File, + callback: (total: Int, progress: Int) -> Unit + ) { val bookPath = FileUtils.getPath(file, filename) val bookFile = FileUtils.createFileWithReplace(bookPath) @Suppress("BlockingMethodInNonBlockingContext") - EpubWriter().write(epubBook, FileOutputStream(bookFile)) + EpubWriter() + .setCallback(object : EpubWriterProcessor.Callback { + override fun onProgressing(total: Int, progress: Int) { + callback(total, progress) + } + }) + .write(epubBook, BufferedOutputStream(FileOutputStream(bookFile))) if (AppConfig.exportToWebDav) { // 导出到webdav AppWebDav.exportWebDav(Uri.fromFile(bookFile), filename) diff --git a/app/src/main/java/io/legado/app/utils/CustomExportUtils.kt b/app/src/main/java/io/legado/app/utils/CustomExportUtils.kt new file mode 100644 index 000000000..80fdf2c70 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/CustomExportUtils.kt @@ -0,0 +1,27 @@ +package io.legado.app.utils + +import io.legado.app.help.config.AppConfig + +// 匹配待“输入的章节”字符串 +private val regexEpisode = Regex("\\d+(-\\d+)?(,\\d+(-\\d+)?)*") + +/** + * 是否启用自定义导出 + * + * @author Discut + */ +fun enableCustomExport(): Boolean { + return AppConfig.enableCustomExport && AppConfig.exportType == 1 +} + +/** + * 验证 输入的范围 是否正确 + * + * @since 1.0.0 + * @author Discut + * @param text 输入的范围 字符串 + * @return 是否正确 + */ +fun verificationField(text: String): Boolean { + return text.matches(regexEpisode) +} \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_select_section_export.xml b/app/src/main/res/layout/dialog_select_section_export.xml index 6bff4baff..da96e9c91 100644 --- a/app/src/main/res/layout/dialog_select_section_export.xml +++ b/app/src/main/res/layout/dialog_select_section_export.xml @@ -1,5 +1,6 @@ + + + + + + + diff --git a/app/src/main/res/values-es-rES/strings.xml b/app/src/main/res/values-es-rES/strings.xml index f6be8e452..d0a95e78a 100644 --- a/app/src/main/res/values-es-rES/strings.xml +++ b/app/src/main/res/values-es-rES/strings.xml @@ -1119,4 +1119,5 @@ 退出软件 全部加入书架 页至 + Analyzed diff --git a/app/src/main/res/values-ja-rJP/strings.xml b/app/src/main/res/values-ja-rJP/strings.xml index cdca05d5b..ec34bd86d 100644 --- a/app/src/main/res/values-ja-rJP/strings.xml +++ b/app/src/main/res/values-ja-rJP/strings.xml @@ -1122,4 +1122,5 @@ 退出软件 全部加入书架 页至 + Analyzed diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 0dfc2edfe..7e49cbf65 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -1122,4 +1122,5 @@ 退出软件 全部加入书架 页至 + Analyzed diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index c6079296b..23fa21308 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -1118,4 +1118,5 @@ Còn Thoát khỏi phần mềm Thêm tất cả vào giá sách Trang tới + Analyzed \ No newline at end of file diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index 0a89e309e..724082fa0 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -1119,4 +1119,5 @@ 退出软件 全部加入书架 页至 + 解析示例 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index aa1381da1..bc56b57d7 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -1121,4 +1121,5 @@ 退出软件 全部加入书架 页至 + 解析示例 diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 00fd59ddf..1c6619b07 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -1121,4 +1121,5 @@ 退出软件 全部加入书架 页至 + 解析示例 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ceb4df3c0..97800492b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1122,4 +1122,5 @@ 退出软件 全部加入书架 页至 + Analyzed diff --git a/modules/book/src/main/java/me/ag2s/epublib/epub/EpubWriter.java b/modules/book/src/main/java/me/ag2s/epublib/epub/EpubWriter.java index 6c4fa1a8a..9c0782075 100644 --- a/modules/book/src/main/java/me/ag2s/epublib/epub/EpubWriter.java +++ b/modules/book/src/main/java/me/ag2s/epublib/epub/EpubWriter.java @@ -9,6 +9,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Writer; +import java.util.Objects; import java.util.zip.CRC32; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; @@ -25,15 +26,20 @@ import me.ag2s.epublib.util.IOUtil; */ public class EpubWriter { - private static final String TAG = EpubWriter.class.getName(); - // package static final String EMPTY_NAMESPACE_PREFIX = ""; - + private static final String TAG = EpubWriter.class.getName(); private BookProcessor bookProcessor; + private EpubWriterProcessor epubWriterProcessor; + public EpubWriter() { this(BookProcessor.IDENTITY_BOOKPROCESSOR); + this.epubWriterProcessor = new EpubWriterProcessor(); + // 写入MimeType、Container,初始化TOCResource整体为1 + // 写入PackageDocument 为1 + // 关闭流 为1 + this.epubWriterProcessor.setTotalProgress(3); } @@ -41,16 +47,31 @@ public class EpubWriter { this.bookProcessor = bookProcessor; } + public EpubWriter setCallback(EpubWriterProcessor.Callback callback) { + epubWriterProcessor.setCallback(callback); + return this; + } public void write(EpubBook book, OutputStream out) throws IOException { + if (Objects.nonNull(this.epubWriterProcessor.getCallback())) { + epubWriterProcessor.getCallback().onStart(book); + } + epubWriterProcessor.setTotalProgress(epubWriterProcessor.getTotalProgress() + book.getResources().size()); book = processBook(book); ZipOutputStream resultStream = new ZipOutputStream(out); + resultStream.setLevel(ZipOutputStream.STORED); writeMimeType(resultStream); writeContainer(resultStream); initTOCResource(book); + epubWriterProcessor.updateCurrentProgress(1); writeResources(book, resultStream); writePackageDocument(book, resultStream); + epubWriterProcessor.updateCurrentProgress(epubWriterProcessor.getCurrentProgress() + 1); resultStream.close(); + epubWriterProcessor.updateCurrentProgress(epubWriterProcessor.getCurrentProgress() + 1); + if (Objects.nonNull(epubWriterProcessor.getCallback())) { + epubWriterProcessor.getCallback().onEnd(book); + } } private EpubBook processBook(EpubBook book) { @@ -76,9 +97,7 @@ public class EpubWriter { book.getSpine().setTocResource(tocResource); book.getResources().add(tocResource); } catch (Exception ex) { - Log.e(TAG, - "Error writing table of contents: " - + ex.getClass().getName() + ": " + ex.getMessage(), ex); + Log.e(TAG, "Error writing table of contents: " + ex.getClass().getName() + ": " + ex.getMessage(), ex); } } @@ -86,6 +105,7 @@ public class EpubWriter { private void writeResources(EpubBook book, ZipOutputStream resultStream) { for (Resource resource : book.getResources().getAll()) { writeResource(resource, resultStream); + epubWriterProcessor.updateCurrentProgress(epubWriterProcessor.getCurrentProgress() + 1); } } @@ -111,11 +131,9 @@ public class EpubWriter { } - private void writePackageDocument(EpubBook book, ZipOutputStream resultStream) - throws IOException { + private void writePackageDocument(EpubBook book, ZipOutputStream resultStream) throws IOException { resultStream.putNextEntry(new ZipEntry("OEBPS/content.opf")); - XmlSerializer xmlSerializer = EpubProcessorSupport - .createXmlSerializer(resultStream); + XmlSerializer xmlSerializer = EpubProcessorSupport.createXmlSerializer(resultStream); PackageDocumentWriter.write(this, xmlSerializer, book); xmlSerializer.flush(); // String resultAsString = result.toString(); @@ -132,11 +150,9 @@ public class EpubWriter { resultStream.putNextEntry(new ZipEntry("META-INF/container.xml")); Writer out = new OutputStreamWriter(resultStream); out.write("\n"); - out.write( - "\n"); + out.write("\n"); out.write("\t\n"); - out.write( - "\t\t\n"); + out.write("\t\t\n"); out.write("\t\n"); out.write(""); out.flush(); diff --git a/modules/book/src/main/java/me/ag2s/epublib/epub/EpubWriterProcessor.java b/modules/book/src/main/java/me/ag2s/epublib/epub/EpubWriterProcessor.java new file mode 100644 index 000000000..85a1aaf7b --- /dev/null +++ b/modules/book/src/main/java/me/ag2s/epublib/epub/EpubWriterProcessor.java @@ -0,0 +1,57 @@ +package me.ag2s.epublib.epub; + +import java.util.Objects; + +import me.ag2s.epublib.domain.EpubBook; + +/** + * epub导出进度管理器 + * + * @author Discut + */ +public class EpubWriterProcessor { + private int totalProgress = 0; + private int currentProgress = 0; + private Callback callback; + + public int getCurrentProgress() { + return currentProgress; + } + + public int getTotalProgress() { + return totalProgress; + } + + public void setTotalProgress(int totalProgress) { + this.totalProgress = totalProgress; + } + + public void setCallback(Callback callback) { + this.callback = callback; + } + + protected void updateCurrentProgress(int current) { + this.currentProgress = Math.min(current, totalProgress); + if (Objects.isNull(callback)) { + return; + } + callback.onProgressing(totalProgress, this.currentProgress); + } + + protected Callback getCallback() { + return callback; + } + + @SuppressWarnings("unused") + public interface Callback { + default void onStart(EpubBook epubBook) { + } + + default void onProgressing(int total, int progress) { + } + + default void onEnd(EpubBook epubBook) { + } + + } +}