Fix CustomExport (#2)

* perf(CacheActivity):  提升性能

* feat(strings): 💄 新增 result_analyzed 字段

* feat(PreferKey):  新增 episodeExportFileName 字段

+ 用于保存 自定义导出文件名 的js脚本

* feat(ExportCustomEpisode):  将导出文件名格式输入框移至导出对话框

+ 新增 测试导出文件名格式按钮
+ 默认选择为 导出所有
+ 优化了保存变量逻辑

* fix(ui): cant show R.string.result_analyzed

* perf(ExportCustomEpisode):  使用协程优化导出

* perf(ExportCustomEpisode): 优化log

* fix(AppConfig): 修复episodeExportFileName值可能为null的问题

* fix(AppConfig): 修复enableCustomExport值可能为null的问题

* fix(AppConfig): 修复第一次选择导出路径不能正常使用自定义导出的问题

* perf(ExportCustomEpisode):  优化进度条显示、优化输出速度

* style(ExportCustomEpisode): 🎨 清理代码
This commit is contained in:
Discut 2023-07-15 17:43:25 +08:00 committed by GitHub
parent b29f65bac1
commit 4d6864051d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 374 additions and 127 deletions

View File

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

View File

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

View File

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

View File

@ -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<ActivityCacheBookBinding, CacheViewModel>()
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<ActivityCacheBookBinding, CacheViewModel>()
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<ActivityCacheBookBinding, CacheViewModel>()
* @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<ActivityCacheBookBinding, CacheViewModel>()
if (isChecked) {
etEpubSize.isEnabled = true
etInputScope.isEnabled = true
etEpubFilename.isEnabled = true
enableLyEtEpubFilenameIcon()
cbAllExport.isChecked = false
}
}
@ -294,6 +353,8 @@ class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>()
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<ActivityCacheBookBinding, CacheViewModel>()
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<ActivityCacheBookBinding, CacheViewModel>()
}
}
/**
* 验证 输入的范围 是否正确
*
* @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<SelectItem<Int>>()
@ -397,11 +450,11 @@ class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>()
@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"

View File

@ -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<Deferred<Unit>>(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<Deferred<Unit>>(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<BookChapter>, 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 <内容模板字符串, <epub文件名, epub对象>>
*/
private fun createEpubs(
doc: DocumentFile,
book: Book
book: Book,
doc: DocumentFile?,
): 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)
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 <内容模板字符串, <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)
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)

View File

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

View File

@ -1,5 +1,6 @@
<?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"
@ -54,6 +55,29 @@
</LinearLayout>
<io.legado.app.ui.widget.text.TextInputLayout
android:id="@+id/ly_et_epub_filename"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:hint="@string/export_file_name"
app:endIconCheckable="true"
app:endIconContentDescription="Execute script"
app:endIconDrawable="@drawable/ic_play_24dp"
app:endIconMode="custom"
app:helperText="Variable: name, author, epubIndex">
<io.legado.app.lib.theme.view.ThemeEditText
android:id="@+id/et_epub_filename"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text"
android:enabled="false"
tools:ignore="SpeakableTextPresentCheck,TouchTargetSizeCheck" />
</io.legado.app.ui.widget.text.TextInputLayout>
<io.legado.app.ui.widget.text.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -64,8 +88,8 @@
android:id="@+id/et_epub_size"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxLength="6"
android:inputType="number"
android:maxLength="6"
tools:ignore="SpeakableTextPresentCheck,TouchTargetSizeCheck" />
</io.legado.app.ui.widget.text.TextInputLayout>

View File

@ -1119,4 +1119,5 @@
<string name="exit_app">退出软件</string>
<string name="add_all_to_bookshelf">全部加入书架</string>
<string name="page_to">页至</string>
<string name="result_analyzed">Analyzed</string>
</resources>

View File

@ -1122,4 +1122,5 @@
<string name="exit_app">退出软件</string>
<string name="add_all_to_bookshelf">全部加入书架</string>
<string name="page_to">页至</string>
<string name="result_analyzed">Analyzed</string>
</resources>

View File

@ -1122,4 +1122,5 @@
<string name="exit_app">退出软件</string>
<string name="add_all_to_bookshelf">全部加入书架</string>
<string name="page_to">页至</string>
<string name="result_analyzed">Analyzed</string>
</resources>

View File

@ -1118,4 +1118,5 @@ Còn </string>
<string name="exit_app">Thoát khỏi phần mềm</string>
<string name="add_all_to_bookshelf">Thêm tất cả vào giá sách</string>
<string name="page_to">Trang tới</string>
<string name="result_analyzed">Analyzed</string>
</resources>

View File

@ -1119,4 +1119,5 @@
<string name="exit_app">退出软件</string>
<string name="add_all_to_bookshelf">全部加入书架</string>
<string name="page_to">页至</string>
<string name="result_analyzed">解析示例</string>
</resources>

View File

@ -1121,4 +1121,5 @@
<string name="exit_app">退出软件</string>
<string name="add_all_to_bookshelf">全部加入书架</string>
<string name="page_to">页至</string>
<string name="result_analyzed">解析示例</string>
</resources>

View File

@ -1121,4 +1121,5 @@
<string name="exit_app">退出软件</string>
<string name="add_all_to_bookshelf">全部加入书架</string>
<string name="page_to">页至</string>
<string name="result_analyzed">解析示例</string>
</resources>

View File

@ -1122,4 +1122,5 @@
<string name="exit_app">退出软件</string>
<string name="add_all_to_bookshelf">全部加入书架</string>
<string name="page_to">页至</string>
<string name="result_analyzed">Analyzed</string>
</resources>

View File

@ -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();
// 写入MimeTypeContainer初始化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("<?xml version=\"1.0\"?>\n");
out.write(
"<container version=\"1.0\" xmlns=\"urn:oasis:names:tc:opendocument:xmlns:container\">\n");
out.write("<container version=\"1.0\" xmlns=\"urn:oasis:names:tc:opendocument:xmlns:container\">\n");
out.write("\t<rootfiles>\n");
out.write(
"\t\t<rootfile full-path=\"OEBPS/content.opf\" media-type=\"application/oebps-package+xml\"/>\n");
out.write("\t\t<rootfile full-path=\"OEBPS/content.opf\" media-type=\"application/oebps-package+xml\"/>\n");
out.write("\t</rootfiles>\n");
out.write("</container>");
out.flush();

View File

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