mirror of
https://github.com/gedoor/legado.git
synced 2024-07-06 23:47:49 +08:00
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:
parent
b29f65bac1
commit
4d6864051d
@ -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"
|
||||
|
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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)
|
||||
|
27
app/src/main/java/io/legado/app/utils/CustomExportUtils.kt
Normal file
27
app/src/main/java/io/legado/app/utils/CustomExportUtils.kt
Normal 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)
|
||||
}
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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("<?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();
|
||||
|
@ -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) {
|
||||
}
|
||||
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user