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:
@@ -39,6 +39,7 @@ object PreferKey {
|
|||||||
const val bookshelfSort = "bookshelfSort"
|
const val bookshelfSort = "bookshelfSort"
|
||||||
const val bookExportFileName = "bookExportFileName"
|
const val bookExportFileName = "bookExportFileName"
|
||||||
const val bookImportFileName = "bookImportFileName"
|
const val bookImportFileName = "bookImportFileName"
|
||||||
|
const val episodeExportFileName = "episodeExportFileName"
|
||||||
const val recordLog = "recordLog"
|
const val recordLog = "recordLog"
|
||||||
const val processText = "process_text"
|
const val processText = "process_text"
|
||||||
const val cleanCache = "cleanCache"
|
const val cleanCache = "cleanCache"
|
||||||
|
|||||||
@@ -236,6 +236,7 @@ fun Book.getExportFileName(suffix: String): String {
|
|||||||
return "$name 作者:${getRealAuthor()}.$suffix"
|
return "$name 作者:${getRealAuthor()}.$suffix"
|
||||||
}
|
}
|
||||||
val bindings = SimpleBindings()
|
val bindings = SimpleBindings()
|
||||||
|
bindings["epubIndex"] = ""// 兼容老版本,修复可能存在的错误
|
||||||
bindings["name"] = name
|
bindings["name"] = name
|
||||||
bindings["author"] = getRealAuthor()
|
bindings["author"] = getRealAuthor()
|
||||||
return kotlin.runCatching {
|
return kotlin.runCatching {
|
||||||
@@ -248,8 +249,11 @@ fun Book.getExportFileName(suffix: String): String {
|
|||||||
/**
|
/**
|
||||||
* 获取分割文件后的文件名
|
* 获取分割文件后的文件名
|
||||||
*/
|
*/
|
||||||
fun Book.getExportFileName(suffix: String, epubIndex: Int): String {
|
fun Book.getExportFileName(
|
||||||
val jsStr = AppConfig.bookExportFileName
|
suffix: String,
|
||||||
|
epubIndex: Int,
|
||||||
|
jsStr: String? = AppConfig.episodeExportFileName
|
||||||
|
): String {
|
||||||
// 默认规则
|
// 默认规则
|
||||||
val default = "$name 作者:${getRealAuthor()} [${epubIndex}].$suffix"
|
val default = "$name 作者:${getRealAuthor()} [${epubIndex}].$suffix"
|
||||||
if (jsStr.isNullOrBlank()) {
|
if (jsStr.isNullOrBlank()) {
|
||||||
@@ -265,3 +269,14 @@ fun Book.getExportFileName(suffix: String, epubIndex: Int): String {
|
|||||||
AppLog.put("导出书名规则错误,使用默认规则\n${it.localizedMessage}", it)
|
AppLog.put("导出书名规则错误,使用默认规则\n${it.localizedMessage}", it)
|
||||||
}.getOrDefault(default)
|
}.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)
|
||||||
|
}
|
||||||
@@ -31,26 +31,37 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
|
|||||||
PreferKey.themeMode -> isEInkMode = appCtx.getPrefString(PreferKey.themeMode) == "3"
|
PreferKey.themeMode -> isEInkMode = appCtx.getPrefString(PreferKey.themeMode) == "3"
|
||||||
PreferKey.clickActionTL -> clickActionTL =
|
PreferKey.clickActionTL -> clickActionTL =
|
||||||
appCtx.getPrefInt(PreferKey.clickActionTL, 2)
|
appCtx.getPrefInt(PreferKey.clickActionTL, 2)
|
||||||
|
|
||||||
PreferKey.clickActionTC -> clickActionTC =
|
PreferKey.clickActionTC -> clickActionTC =
|
||||||
appCtx.getPrefInt(PreferKey.clickActionTC, 2)
|
appCtx.getPrefInt(PreferKey.clickActionTC, 2)
|
||||||
|
|
||||||
PreferKey.clickActionTR -> clickActionTR =
|
PreferKey.clickActionTR -> clickActionTR =
|
||||||
appCtx.getPrefInt(PreferKey.clickActionTR, 1)
|
appCtx.getPrefInt(PreferKey.clickActionTR, 1)
|
||||||
|
|
||||||
PreferKey.clickActionML -> clickActionML =
|
PreferKey.clickActionML -> clickActionML =
|
||||||
appCtx.getPrefInt(PreferKey.clickActionML, 2)
|
appCtx.getPrefInt(PreferKey.clickActionML, 2)
|
||||||
|
|
||||||
PreferKey.clickActionMC -> clickActionMC =
|
PreferKey.clickActionMC -> clickActionMC =
|
||||||
appCtx.getPrefInt(PreferKey.clickActionMC, 0)
|
appCtx.getPrefInt(PreferKey.clickActionMC, 0)
|
||||||
|
|
||||||
PreferKey.clickActionMR -> clickActionMR =
|
PreferKey.clickActionMR -> clickActionMR =
|
||||||
appCtx.getPrefInt(PreferKey.clickActionMR, 1)
|
appCtx.getPrefInt(PreferKey.clickActionMR, 1)
|
||||||
|
|
||||||
PreferKey.clickActionBL -> clickActionBL =
|
PreferKey.clickActionBL -> clickActionBL =
|
||||||
appCtx.getPrefInt(PreferKey.clickActionBL, 2)
|
appCtx.getPrefInt(PreferKey.clickActionBL, 2)
|
||||||
|
|
||||||
PreferKey.clickActionBC -> clickActionBC =
|
PreferKey.clickActionBC -> clickActionBC =
|
||||||
appCtx.getPrefInt(PreferKey.clickActionBC, 1)
|
appCtx.getPrefInt(PreferKey.clickActionBC, 1)
|
||||||
|
|
||||||
PreferKey.clickActionBR -> clickActionBR =
|
PreferKey.clickActionBR -> clickActionBR =
|
||||||
appCtx.getPrefInt(PreferKey.clickActionBR, 1)
|
appCtx.getPrefInt(PreferKey.clickActionBR, 1)
|
||||||
|
|
||||||
PreferKey.readBodyToLh -> ReadBookConfig.readBodyToLh =
|
PreferKey.readBodyToLh -> ReadBookConfig.readBodyToLh =
|
||||||
appCtx.getPrefBoolean(PreferKey.readBodyToLh, true)
|
appCtx.getPrefBoolean(PreferKey.readBodyToLh, true)
|
||||||
|
|
||||||
PreferKey.useZhLayout -> ReadBookConfig.useZhLayout =
|
PreferKey.useZhLayout -> ReadBookConfig.useZhLayout =
|
||||||
appCtx.getPrefBoolean(PreferKey.useZhLayout)
|
appCtx.getPrefBoolean(PreferKey.useZhLayout)
|
||||||
|
|
||||||
PreferKey.userAgent -> userAgent = getPrefUserAgent()
|
PreferKey.userAgent -> userAgent = getPrefUserAgent()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -132,6 +143,13 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
|
|||||||
appCtx.putPrefString(PreferKey.bookExportFileName, value)
|
appCtx.putPrefString(PreferKey.bookExportFileName, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 保存 自定义导出章节模式 文件名js表达式
|
||||||
|
var episodeExportFileName: String?
|
||||||
|
get() = appCtx.getPrefString(PreferKey.episodeExportFileName, "")
|
||||||
|
set(value) {
|
||||||
|
appCtx.putPrefString(PreferKey.episodeExportFileName, value)
|
||||||
|
}
|
||||||
|
|
||||||
var bookImportFileName: String?
|
var bookImportFileName: String?
|
||||||
get() = appCtx.getPrefString(PreferKey.bookImportFileName)
|
get() = appCtx.getPrefString(PreferKey.bookImportFileName)
|
||||||
set(value) {
|
set(value) {
|
||||||
@@ -274,8 +292,10 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
|
|||||||
set(value) {
|
set(value) {
|
||||||
appCtx.putPrefBoolean(PreferKey.exportNoChapterName, value)
|
appCtx.putPrefBoolean(PreferKey.exportNoChapterName, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 是否启用自定义导出 default->false
|
||||||
var enableCustomExport: Boolean
|
var enableCustomExport: Boolean
|
||||||
get() = appCtx.getPrefBoolean(PreferKey.enableCustomExport)
|
get() = appCtx.getPrefBoolean(PreferKey.enableCustomExport, false)
|
||||||
set(value) {
|
set(value) {
|
||||||
appCtx.putPrefBoolean(PreferKey.enableCustomExport, value)
|
appCtx.putPrefBoolean(PreferKey.enableCustomExport, value)
|
||||||
}
|
}
|
||||||
|
|||||||
+73
-20
@@ -8,10 +8,12 @@ import android.view.View
|
|||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
import io.legado.app.base.VMBaseActivity
|
import io.legado.app.base.VMBaseActivity
|
||||||
import io.legado.app.constant.AppConst
|
import io.legado.app.constant.AppConst
|
||||||
import io.legado.app.constant.AppConst.charsets
|
import io.legado.app.constant.AppConst.charsets
|
||||||
|
import io.legado.app.constant.AppLog
|
||||||
import io.legado.app.constant.EventBus
|
import io.legado.app.constant.EventBus
|
||||||
import io.legado.app.data.appDb
|
import io.legado.app.data.appDb
|
||||||
import io.legado.app.data.entities.Book
|
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.ActivityCacheBookBinding
|
||||||
import io.legado.app.databinding.DialogEditTextBinding
|
import io.legado.app.databinding.DialogEditTextBinding
|
||||||
import io.legado.app.databinding.DialogSelectSectionExportBinding
|
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.isAudio
|
||||||
|
import io.legado.app.help.book.tryParesExportFileName
|
||||||
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
|
||||||
import io.legado.app.lib.dialogs.alert
|
import io.legado.app.lib.dialogs.alert
|
||||||
@@ -55,17 +59,29 @@ class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>()
|
|||||||
private var groupId: Long = -1
|
private var groupId: Long = -1
|
||||||
|
|
||||||
private val exportDir = registerForActivityResult(HandleFileContract()) { result ->
|
private val exportDir = registerForActivityResult(HandleFileContract()) { result ->
|
||||||
|
var isReadyPath = false
|
||||||
|
var dirPath = ""
|
||||||
result.uri?.let { uri ->
|
result.uri?.let { uri ->
|
||||||
if (uri.isContentScheme()) {
|
if (uri.isContentScheme()) {
|
||||||
ACache.get().put(exportBookPathKey, uri.toString())
|
ACache.get().put(exportBookPathKey, uri.toString())
|
||||||
startExport(uri.toString(), result.requestCode)
|
dirPath = uri.toString()
|
||||||
|
isReadyPath = true
|
||||||
} else {
|
} else {
|
||||||
uri.path?.let { path ->
|
uri.path?.let { path ->
|
||||||
ACache.get().put(exportBookPathKey, 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?) {
|
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||||
@@ -249,7 +265,7 @@ 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
|
} else if (enableCustomExport()) {// 启用自定义导出 and 导出类型为Epub
|
||||||
configExportSection(path, position)
|
configExportSection(path, position)
|
||||||
} else {
|
} else {
|
||||||
startExport(path, position)
|
startExport(path, position)
|
||||||
@@ -274,9 +290,50 @@ class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>()
|
|||||||
* @since 1.0.0
|
* @since 1.0.0
|
||||||
*/
|
*/
|
||||||
private fun configExportSection(path: String, position: Int) {
|
private fun configExportSection(path: String, position: Int) {
|
||||||
|
|
||||||
|
|
||||||
val alertBinding = DialogSelectSectionExportBinding.inflate(layoutInflater)
|
val alertBinding = DialogSelectSectionExportBinding.inflate(layoutInflater)
|
||||||
.apply {
|
.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")
|
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 {
|
tvAllExport.setOnClickListener {
|
||||||
cbAllExport.callOnClick()
|
cbAllExport.callOnClick()
|
||||||
}
|
}
|
||||||
@@ -287,6 +344,8 @@ class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>()
|
|||||||
if (isChecked) {
|
if (isChecked) {
|
||||||
etEpubSize.isEnabled = true
|
etEpubSize.isEnabled = true
|
||||||
etInputScope.isEnabled = true
|
etInputScope.isEnabled = true
|
||||||
|
etEpubFilename.isEnabled = true
|
||||||
|
enableLyEtEpubFilenameIcon()
|
||||||
cbAllExport.isChecked = false
|
cbAllExport.isChecked = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -294,6 +353,8 @@ class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>()
|
|||||||
if (isChecked) {
|
if (isChecked) {
|
||||||
etEpubSize.isEnabled = false
|
etEpubSize.isEnabled = false
|
||||||
etInputScope.isEnabled = false
|
etInputScope.isEnabled = false
|
||||||
|
etEpubFilename.isEnabled = false
|
||||||
|
lyEtEpubFilename.endIconMode = TextInputLayout.END_ICON_NONE
|
||||||
cbSelectExport.isChecked = false
|
cbSelectExport.isChecked = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -306,7 +367,9 @@ class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>()
|
|||||||
etInputScope.hint = ""
|
etInputScope.hint = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cbAllExport.callOnClick()
|
|
||||||
|
// 默认选择自定义导出
|
||||||
|
cbSelectExport.callOnClick()
|
||||||
}
|
}
|
||||||
val alertDialog = alert(titleResource = R.string.select_section_export) {
|
val alertDialog = alert(titleResource = R.string.select_section_export) {
|
||||||
customView { alertBinding.root }
|
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) {
|
private fun selectExportFolder(exportPosition: Int) {
|
||||||
val default = arrayListOf<SelectItem<Int>>()
|
val default = arrayListOf<SelectItem<Int>>()
|
||||||
@@ -397,11 +450,11 @@ 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) {
|
||||||
var message =
|
val message =
|
||||||
"js内有name和author变量,返回书名\n启用自定义epub导出章节时包含额外变量[epubIndex]"
|
"Variable: name, author."
|
||||||
if (AppConfig.bookExportFileName.isNullOrBlank()) {
|
// if (AppConfig.bookExportFileName.isNullOrBlank()) {
|
||||||
message += "\n例如:\nname+\"-\"+author+(epubIndex?\"(\"+epubIndex+\")\":\"\")"
|
// message += "\n例如:\nname+\"-\"+author+(epubIndex?\"(\"+epubIndex+\")\":\"\")"
|
||||||
}
|
// }
|
||||||
setMessage(message)
|
setMessage(message)
|
||||||
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
|
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
|
||||||
editView.hint = "file name js"
|
editView.hint = "file name js"
|
||||||
|
|||||||
+115
-89
@@ -33,8 +33,10 @@ import kotlinx.coroutines.sync.withLock
|
|||||||
import me.ag2s.epublib.domain.*
|
import me.ag2s.epublib.domain.*
|
||||||
import me.ag2s.epublib.domain.Date
|
import me.ag2s.epublib.domain.Date
|
||||||
import me.ag2s.epublib.epub.EpubWriter
|
import me.ag2s.epublib.epub.EpubWriter
|
||||||
|
import me.ag2s.epublib.epub.EpubWriterProcessor
|
||||||
import me.ag2s.epublib.util.ResourceUtil
|
import me.ag2s.epublib.util.ResourceUtil
|
||||||
import splitties.init.appCtx
|
import splitties.init.appCtx
|
||||||
|
import java.io.BufferedOutputStream
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
@@ -42,7 +44,6 @@ 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
|
||||||
|
|
||||||
|
|
||||||
@@ -270,7 +271,7 @@ class CacheViewModel(application: Application) : BaseViewModel(application) {
|
|||||||
}
|
}
|
||||||
val left = v[0].toInt()
|
val left = v[0].toInt()
|
||||||
val right = v[1].toInt()
|
val right = v[1].toInt()
|
||||||
if (left > right){
|
if (left > right) {
|
||||||
AppLog.put("Error expression : $s; left > right")
|
AppLog.put("Error expression : $s; left > right")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -354,7 +355,7 @@ class CacheViewModel(application: Application) : BaseViewModel(application) {
|
|||||||
setEpubContent(contentModel, book, epubBook)
|
setEpubContent(contentModel, book, epubBook)
|
||||||
DocumentUtils.createFileIfNotExist(doc, filename)?.let { bookDoc ->
|
DocumentUtils.createFileIfNotExist(doc, filename)?.let { bookDoc ->
|
||||||
context.contentResolver.openOutputStream(bookDoc.uri, "wa")?.use { bookOs ->
|
context.contentResolver.openOutputStream(bookDoc.uri, "wa")?.use { bookOs ->
|
||||||
EpubWriter().write(epubBook, bookOs)
|
EpubWriter().write(epubBook, BufferedOutputStream(bookOs))
|
||||||
}
|
}
|
||||||
if (AppConfig.exportToWebDav) {
|
if (AppConfig.exportToWebDav) {
|
||||||
// 导出到webdav
|
// 导出到webdav
|
||||||
@@ -380,7 +381,7 @@ class CacheViewModel(application: Application) : BaseViewModel(application) {
|
|||||||
//设置正文
|
//设置正文
|
||||||
setEpubContent(contentModel, book, epubBook)
|
setEpubContent(contentModel, book, epubBook)
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
EpubWriter().write(epubBook, FileOutputStream(bookFile))
|
EpubWriter().write(epubBook, BufferedOutputStream(FileOutputStream(bookFile)))
|
||||||
if (AppConfig.exportToWebDav) {
|
if (AppConfig.exportToWebDav) {
|
||||||
// 导出到webdav
|
// 导出到webdav
|
||||||
AppWebDav.exportWebDav(Uri.fromFile(bookFile), filename)
|
AppWebDav.exportWebDav(Uri.fromFile(bookFile), filename)
|
||||||
@@ -636,6 +637,10 @@ class CacheViewModel(application: Application) : BaseViewModel(application) {
|
|||||||
*/
|
*/
|
||||||
class CustomExporter(private val context: CacheViewModel) {
|
class CustomExporter(private val context: CacheViewModel) {
|
||||||
var scope: IntArray = IntArray(0)
|
var scope: IntArray = IntArray(0)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* epub 文件包含最大章节数
|
||||||
|
*/
|
||||||
var size: Int = 1
|
var size: Int = 1
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -659,14 +664,75 @@ class CacheViewModel(application: Application) : BaseViewModel(application) {
|
|||||||
}
|
}
|
||||||
context.exportNumber++
|
context.exportNumber++
|
||||||
}
|
}
|
||||||
if (path.isContentScheme()) {
|
val currentTimeMillis = System.currentTimeMillis()
|
||||||
val uri = Uri.parse(path)
|
when (path.isContentScheme()) {
|
||||||
val doc = DocumentFile.fromTreeUri(context.context, uri)
|
true -> {
|
||||||
?: throw NoStackTraceException("获取导出文档失败")
|
val uri = Uri.parse(path)
|
||||||
exportEpub(doc, book)
|
val doc = DocumentFile.fromTreeUri(context.context, uri)
|
||||||
} else {
|
?: throw NoStackTraceException("获取导出文档失败")
|
||||||
exportEpub(File(path).createFolderIfNotExist(), book)
|
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 {
|
}.onError {
|
||||||
context.exportProgress.remove(book.bookUrl)
|
context.exportProgress.remove(book.bookUrl)
|
||||||
context.exportMsg[book.bookUrl] = it.localizedMessage ?: "ERROR"
|
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正文
|
* 设置epub正文
|
||||||
@@ -729,7 +763,8 @@ class CacheViewModel(application: Application) : BaseViewModel(application) {
|
|||||||
contentModel: String,
|
contentModel: String,
|
||||||
book: Book,
|
book: Book,
|
||||||
epubBook: EpubBook,
|
epubBook: EpubBook,
|
||||||
epubBookIndex: Int
|
epubBookIndex: Int,
|
||||||
|
updateProgress: (chapterList: MutableList<BookChapter>, index: Int) -> Unit
|
||||||
) {
|
) {
|
||||||
//正文
|
//正文
|
||||||
val useReplace = AppConfig.exportUseReplace && book.getUseReplaceRule()
|
val useReplace = AppConfig.exportUseReplace && book.getUseReplaceRule()
|
||||||
@@ -743,7 +778,7 @@ class CacheViewModel(application: Application) : BaseViewModel(application) {
|
|||||||
return@forEachIndexed
|
return@forEachIndexed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val totalChapterNum = book.totalChapterNum / scope.size
|
// val totalChapterNum = book.totalChapterNum / scope.size
|
||||||
if (chapterList.size == 0) {
|
if (chapterList.size == 0) {
|
||||||
throw RuntimeException("书籍<${book.name}>(${epubBookIndex + 1})未找到章节信息")
|
throw RuntimeException("书籍<${book.name}>(${epubBookIndex + 1})未找到章节信息")
|
||||||
}
|
}
|
||||||
@@ -753,9 +788,7 @@ class CacheViewModel(application: Application) : BaseViewModel(application) {
|
|||||||
)
|
)
|
||||||
chapterList.forEachIndexed { index, chapter ->
|
chapterList.forEachIndexed { index, chapter ->
|
||||||
coroutineContext.ensureActive()
|
coroutineContext.ensureActive()
|
||||||
context.upAdapterLiveData.postValue(book.bookUrl)
|
updateProgress(chapterList, index)
|
||||||
context.exportProgress[book.bookUrl] =
|
|
||||||
totalChapterNum * (epubBookIndex * size + index)
|
|
||||||
BookHelp.getContent(book, chapter).let { content ->
|
BookHelp.getContent(book, chapter).let { content ->
|
||||||
var content1 = context.fixPic(
|
var content1 = context.fixPic(
|
||||||
epubBook,
|
epubBook,
|
||||||
@@ -798,22 +831,23 @@ class CacheViewModel(application: Application) : BaseViewModel(application) {
|
|||||||
* 创建多个epub 对象
|
* 创建多个epub 对象
|
||||||
*
|
*
|
||||||
* 分割epub时,一个书籍需要创建多个epub对象
|
* 分割epub时,一个书籍需要创建多个epub对象
|
||||||
*
|
|
||||||
* @param doc 导出文档
|
* @param doc 导出文档
|
||||||
* @param book 书籍
|
* @param book 书籍
|
||||||
*
|
*
|
||||||
* @return <内容模板字符串, <epub文件名, epub对象>>
|
* @return <内容模板字符串, <epub文件名, epub对象>>
|
||||||
*/
|
*/
|
||||||
private fun createEpubs(
|
private fun createEpubs(
|
||||||
doc: DocumentFile,
|
book: Book,
|
||||||
book: Book
|
doc: DocumentFile?,
|
||||||
): Pair<String, List<Pair<String, EpubBook>>> {
|
): Pair<String, List<Pair<String, EpubBook>>> {
|
||||||
val paresNumOfEpub = paresNumOfEpub(scope.size, size)
|
val paresNumOfEpub = paresNumOfEpub(scope.size, size)
|
||||||
val result: MutableList<Pair<String, EpubBook>> = ArrayList(paresNumOfEpub)
|
val result: MutableList<Pair<String, EpubBook>> = ArrayList(paresNumOfEpub)
|
||||||
var contentModel = ""
|
var contentModel = ""
|
||||||
for (i in 1..paresNumOfEpub) {
|
for (i in 1..paresNumOfEpub) {
|
||||||
val filename = book.getExportFileName("epub", i)
|
val filename = book.getExportFileName("epub", i)
|
||||||
DocumentUtils.delete(doc, filename)
|
doc?.let {
|
||||||
|
DocumentUtils.delete(it, filename)
|
||||||
|
}
|
||||||
val epubBook = EpubBook()
|
val epubBook = EpubBook()
|
||||||
epubBook.version = "2.0"
|
epubBook.version = "2.0"
|
||||||
//set metadata
|
//set metadata
|
||||||
@@ -821,39 +855,9 @@ class CacheViewModel(application: Application) : BaseViewModel(application) {
|
|||||||
//set cover
|
//set cover
|
||||||
context.setCover(book, epubBook)
|
context.setCover(book, epubBook)
|
||||||
//set css
|
//set css
|
||||||
contentModel = context.setAssets(doc, book, epubBook)
|
contentModel = doc?.let {
|
||||||
|
context.setAssets(it, book, epubBook)
|
||||||
// add epubBook
|
} ?: context.setAssets(book, 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
|
// add epubBook
|
||||||
result.add(Pair(filename, 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 ->
|
DocumentUtils.createFileIfNotExist(doc, filename)?.let { bookDoc ->
|
||||||
context.context.contentResolver.openOutputStream(bookDoc.uri, "wa")?.use { bookOs ->
|
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) {
|
if (AppConfig.exportToWebDav) {
|
||||||
// 导出到webdav
|
// 导出到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 bookPath = FileUtils.getPath(file, filename)
|
||||||
val bookFile = FileUtils.createFileWithReplace(bookPath)
|
val bookFile = FileUtils.createFileWithReplace(bookPath)
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
@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) {
|
if (AppConfig.exportToWebDav) {
|
||||||
// 导出到webdav
|
// 导出到webdav
|
||||||
AppWebDav.exportWebDav(Uri.fromFile(bookFile), filename)
|
AppWebDav.exportWebDav(Uri.fromFile(bookFile), filename)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<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"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
@@ -54,6 +55,29 @@
|
|||||||
|
|
||||||
</LinearLayout>
|
</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
|
<io.legado.app.ui.widget.text.TextInputLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
@@ -64,8 +88,8 @@
|
|||||||
android:id="@+id/et_epub_size"
|
android:id="@+id/et_epub_size"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:maxLength="6"
|
|
||||||
android:inputType="number"
|
android:inputType="number"
|
||||||
|
android:maxLength="6"
|
||||||
tools:ignore="SpeakableTextPresentCheck,TouchTargetSizeCheck" />
|
tools:ignore="SpeakableTextPresentCheck,TouchTargetSizeCheck" />
|
||||||
|
|
||||||
</io.legado.app.ui.widget.text.TextInputLayout>
|
</io.legado.app.ui.widget.text.TextInputLayout>
|
||||||
|
|||||||
@@ -1119,4 +1119,5 @@
|
|||||||
<string name="exit_app">退出软件</string>
|
<string name="exit_app">退出软件</string>
|
||||||
<string name="add_all_to_bookshelf">全部加入书架</string>
|
<string name="add_all_to_bookshelf">全部加入书架</string>
|
||||||
<string name="page_to">页至</string>
|
<string name="page_to">页至</string>
|
||||||
|
<string name="result_analyzed">Analyzed</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1122,4 +1122,5 @@
|
|||||||
<string name="exit_app">退出软件</string>
|
<string name="exit_app">退出软件</string>
|
||||||
<string name="add_all_to_bookshelf">全部加入书架</string>
|
<string name="add_all_to_bookshelf">全部加入书架</string>
|
||||||
<string name="page_to">页至</string>
|
<string name="page_to">页至</string>
|
||||||
|
<string name="result_analyzed">Analyzed</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1122,4 +1122,5 @@
|
|||||||
<string name="exit_app">退出软件</string>
|
<string name="exit_app">退出软件</string>
|
||||||
<string name="add_all_to_bookshelf">全部加入书架</string>
|
<string name="add_all_to_bookshelf">全部加入书架</string>
|
||||||
<string name="page_to">页至</string>
|
<string name="page_to">页至</string>
|
||||||
|
<string name="result_analyzed">Analyzed</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1118,4 +1118,5 @@ Còn </string>
|
|||||||
<string name="exit_app">Thoát khỏi phần mềm</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="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="page_to">Trang tới</string>
|
||||||
|
<string name="result_analyzed">Analyzed</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1119,4 +1119,5 @@
|
|||||||
<string name="exit_app">退出软件</string>
|
<string name="exit_app">退出软件</string>
|
||||||
<string name="add_all_to_bookshelf">全部加入书架</string>
|
<string name="add_all_to_bookshelf">全部加入书架</string>
|
||||||
<string name="page_to">页至</string>
|
<string name="page_to">页至</string>
|
||||||
|
<string name="result_analyzed">解析示例</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1121,4 +1121,5 @@
|
|||||||
<string name="exit_app">退出软件</string>
|
<string name="exit_app">退出软件</string>
|
||||||
<string name="add_all_to_bookshelf">全部加入书架</string>
|
<string name="add_all_to_bookshelf">全部加入书架</string>
|
||||||
<string name="page_to">页至</string>
|
<string name="page_to">页至</string>
|
||||||
|
<string name="result_analyzed">解析示例</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1121,4 +1121,5 @@
|
|||||||
<string name="exit_app">退出软件</string>
|
<string name="exit_app">退出软件</string>
|
||||||
<string name="add_all_to_bookshelf">全部加入书架</string>
|
<string name="add_all_to_bookshelf">全部加入书架</string>
|
||||||
<string name="page_to">页至</string>
|
<string name="page_to">页至</string>
|
||||||
|
<string name="result_analyzed">解析示例</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1122,4 +1122,5 @@
|
|||||||
<string name="exit_app">退出软件</string>
|
<string name="exit_app">退出软件</string>
|
||||||
<string name="add_all_to_bookshelf">全部加入书架</string>
|
<string name="add_all_to_bookshelf">全部加入书架</string>
|
||||||
<string name="page_to">页至</string>
|
<string name="page_to">页至</string>
|
||||||
|
<string name="result_analyzed">Analyzed</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import java.io.InputStream;
|
|||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.io.OutputStreamWriter;
|
import java.io.OutputStreamWriter;
|
||||||
import java.io.Writer;
|
import java.io.Writer;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.zip.CRC32;
|
import java.util.zip.CRC32;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipOutputStream;
|
import java.util.zip.ZipOutputStream;
|
||||||
@@ -25,15 +26,20 @@ import me.ag2s.epublib.util.IOUtil;
|
|||||||
*/
|
*/
|
||||||
public class EpubWriter {
|
public class EpubWriter {
|
||||||
|
|
||||||
private static final String TAG = EpubWriter.class.getName();
|
|
||||||
|
|
||||||
// package
|
// package
|
||||||
static final String EMPTY_NAMESPACE_PREFIX = "";
|
static final String EMPTY_NAMESPACE_PREFIX = "";
|
||||||
|
private static final String TAG = EpubWriter.class.getName();
|
||||||
private BookProcessor bookProcessor;
|
private BookProcessor bookProcessor;
|
||||||
|
|
||||||
|
private EpubWriterProcessor epubWriterProcessor;
|
||||||
|
|
||||||
public EpubWriter() {
|
public EpubWriter() {
|
||||||
this(BookProcessor.IDENTITY_BOOKPROCESSOR);
|
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;
|
this.bookProcessor = bookProcessor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public EpubWriter setCallback(EpubWriterProcessor.Callback callback) {
|
||||||
|
epubWriterProcessor.setCallback(callback);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public void write(EpubBook book, OutputStream out) throws IOException {
|
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);
|
book = processBook(book);
|
||||||
ZipOutputStream resultStream = new ZipOutputStream(out);
|
ZipOutputStream resultStream = new ZipOutputStream(out);
|
||||||
|
resultStream.setLevel(ZipOutputStream.STORED);
|
||||||
writeMimeType(resultStream);
|
writeMimeType(resultStream);
|
||||||
writeContainer(resultStream);
|
writeContainer(resultStream);
|
||||||
initTOCResource(book);
|
initTOCResource(book);
|
||||||
|
epubWriterProcessor.updateCurrentProgress(1);
|
||||||
writeResources(book, resultStream);
|
writeResources(book, resultStream);
|
||||||
writePackageDocument(book, resultStream);
|
writePackageDocument(book, resultStream);
|
||||||
|
epubWriterProcessor.updateCurrentProgress(epubWriterProcessor.getCurrentProgress() + 1);
|
||||||
resultStream.close();
|
resultStream.close();
|
||||||
|
epubWriterProcessor.updateCurrentProgress(epubWriterProcessor.getCurrentProgress() + 1);
|
||||||
|
if (Objects.nonNull(epubWriterProcessor.getCallback())) {
|
||||||
|
epubWriterProcessor.getCallback().onEnd(book);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private EpubBook processBook(EpubBook book) {
|
private EpubBook processBook(EpubBook book) {
|
||||||
@@ -76,9 +97,7 @@ public class EpubWriter {
|
|||||||
book.getSpine().setTocResource(tocResource);
|
book.getSpine().setTocResource(tocResource);
|
||||||
book.getResources().add(tocResource);
|
book.getResources().add(tocResource);
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Log.e(TAG,
|
Log.e(TAG, "Error writing table of contents: " + ex.getClass().getName() + ": " + ex.getMessage(), ex);
|
||||||
"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) {
|
private void writeResources(EpubBook book, ZipOutputStream resultStream) {
|
||||||
for (Resource resource : book.getResources().getAll()) {
|
for (Resource resource : book.getResources().getAll()) {
|
||||||
writeResource(resource, resultStream);
|
writeResource(resource, resultStream);
|
||||||
|
epubWriterProcessor.updateCurrentProgress(epubWriterProcessor.getCurrentProgress() + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,11 +131,9 @@ public class EpubWriter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void writePackageDocument(EpubBook book, ZipOutputStream resultStream)
|
private void writePackageDocument(EpubBook book, ZipOutputStream resultStream) throws IOException {
|
||||||
throws IOException {
|
|
||||||
resultStream.putNextEntry(new ZipEntry("OEBPS/content.opf"));
|
resultStream.putNextEntry(new ZipEntry("OEBPS/content.opf"));
|
||||||
XmlSerializer xmlSerializer = EpubProcessorSupport
|
XmlSerializer xmlSerializer = EpubProcessorSupport.createXmlSerializer(resultStream);
|
||||||
.createXmlSerializer(resultStream);
|
|
||||||
PackageDocumentWriter.write(this, xmlSerializer, book);
|
PackageDocumentWriter.write(this, xmlSerializer, book);
|
||||||
xmlSerializer.flush();
|
xmlSerializer.flush();
|
||||||
// String resultAsString = result.toString();
|
// String resultAsString = result.toString();
|
||||||
@@ -132,11 +150,9 @@ public class EpubWriter {
|
|||||||
resultStream.putNextEntry(new ZipEntry("META-INF/container.xml"));
|
resultStream.putNextEntry(new ZipEntry("META-INF/container.xml"));
|
||||||
Writer out = new OutputStreamWriter(resultStream);
|
Writer out = new OutputStreamWriter(resultStream);
|
||||||
out.write("<?xml version=\"1.0\"?>\n");
|
out.write("<?xml version=\"1.0\"?>\n");
|
||||||
out.write(
|
out.write("<container version=\"1.0\" xmlns=\"urn:oasis:names:tc:opendocument:xmlns:container\">\n");
|
||||||
"<container version=\"1.0\" xmlns=\"urn:oasis:names:tc:opendocument:xmlns:container\">\n");
|
|
||||||
out.write("\t<rootfiles>\n");
|
out.write("\t<rootfiles>\n");
|
||||||
out.write(
|
out.write("\t\t<rootfile full-path=\"OEBPS/content.opf\" media-type=\"application/oebps-package+xml\"/>\n");
|
||||||
"\t\t<rootfile full-path=\"OEBPS/content.opf\" media-type=\"application/oebps-package+xml\"/>\n");
|
|
||||||
out.write("\t</rootfiles>\n");
|
out.write("\t</rootfiles>\n");
|
||||||
out.write("</container>");
|
out.write("</container>");
|
||||||
out.flush();
|
out.flush();
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user