优化
This commit is contained in:
@@ -417,6 +417,7 @@
|
|||||||
|
|
||||||
<service android:name=".service.CheckSourceService" />
|
<service android:name=".service.CheckSourceService" />
|
||||||
<service android:name=".service.CacheBookService" />
|
<service android:name=".service.CacheBookService" />
|
||||||
|
<service android:name=".service.ExportBookService" />
|
||||||
<service android:name=".service.WebService" />
|
<service android:name=".service.WebService" />
|
||||||
<service
|
<service
|
||||||
android:name=".service.WebTileService"
|
android:name=".service.WebTileService"
|
||||||
|
|||||||
@@ -30,4 +30,5 @@ object EventBus {
|
|||||||
const val UPDATE_READ_ACTION_BAR = "updateReadActionBar"
|
const val UPDATE_READ_ACTION_BAR = "updateReadActionBar"
|
||||||
const val UP_SEEK_BAR = "upSeekBar"
|
const val UP_SEEK_BAR = "upSeekBar"
|
||||||
const val READ_ALOUD_PLAY = "readAloudPlay"
|
const val READ_ALOUD_PLAY = "readAloudPlay"
|
||||||
|
const val EXPORT_BOOK = "exportBook"
|
||||||
}
|
}
|
||||||
@@ -26,6 +26,9 @@ import splitties.init.appCtx
|
|||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓存书籍服务
|
||||||
|
*/
|
||||||
class CacheBookService : BaseService() {
|
class CacheBookService : BaseService() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -75,6 +78,7 @@ class CacheBookService : BaseService() {
|
|||||||
intent.getIntExtra("start", 0),
|
intent.getIntExtra("start", 0),
|
||||||
intent.getIntExtra("end", 0)
|
intent.getIntExtra("end", 0)
|
||||||
)
|
)
|
||||||
|
|
||||||
IntentAction.remove -> removeDownload(intent.getStringExtra("bookUrl"))
|
IntentAction.remove -> removeDownload(intent.getStringExtra("bookUrl"))
|
||||||
IntentAction.stop -> stopSelf()
|
IntentAction.stop -> stopSelf()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,930 @@
|
|||||||
|
package io.legado.app.service
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.net.Uri
|
||||||
|
import android.util.ArraySet
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.bumptech.glide.request.target.CustomTarget
|
||||||
|
import com.bumptech.glide.request.transition.Transition
|
||||||
|
import io.legado.app.R
|
||||||
|
import io.legado.app.base.BaseService
|
||||||
|
import io.legado.app.constant.AppConst
|
||||||
|
import io.legado.app.constant.AppLog
|
||||||
|
import io.legado.app.constant.AppPattern
|
||||||
|
import io.legado.app.constant.EventBus
|
||||||
|
import io.legado.app.constant.IntentAction
|
||||||
|
import io.legado.app.data.appDb
|
||||||
|
import io.legado.app.data.entities.Book
|
||||||
|
import io.legado.app.data.entities.BookChapter
|
||||||
|
import io.legado.app.exception.NoStackTraceException
|
||||||
|
import io.legado.app.help.AppWebDav
|
||||||
|
import io.legado.app.help.book.BookHelp
|
||||||
|
import io.legado.app.help.book.ContentProcessor
|
||||||
|
import io.legado.app.help.book.getExportFileName
|
||||||
|
import io.legado.app.help.config.AppConfig
|
||||||
|
import io.legado.app.help.coroutine.OrderCoroutine
|
||||||
|
import io.legado.app.ui.book.cache.CacheActivity
|
||||||
|
import io.legado.app.utils.DocumentUtils
|
||||||
|
import io.legado.app.utils.FileUtils
|
||||||
|
import io.legado.app.utils.HtmlFormatter
|
||||||
|
import io.legado.app.utils.MD5Utils
|
||||||
|
import io.legado.app.utils.NetworkUtils
|
||||||
|
import io.legado.app.utils.activityPendingIntent
|
||||||
|
import io.legado.app.utils.cnCompare
|
||||||
|
import io.legado.app.utils.createFolderIfNotExist
|
||||||
|
import io.legado.app.utils.isContentScheme
|
||||||
|
import io.legado.app.utils.postEvent
|
||||||
|
import io.legado.app.utils.readBytes
|
||||||
|
import io.legado.app.utils.readText
|
||||||
|
import io.legado.app.utils.servicePendingIntent
|
||||||
|
import io.legado.app.utils.toastOnUi
|
||||||
|
import io.legado.app.utils.writeBytes
|
||||||
|
import kotlinx.coroutines.Deferred
|
||||||
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
|
import kotlinx.coroutines.Dispatchers.Main
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.ensureActive
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import me.ag2s.epublib.domain.Author
|
||||||
|
import me.ag2s.epublib.domain.Date
|
||||||
|
import me.ag2s.epublib.domain.EpubBook
|
||||||
|
import me.ag2s.epublib.domain.FileResourceProvider
|
||||||
|
import me.ag2s.epublib.domain.LazyResource
|
||||||
|
import me.ag2s.epublib.domain.Metadata
|
||||||
|
import me.ag2s.epublib.domain.Resource
|
||||||
|
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
|
||||||
|
import java.nio.charset.Charset
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出书籍服务
|
||||||
|
*/
|
||||||
|
class ExportBookService : BaseService() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val exportProgress = ConcurrentHashMap<String, Int>()
|
||||||
|
val exportMsg = ConcurrentHashMap<String, String>()
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ExportConfig(
|
||||||
|
val path: String,
|
||||||
|
val type: String,
|
||||||
|
val epubSize: Int = 1,
|
||||||
|
val epubScope: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
private val waitExportBooks = linkedMapOf<String, ExportConfig>()
|
||||||
|
private var exportJob: Job? = null
|
||||||
|
|
||||||
|
private var notificationContent = appCtx.getString(R.string.service_starting)
|
||||||
|
|
||||||
|
|
||||||
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
when (intent?.action) {
|
||||||
|
IntentAction.start -> kotlin.runCatching {
|
||||||
|
val bookUrl = intent.getStringExtra("bookUrl")!!
|
||||||
|
if (!exportProgress.contains(bookUrl)) {
|
||||||
|
val exportConfig = ExportConfig(
|
||||||
|
path = intent.getStringExtra("exportPath")!!,
|
||||||
|
type = intent.getStringExtra("exportType")!!,
|
||||||
|
epubSize = intent.getIntExtra("epubSize", 1),
|
||||||
|
epubScope = intent.getStringExtra("epubScope")
|
||||||
|
)
|
||||||
|
waitExportBooks[bookUrl] = exportConfig
|
||||||
|
export()
|
||||||
|
}
|
||||||
|
}.onFailure {
|
||||||
|
toastOnUi(it.localizedMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
IntentAction.stop -> stopSelf()
|
||||||
|
}
|
||||||
|
return super.onStartCommand(intent, flags, startId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
exportProgress.clear()
|
||||||
|
exportMsg.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun upNotification() {
|
||||||
|
val notification = NotificationCompat.Builder(this, AppConst.channelIdDownload)
|
||||||
|
.setSmallIcon(R.drawable.ic_export)
|
||||||
|
.setOngoing(true)
|
||||||
|
.setContentTitle(getString(R.string.export))
|
||||||
|
.setContentIntent(activityPendingIntent<CacheActivity>("cacheActivity"))
|
||||||
|
notification.addAction(
|
||||||
|
R.drawable.ic_stop_black_24dp,
|
||||||
|
getString(R.string.cancel),
|
||||||
|
servicePendingIntent<ExportBookService>(IntentAction.stop)
|
||||||
|
)
|
||||||
|
notification.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||||
|
notification.setContentText(notificationContent)
|
||||||
|
startForeground(AppConst.notificationIdCache, notification.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun export() {
|
||||||
|
if (exportJob?.isActive == true) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val entry = waitExportBooks.firstNotNullOfOrNull { it }
|
||||||
|
if (entry == null) {
|
||||||
|
notificationContent = "导出完成"
|
||||||
|
upNotification()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val bookUrl = entry.key
|
||||||
|
val exportConfig = entry.value
|
||||||
|
if (exportProgress.contains(bookUrl)) return
|
||||||
|
exportProgress[bookUrl] = 0
|
||||||
|
waitExportBooks.remove(bookUrl)
|
||||||
|
exportJob = launch(IO) {
|
||||||
|
val book = appDb.bookDao.getBook(bookUrl)
|
||||||
|
try {
|
||||||
|
book ?: throw NoStackTraceException("获取${bookUrl}书籍出错")
|
||||||
|
notificationContent = "正在导出(${book.name}),还有${waitExportBooks.size}本待导出"
|
||||||
|
if (exportConfig.type == "epub") {
|
||||||
|
if (exportConfig.epubScope.isNullOrBlank()) {
|
||||||
|
exportEPUB(exportConfig.path, book)
|
||||||
|
} else {
|
||||||
|
CustomExporter(paresScope(exportConfig.epubScope), exportConfig.epubSize)
|
||||||
|
.export(exportConfig.path, book)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
export(exportConfig.path, book)
|
||||||
|
}
|
||||||
|
exportMsg[book.bookUrl] = getString(R.string.export_success)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
exportMsg[bookUrl] = e.localizedMessage ?: "ERROR"
|
||||||
|
AppLog.put("导出书籍<${book?.name ?: bookUrl}>出错", e)
|
||||||
|
} finally {
|
||||||
|
exportProgress.remove(bookUrl)
|
||||||
|
postEvent(EventBus.EXPORT_BOOK, bookUrl)
|
||||||
|
}
|
||||||
|
withContext(Main) {
|
||||||
|
export()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun export(path: String, book: Book) {
|
||||||
|
exportMsg.remove(book.bookUrl)
|
||||||
|
postEvent(EventBus.EXPORT_BOOK, book.bookUrl)
|
||||||
|
if (path.isContentScheme()) {
|
||||||
|
val uri = Uri.parse(path)
|
||||||
|
val doc = DocumentFile.fromTreeUri(this@ExportBookService, uri)
|
||||||
|
?: throw NoStackTraceException("获取导出文档失败")
|
||||||
|
export(doc, book)
|
||||||
|
} else {
|
||||||
|
export(File(path).createFolderIfNotExist(), book)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun export(doc: DocumentFile, book: Book) {
|
||||||
|
val filename = book.getExportFileName("txt")
|
||||||
|
DocumentUtils.delete(doc, filename)
|
||||||
|
val bookDoc = DocumentUtils.createFileIfNotExist(doc, filename)
|
||||||
|
?: throw NoStackTraceException("创建文档失败,请尝试重新设置导出文件夹")
|
||||||
|
contentResolver.openOutputStream(bookDoc.uri, "wa")?.use { bookOs ->
|
||||||
|
getAllContents(book) { text, srcList ->
|
||||||
|
bookOs.write(text.toByteArray(Charset.forName(AppConfig.exportCharset)))
|
||||||
|
srcList?.forEach {
|
||||||
|
val vFile = BookHelp.getImage(book, it.third)
|
||||||
|
if (vFile.exists()) {
|
||||||
|
DocumentUtils.createFileIfNotExist(
|
||||||
|
doc,
|
||||||
|
"${it.second}-${MD5Utils.md5Encode16(it.third)}.jpg",
|
||||||
|
subDirs = arrayOf("${book.name}_${book.author}", "images", it.first)
|
||||||
|
)?.writeBytes(this, vFile.readBytes())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (AppConfig.exportToWebDav) {
|
||||||
|
// 导出到webdav
|
||||||
|
AppWebDav.exportWebDav(bookDoc.uri, filename)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun export(file: File, book: Book) {
|
||||||
|
val filename = book.getExportFileName("txt")
|
||||||
|
val bookPath = FileUtils.getPath(file, filename)
|
||||||
|
val bookFile = FileUtils.createFileWithReplace(bookPath)
|
||||||
|
getAllContents(book) { text, srcList ->
|
||||||
|
bookFile.appendText(text, Charset.forName(AppConfig.exportCharset))
|
||||||
|
srcList?.forEach {
|
||||||
|
val vFile = BookHelp.getImage(book, it.third)
|
||||||
|
if (vFile.exists()) {
|
||||||
|
FileUtils.createFileIfNotExist(
|
||||||
|
file,
|
||||||
|
"${book.name}_${book.author}",
|
||||||
|
"images",
|
||||||
|
it.first,
|
||||||
|
"${it.second}-${MD5Utils.md5Encode16(it.third)}.jpg"
|
||||||
|
).writeBytes(vFile.readBytes())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (AppConfig.exportToWebDav) {
|
||||||
|
AppWebDav.exportWebDav(Uri.fromFile(bookFile), filename) // 导出到webdav
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getAllContents(
|
||||||
|
book: Book,
|
||||||
|
append: (text: String, srcList: ArrayList<Triple<String, Int, String>>?) -> Unit
|
||||||
|
) {
|
||||||
|
val useReplace = AppConfig.exportUseReplace && book.getUseReplaceRule()
|
||||||
|
val contentProcessor = ContentProcessor.get(book.name, book.origin)
|
||||||
|
val qy = "${book.name}\n${
|
||||||
|
getString(R.string.author_show, book.getRealAuthor())
|
||||||
|
}\n${
|
||||||
|
getString(
|
||||||
|
R.string.intro_show,
|
||||||
|
"\n" + HtmlFormatter.format(book.getDisplayIntro())
|
||||||
|
)
|
||||||
|
}"
|
||||||
|
append(qy, null)
|
||||||
|
if (AppConfig.parallelExportBook) {
|
||||||
|
val oc =
|
||||||
|
OrderCoroutine<Pair<String, ArrayList<Triple<String, Int, String>>?>>(AppConfig.threadCount)
|
||||||
|
appDb.bookChapterDao.getChapterList(book.bookUrl).forEach { chapter ->
|
||||||
|
oc.submit { getExportData(book, chapter, contentProcessor, useReplace) }
|
||||||
|
}
|
||||||
|
oc.collect { index, result ->
|
||||||
|
postEvent(EventBus.EXPORT_BOOK, book.bookUrl)
|
||||||
|
exportProgress[book.bookUrl] = index
|
||||||
|
append.invoke(result.first, result.second)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
appDb.bookChapterDao.getChapterList(book.bookUrl).forEachIndexed { index, chapter ->
|
||||||
|
kotlin.coroutines.coroutineContext.ensureActive()
|
||||||
|
postEvent(EventBus.EXPORT_BOOK, book.bookUrl)
|
||||||
|
exportProgress[book.bookUrl] = index
|
||||||
|
val result = getExportData(book, chapter, contentProcessor, useReplace)
|
||||||
|
append.invoke(result.first, result.second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getExportData(
|
||||||
|
book: Book,
|
||||||
|
chapter: BookChapter,
|
||||||
|
contentProcessor: ContentProcessor,
|
||||||
|
useReplace: Boolean
|
||||||
|
): Pair<String, ArrayList<Triple<String, Int, String>>?> {
|
||||||
|
BookHelp.getContent(book, chapter).let { content ->
|
||||||
|
val content1 = contentProcessor
|
||||||
|
.getContent(
|
||||||
|
book,
|
||||||
|
// 不导出vip标识
|
||||||
|
chapter.apply { isVip = false },
|
||||||
|
content ?: if (chapter.isVolume) "" else "null",
|
||||||
|
includeTitle = !AppConfig.exportNoChapterName,
|
||||||
|
useReplace = useReplace,
|
||||||
|
chineseConvert = false,
|
||||||
|
reSegment = false
|
||||||
|
).toString()
|
||||||
|
if (AppConfig.exportPictureFile) {
|
||||||
|
//txt导出图片文件
|
||||||
|
val srcList = arrayListOf<Triple<String, Int, String>>()
|
||||||
|
content?.split("\n")?.forEachIndexed { index, text ->
|
||||||
|
val matcher = AppPattern.imgPattern.matcher(text)
|
||||||
|
while (matcher.find()) {
|
||||||
|
matcher.group(1)?.let {
|
||||||
|
val src = NetworkUtils.getAbsoluteURL(chapter.url, it)
|
||||||
|
srcList.add(Triple(chapter.title, index, src))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Pair("\n\n$content1", srcList)
|
||||||
|
} else {
|
||||||
|
return Pair("\n\n$content1", null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析范围字符串
|
||||||
|
*
|
||||||
|
* @param scope 范围字符串
|
||||||
|
* @return 范围
|
||||||
|
*
|
||||||
|
* @since 2023/5/22
|
||||||
|
* @author Discut
|
||||||
|
*/
|
||||||
|
private fun paresScope(scope: String): IntArray {
|
||||||
|
val split = scope.split(",")
|
||||||
|
val result = ArraySet<Int>()
|
||||||
|
for (s in split) {
|
||||||
|
val v = s.split("-")
|
||||||
|
if (v.size != 2) {
|
||||||
|
result.add(s.toInt() - 1)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val left = v[0].toInt()
|
||||||
|
val right = v[1].toInt()
|
||||||
|
if (left > right) {
|
||||||
|
AppLog.put("Error expression : $s; left > right")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for (i in left..right)
|
||||||
|
result.add(i - 1)
|
||||||
|
}
|
||||||
|
return result.toIntArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出Epub
|
||||||
|
*/
|
||||||
|
private suspend fun exportEPUB(path: String, book: Book) {
|
||||||
|
exportMsg.remove(book.bookUrl)
|
||||||
|
postEvent(EventBus.EXPORT_BOOK, book.bookUrl)
|
||||||
|
if (path.isContentScheme()) {
|
||||||
|
val uri = Uri.parse(path)
|
||||||
|
val doc = DocumentFile.fromTreeUri(this@ExportBookService, uri)
|
||||||
|
?: throw NoStackTraceException("获取导出文档失败")
|
||||||
|
exportEpub(doc, book)
|
||||||
|
} else {
|
||||||
|
exportEpub(File(path).createFolderIfNotExist(), book)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun exportEpub(doc: DocumentFile, book: Book) {
|
||||||
|
val filename = book.getExportFileName("epub")
|
||||||
|
DocumentUtils.delete(doc, filename)
|
||||||
|
val epubBook = EpubBook()
|
||||||
|
epubBook.version = "2.0"
|
||||||
|
//set metadata
|
||||||
|
setEpubMetadata(book, epubBook)
|
||||||
|
//set cover
|
||||||
|
setCover(book, epubBook)
|
||||||
|
//set css
|
||||||
|
val contentModel = setAssets(doc, book, epubBook)
|
||||||
|
|
||||||
|
//设置正文
|
||||||
|
setEpubContent(contentModel, book, epubBook)
|
||||||
|
DocumentUtils.createFileIfNotExist(doc, filename)?.let { bookDoc ->
|
||||||
|
contentResolver.openOutputStream(bookDoc.uri, "wa")?.use { bookOs ->
|
||||||
|
EpubWriter().write(epubBook, BufferedOutputStream(bookOs))
|
||||||
|
}
|
||||||
|
if (AppConfig.exportToWebDav) {
|
||||||
|
// 导出到webdav
|
||||||
|
AppWebDav.exportWebDav(bookDoc.uri, filename)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private suspend fun exportEpub(file: File, book: Book) {
|
||||||
|
val filename = book.getExportFileName("epub")
|
||||||
|
val epubBook = EpubBook()
|
||||||
|
epubBook.version = "2.0"
|
||||||
|
//set metadata
|
||||||
|
setEpubMetadata(book, epubBook)
|
||||||
|
//set cover
|
||||||
|
setCover(book, epubBook)
|
||||||
|
//set css
|
||||||
|
val contentModel = setAssets(book, epubBook)
|
||||||
|
|
||||||
|
val bookPath = FileUtils.getPath(file, filename)
|
||||||
|
val bookFile = FileUtils.createFileWithReplace(bookPath)
|
||||||
|
//设置正文
|
||||||
|
setEpubContent(contentModel, book, epubBook)
|
||||||
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
|
EpubWriter().write(epubBook, BufferedOutputStream(FileOutputStream(bookFile)))
|
||||||
|
if (AppConfig.exportToWebDav) {
|
||||||
|
// 导出到webdav
|
||||||
|
AppWebDav.exportWebDav(Uri.fromFile(bookFile), filename)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setAssets(doc: DocumentFile, book: Book, epubBook: EpubBook): String {
|
||||||
|
var contentModel = ""
|
||||||
|
DocumentUtils.getDirDocument(doc, "Asset").let { customPath ->
|
||||||
|
if (customPath == null) {//使用内置模板
|
||||||
|
contentModel = setAssets(book, epubBook)
|
||||||
|
} else {//外部模板
|
||||||
|
customPath.listFiles().forEach { folder ->
|
||||||
|
if (folder.isDirectory && folder.name == "Text") {
|
||||||
|
folder.listFiles().sortedWith { o1, o2 ->
|
||||||
|
val name1 = o1.name ?: ""
|
||||||
|
val name2 = o2.name ?: ""
|
||||||
|
name1.cnCompare(name2)
|
||||||
|
}.forEach { file ->
|
||||||
|
if (file.isFile) {
|
||||||
|
when {
|
||||||
|
//正文模板
|
||||||
|
file.name.equals("chapter.html", true)
|
||||||
|
|| file.name.equals("chapter.xhtml", true) -> {
|
||||||
|
contentModel = file.readText(this)
|
||||||
|
}
|
||||||
|
//封面等其他模板
|
||||||
|
true == file.name?.endsWith("html", true) -> {
|
||||||
|
epubBook.addSection(
|
||||||
|
FileUtils.getNameExcludeExtension(
|
||||||
|
file.name ?: "Cover.html"
|
||||||
|
),
|
||||||
|
ResourceUtil.createPublicResource(
|
||||||
|
book.name,
|
||||||
|
book.getRealAuthor(),
|
||||||
|
book.getDisplayIntro(),
|
||||||
|
book.kind,
|
||||||
|
book.wordCount,
|
||||||
|
file.readText(this),
|
||||||
|
"${folder.name}/${file.name}"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
//其他格式文件当做资源文件
|
||||||
|
folder.listFiles().forEach {
|
||||||
|
if (it.isFile)
|
||||||
|
epubBook.resources.add(
|
||||||
|
Resource(
|
||||||
|
it.readBytes(this),
|
||||||
|
"${folder.name}/${it.name}"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (folder.isDirectory) {
|
||||||
|
//资源文件
|
||||||
|
folder.listFiles().forEach {
|
||||||
|
if (it.isFile)
|
||||||
|
epubBook.resources.add(
|
||||||
|
Resource(
|
||||||
|
it.readBytes(this),
|
||||||
|
"${folder.name}/${it.name}"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {//Asset下面的资源文件
|
||||||
|
epubBook.resources.add(
|
||||||
|
Resource(
|
||||||
|
folder.readBytes(this),
|
||||||
|
"${folder.name}"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return contentModel
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setAssets(book: Book, epubBook: EpubBook): String {
|
||||||
|
epubBook.resources.add(
|
||||||
|
Resource(
|
||||||
|
appCtx.assets.open("epub/fonts.css").readBytes(),
|
||||||
|
"Styles/fonts.css"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
epubBook.resources.add(
|
||||||
|
Resource(
|
||||||
|
appCtx.assets.open("epub/main.css").readBytes(),
|
||||||
|
"Styles/main.css"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
epubBook.resources.add(
|
||||||
|
Resource(
|
||||||
|
appCtx.assets.open("epub/logo.png").readBytes(),
|
||||||
|
"Images/logo.png"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
epubBook.addSection(
|
||||||
|
getString(R.string.img_cover),
|
||||||
|
ResourceUtil.createPublicResource(
|
||||||
|
book.name,
|
||||||
|
book.getRealAuthor(),
|
||||||
|
book.getDisplayIntro(),
|
||||||
|
book.kind,
|
||||||
|
book.wordCount,
|
||||||
|
String(appCtx.assets.open("epub/cover.html").readBytes()),
|
||||||
|
"Text/cover.html"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
epubBook.addSection(
|
||||||
|
getString(R.string.book_intro),
|
||||||
|
ResourceUtil.createPublicResource(
|
||||||
|
book.name,
|
||||||
|
book.getRealAuthor(),
|
||||||
|
book.getDisplayIntro(),
|
||||||
|
book.kind,
|
||||||
|
book.wordCount,
|
||||||
|
String(appCtx.assets.open("epub/intro.html").readBytes()),
|
||||||
|
"Text/intro.html"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return String(appCtx.assets.open("epub/chapter.html").readBytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setCover(book: Book, epubBook: EpubBook) {
|
||||||
|
Glide.with(this)
|
||||||
|
.asBitmap()
|
||||||
|
.load(book.getDisplayCover())
|
||||||
|
.into(object : CustomTarget<Bitmap>() {
|
||||||
|
override fun onResourceReady(
|
||||||
|
resource: Bitmap,
|
||||||
|
transition: Transition<in Bitmap>?
|
||||||
|
) {
|
||||||
|
val stream = ByteArrayOutputStream()
|
||||||
|
resource.compress(Bitmap.CompressFormat.JPEG, 100, stream)
|
||||||
|
val byteArray: ByteArray = stream.toByteArray()
|
||||||
|
stream.close()
|
||||||
|
epubBook.coverImage = Resource(byteArray, "Images/cover.jpg")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLoadCleared(placeholder: Drawable?) {
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun setEpubContent(
|
||||||
|
contentModel: String,
|
||||||
|
book: Book,
|
||||||
|
epubBook: EpubBook
|
||||||
|
) {
|
||||||
|
//正文
|
||||||
|
val useReplace = AppConfig.exportUseReplace && book.getUseReplaceRule()
|
||||||
|
val contentProcessor = ContentProcessor.get(book.name, book.origin)
|
||||||
|
appDb.bookChapterDao.getChapterList(book.bookUrl).forEachIndexed { index, chapter ->
|
||||||
|
kotlin.coroutines.coroutineContext.ensureActive()
|
||||||
|
postEvent(EventBus.EXPORT_BOOK, book.bookUrl)
|
||||||
|
exportProgress[book.bookUrl] = index
|
||||||
|
BookHelp.getContent(book, chapter).let { content ->
|
||||||
|
var content1 = fixPic(
|
||||||
|
epubBook,
|
||||||
|
book,
|
||||||
|
content ?: if (chapter.isVolume) "" else "null",
|
||||||
|
chapter
|
||||||
|
)
|
||||||
|
content1 = contentProcessor
|
||||||
|
.getContent(
|
||||||
|
book,
|
||||||
|
chapter,
|
||||||
|
content1,
|
||||||
|
includeTitle = false,
|
||||||
|
useReplace = useReplace,
|
||||||
|
chineseConvert = false,
|
||||||
|
reSegment = false
|
||||||
|
).toString()
|
||||||
|
val title = chapter.run {
|
||||||
|
// 不导出vip标识
|
||||||
|
isVip = false
|
||||||
|
getDisplayTitle(
|
||||||
|
contentProcessor.getTitleReplaceRules(),
|
||||||
|
useReplace = useReplace
|
||||||
|
)
|
||||||
|
}
|
||||||
|
epubBook.addSection(
|
||||||
|
title,
|
||||||
|
ResourceUtil.createChapterResource(
|
||||||
|
title.replace("\uD83D\uDD12", ""),
|
||||||
|
content1,
|
||||||
|
contentModel,
|
||||||
|
"Text/chapter_${index}.html"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fixPic(
|
||||||
|
epubBook: EpubBook,
|
||||||
|
book: Book,
|
||||||
|
content: String,
|
||||||
|
chapter: BookChapter
|
||||||
|
): String {
|
||||||
|
val data = StringBuilder("")
|
||||||
|
content.split("\n").forEach { text ->
|
||||||
|
var text1 = text
|
||||||
|
val matcher = AppPattern.imgPattern.matcher(text)
|
||||||
|
while (matcher.find()) {
|
||||||
|
matcher.group(1)?.let {
|
||||||
|
val src = NetworkUtils.getAbsoluteURL(chapter.url, it)
|
||||||
|
val originalHref =
|
||||||
|
"${MD5Utils.md5Encode16(src)}.${BookHelp.getImageSuffix(src)}"
|
||||||
|
val href =
|
||||||
|
"Images/${MD5Utils.md5Encode16(src)}.${BookHelp.getImageSuffix(src)}"
|
||||||
|
val vFile = BookHelp.getImage(book, src)
|
||||||
|
val fp = FileResourceProvider(vFile.parent)
|
||||||
|
if (vFile.exists()) {
|
||||||
|
val img = LazyResource(fp, href, originalHref)
|
||||||
|
epubBook.resources.add(img)
|
||||||
|
}
|
||||||
|
text1 = text1.replace(src, "../${href}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data.append(text1).append("\n")
|
||||||
|
}
|
||||||
|
return data.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setEpubMetadata(book: Book, epubBook: EpubBook) {
|
||||||
|
val metadata = Metadata()
|
||||||
|
metadata.titles.add(book.name)//书籍的名称
|
||||||
|
metadata.authors.add(Author(book.getRealAuthor()))//书籍的作者
|
||||||
|
metadata.language = "zh"//数据的语言
|
||||||
|
metadata.dates.add(Date())//数据的创建日期
|
||||||
|
metadata.publishers.add("Legado")//数据的创建者
|
||||||
|
metadata.descriptions.add(book.getDisplayIntro())//书籍的简介
|
||||||
|
//metadata.subjects.add("")//书籍的主题,在静读天下里面有使用这个分类书籍
|
||||||
|
epubBook.metadata = metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
//////end of EPUB
|
||||||
|
|
||||||
|
//////start of custom exporter
|
||||||
|
/**
|
||||||
|
* 自定义Exporter
|
||||||
|
* @param scope 导出范围
|
||||||
|
* @param size epub 文件包含最大章节数
|
||||||
|
*/
|
||||||
|
inner class CustomExporter(private val scope: IntArray, private val size: Int) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出Epub
|
||||||
|
* @param path 导出的路径
|
||||||
|
* @param book 书籍
|
||||||
|
*/
|
||||||
|
suspend fun export(
|
||||||
|
path: String,
|
||||||
|
book: Book
|
||||||
|
) {
|
||||||
|
exportProgress[book.bookUrl] = 0
|
||||||
|
exportMsg.remove(book.bookUrl)
|
||||||
|
postEvent(EventBus.EXPORT_BOOK, book.bookUrl)
|
||||||
|
val currentTimeMillis = System.currentTimeMillis()
|
||||||
|
when (path.isContentScheme()) {
|
||||||
|
true -> {
|
||||||
|
val uri = Uri.parse(path)
|
||||||
|
val doc = DocumentFile.fromTreeUri(this@ExportBookService, 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
|
||||||
|
) { _, _ ->
|
||||||
|
// 将章节写入内存时更新进度条
|
||||||
|
postEvent(EventBus.EXPORT_BOOK, book.bookUrl)
|
||||||
|
progressBar += book.totalChapterNum.toDouble() / scope.size / 2
|
||||||
|
exportProgress[book.bookUrl] = progressBar.toInt()
|
||||||
|
}
|
||||||
|
save2Drive(filename, epubBook, doc) { total, progress ->
|
||||||
|
//写入硬盘时更新进度条
|
||||||
|
progressBar += book.totalChapterNum.toDouble() / epubList.size / total / 2
|
||||||
|
postEvent(EventBus.EXPORT_BOOK, book.bookUrl)
|
||||||
|
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
|
||||||
|
) { _, _ ->
|
||||||
|
postEvent(EventBus.EXPORT_BOOK, book.bookUrl)
|
||||||
|
exportProgress[book.bookUrl] =
|
||||||
|
exportProgress[book.bookUrl]?.plus(book.totalChapterNum / scope.size)
|
||||||
|
?: 1
|
||||||
|
}
|
||||||
|
save2Drive(filename, epubBook, file) { total, progress ->
|
||||||
|
//设置进度
|
||||||
|
progressBar += book.totalChapterNum.toDouble() / epubList.size / total / 2
|
||||||
|
postEvent(EventBus.EXPORT_BOOK, book.bookUrl)
|
||||||
|
exportProgress[book.bookUrl] = progressBar.toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
asyncBlocks.add(asyncBlock)
|
||||||
|
}
|
||||||
|
asyncBlocks.forEach { it.await() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AppLog.put("分割导出书籍 ${book.name} 一共耗时 ${System.currentTimeMillis() - currentTimeMillis}")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置epub正文
|
||||||
|
*
|
||||||
|
* @param contentModel 正文模板
|
||||||
|
* @param book 书籍
|
||||||
|
* @param epubBook 分割后的epub
|
||||||
|
* @param epubBookIndex 分割后的epub序号
|
||||||
|
*/
|
||||||
|
private fun setEpubContent(
|
||||||
|
contentModel: String,
|
||||||
|
book: Book,
|
||||||
|
epubBook: EpubBook,
|
||||||
|
epubBookIndex: Int,
|
||||||
|
updateProgress: (chapterList: MutableList<BookChapter>, index: Int) -> Unit
|
||||||
|
) {
|
||||||
|
//正文
|
||||||
|
val useReplace = AppConfig.exportUseReplace && book.getUseReplaceRule()
|
||||||
|
val contentProcessor = ContentProcessor.get(book.name, book.origin)
|
||||||
|
var chapterList: MutableList<BookChapter> = ArrayList()
|
||||||
|
appDb.bookChapterDao.getChapterList(book.bookUrl).forEachIndexed { index, chapter ->
|
||||||
|
if (scope.indexOf(index) >= 0) {
|
||||||
|
chapterList.add(chapter)
|
||||||
|
}
|
||||||
|
if (scope.size == chapterList.size) {
|
||||||
|
return@forEachIndexed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// val totalChapterNum = book.totalChapterNum / scope.size
|
||||||
|
if (chapterList.size == 0) {
|
||||||
|
throw RuntimeException("书籍<${book.name}>(${epubBookIndex + 1})未找到章节信息")
|
||||||
|
}
|
||||||
|
chapterList = chapterList.subList(
|
||||||
|
epubBookIndex * size,
|
||||||
|
if ((epubBookIndex + 1) * size > scope.size) scope.size else (epubBookIndex + 1) * size
|
||||||
|
)
|
||||||
|
chapterList.forEachIndexed { index, chapter ->
|
||||||
|
coroutineContext.ensureActive()
|
||||||
|
updateProgress(chapterList, index)
|
||||||
|
BookHelp.getContent(book, chapter).let { content ->
|
||||||
|
var content1 = fixPic(
|
||||||
|
epubBook,
|
||||||
|
book,
|
||||||
|
content ?: if (chapter.isVolume) "" else "null",
|
||||||
|
chapter
|
||||||
|
)
|
||||||
|
content1 = contentProcessor
|
||||||
|
.getContent(
|
||||||
|
book,
|
||||||
|
chapter,
|
||||||
|
content1,
|
||||||
|
includeTitle = false,
|
||||||
|
useReplace = useReplace,
|
||||||
|
chineseConvert = false,
|
||||||
|
reSegment = false
|
||||||
|
).toString()
|
||||||
|
val title = chapter.run {
|
||||||
|
// 不导出vip标识
|
||||||
|
isVip = false
|
||||||
|
getDisplayTitle(
|
||||||
|
contentProcessor.getTitleReplaceRules(),
|
||||||
|
useReplace = useReplace
|
||||||
|
)
|
||||||
|
}
|
||||||
|
epubBook.addSection(
|
||||||
|
title,
|
||||||
|
ResourceUtil.createChapterResource(
|
||||||
|
title.replace("\uD83D\uDD12", ""),
|
||||||
|
content1,
|
||||||
|
contentModel,
|
||||||
|
"Text/chapter_${index}.html"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建多个epub 对象
|
||||||
|
*
|
||||||
|
* 分割epub时,一个书籍需要创建多个epub对象
|
||||||
|
* @param doc 导出文档
|
||||||
|
* @param book 书籍
|
||||||
|
*
|
||||||
|
* @return <内容模板字符串, <epub文件名, epub对象>>
|
||||||
|
*/
|
||||||
|
private fun createEpubs(
|
||||||
|
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)
|
||||||
|
doc?.let {
|
||||||
|
DocumentUtils.delete(it, filename)
|
||||||
|
}
|
||||||
|
val epubBook = EpubBook()
|
||||||
|
epubBook.version = "2.0"
|
||||||
|
//set metadata
|
||||||
|
setEpubMetadata(book, epubBook)
|
||||||
|
//set cover
|
||||||
|
setCover(book, epubBook)
|
||||||
|
//set css
|
||||||
|
contentModel = doc?.let {
|
||||||
|
setAssets(it, book, epubBook)
|
||||||
|
} ?: setAssets(book, epubBook)
|
||||||
|
|
||||||
|
// add epubBook
|
||||||
|
result.add(Pair(filename, epubBook))
|
||||||
|
}
|
||||||
|
return Pair(contentModel, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存文件到 设备
|
||||||
|
*/
|
||||||
|
private suspend fun save2Drive(
|
||||||
|
filename: String,
|
||||||
|
epubBook: EpubBook,
|
||||||
|
doc: DocumentFile,
|
||||||
|
callback: (total: Int, progress: Int) -> Unit
|
||||||
|
) {
|
||||||
|
DocumentUtils.createFileIfNotExist(doc, filename)?.let { bookDoc ->
|
||||||
|
contentResolver.openOutputStream(bookDoc.uri, "wa")?.use { bookOs ->
|
||||||
|
EpubWriter()
|
||||||
|
.setCallback(object : EpubWriterProcessor.Callback {
|
||||||
|
override fun onProgressing(total: Int, progress: Int) {
|
||||||
|
callback(total, progress)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.write(epubBook, BufferedOutputStream(bookOs))
|
||||||
|
}
|
||||||
|
if (AppConfig.exportToWebDav) {
|
||||||
|
// 导出到webdav
|
||||||
|
AppWebDav.exportWebDav(bookDoc.uri, filename)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存文件到 设备
|
||||||
|
*/
|
||||||
|
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()
|
||||||
|
.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 分割epub后的数量
|
||||||
|
*
|
||||||
|
* @param total 章节总数
|
||||||
|
* @param size 每个epub文件包含多少章节
|
||||||
|
*/
|
||||||
|
private fun paresNumOfEpub(total: Int, size: Int): Int {
|
||||||
|
val i = total % size
|
||||||
|
var result = total / size
|
||||||
|
if (i > 0) {
|
||||||
|
result++
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+32
-30
@@ -15,6 +15,7 @@ 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.AppLog
|
||||||
import io.legado.app.constant.EventBus
|
import io.legado.app.constant.EventBus
|
||||||
|
import io.legado.app.constant.IntentAction
|
||||||
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
|
||||||
import io.legado.app.data.entities.BookChapter
|
import io.legado.app.data.entities.BookChapter
|
||||||
@@ -30,6 +31,7 @@ import io.legado.app.lib.dialogs.SelectItem
|
|||||||
import io.legado.app.lib.dialogs.alert
|
import io.legado.app.lib.dialogs.alert
|
||||||
import io.legado.app.lib.dialogs.selector
|
import io.legado.app.lib.dialogs.selector
|
||||||
import io.legado.app.model.CacheBook
|
import io.legado.app.model.CacheBook
|
||||||
|
import io.legado.app.service.ExportBookService
|
||||||
import io.legado.app.ui.about.AppLogDialog
|
import io.legado.app.ui.about.AppLogDialog
|
||||||
import io.legado.app.ui.file.HandleFileContract
|
import io.legado.app.ui.file.HandleFileContract
|
||||||
import io.legado.app.utils.*
|
import io.legado.app.utils.*
|
||||||
@@ -239,6 +241,9 @@ class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>()
|
|||||||
viewModel.upAdapterLiveData.observe(this) {
|
viewModel.upAdapterLiveData.observe(this) {
|
||||||
notifyItemChanged(it)
|
notifyItemChanged(it)
|
||||||
}
|
}
|
||||||
|
observeEvent<String>(EventBus.EXPORT_BOOK) {
|
||||||
|
notifyItemChanged(it)
|
||||||
|
}
|
||||||
observeEvent<String>(EventBus.UP_DOWNLOAD) {
|
observeEvent<String>(EventBus.UP_DOWNLOAD) {
|
||||||
if (!CacheBook.isRun) {
|
if (!CacheBook.isRun) {
|
||||||
menu?.findItem(R.id.menu_download)?.let { item ->
|
menu?.findItem(R.id.menu_download)?.let { item ->
|
||||||
@@ -291,7 +296,6 @@ class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>()
|
|||||||
*/
|
*/
|
||||||
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 {
|
fun verifyExportFileNameJsStr(js: String): Boolean {
|
||||||
@@ -391,16 +395,22 @@ class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>()
|
|||||||
}
|
}
|
||||||
etInputScope.error = null
|
etInputScope.error = null
|
||||||
val toInt = etEpubSize.text.toString().toInt()
|
val toInt = etEpubSize.text.toString().toInt()
|
||||||
startExport(path, position, toInt, text.toString())
|
adapter.getItem(position)?.let { book ->
|
||||||
|
startService<ExportBookService> {
|
||||||
|
action = IntentAction.start
|
||||||
|
putExtra("bookUrl", book.bookUrl)
|
||||||
|
putExtra("exportType", "epub")
|
||||||
|
putExtra("exportPath", path)
|
||||||
|
putExtra("epubSize", toInt)
|
||||||
|
putExtra("epubScope", text.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
alertDialog.hide()
|
alertDialog.hide()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private fun selectExportFolder(exportPosition: Int) {
|
private fun selectExportFolder(exportPosition: Int) {
|
||||||
val default = arrayListOf<SelectItem<Int>>()
|
val default = arrayListOf<SelectItem<Int>>()
|
||||||
val path = ACache.get().getAsString(exportBookPathKey)
|
val path = ACache.get().getAsString(exportBookPathKey)
|
||||||
@@ -413,25 +423,19 @@ class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startExport(path: String, exportPosition: Int, size: Int, scope: String) {
|
|
||||||
if (exportPosition >= 0) {
|
|
||||||
adapter.getItem(exportPosition)?.let { book ->
|
|
||||||
when (AppConfig.exportType) {
|
|
||||||
1 -> viewModel.exportEPUBs(path, book, size, scope)
|
|
||||||
// 目前仅支持 epub
|
|
||||||
//else -> viewModel.export(path, book)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun startExport(path: String, exportPosition: Int) {
|
private fun startExport(path: String, exportPosition: Int) {
|
||||||
|
val exportType = when (AppConfig.exportType) {
|
||||||
|
1 -> "epub"
|
||||||
|
else -> "txt"
|
||||||
|
}
|
||||||
if (exportPosition == -10) {
|
if (exportPosition == -10) {
|
||||||
if (adapter.getItems().isNotEmpty()) {
|
if (adapter.getItems().isNotEmpty()) {
|
||||||
adapter.getItems().forEach { book ->
|
adapter.getItems().forEach { book ->
|
||||||
when (AppConfig.exportType) {
|
startService<ExportBookService> {
|
||||||
1 -> viewModel.exportEPUB(path, book)
|
action = IntentAction.start
|
||||||
else -> viewModel.export(path, book)
|
putExtra("bookUrl", book.bookUrl)
|
||||||
|
putExtra("exportType", exportType)
|
||||||
|
putExtra("exportPath", path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -439,9 +443,11 @@ class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>()
|
|||||||
}
|
}
|
||||||
} else if (exportPosition >= 0) {
|
} else if (exportPosition >= 0) {
|
||||||
adapter.getItem(exportPosition)?.let { book ->
|
adapter.getItem(exportPosition)?.let { book ->
|
||||||
when (AppConfig.exportType) {
|
startService<ExportBookService> {
|
||||||
1 -> viewModel.exportEPUB(path, book)
|
action = IntentAction.start
|
||||||
else -> viewModel.export(path, book)
|
putExtra("bookUrl", book.bookUrl)
|
||||||
|
putExtra("exportType", exportType)
|
||||||
|
putExtra("exportPath", path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -450,11 +456,7 @@ 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) {
|
||||||
val message =
|
val message = "Variable: name, author."
|
||||||
"Variable: name, author."
|
|
||||||
// if (AppConfig.bookExportFileName.isNullOrBlank()) {
|
|
||||||
// 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"
|
||||||
@@ -499,11 +501,11 @@ class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>()
|
|||||||
get() = viewModel.cacheChapters
|
get() = viewModel.cacheChapters
|
||||||
|
|
||||||
override fun exportProgress(bookUrl: String): Int? {
|
override fun exportProgress(bookUrl: String): Int? {
|
||||||
return viewModel.exportProgress[bookUrl]
|
return ExportBookService.exportProgress[bookUrl]
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun exportMsg(bookUrl: String): String? {
|
override fun exportMsg(bookUrl: String): String? {
|
||||||
return viewModel.exportMsg[bookUrl]
|
return ExportBookService.exportMsg[bookUrl]
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
+12
-895
@@ -1,62 +1,30 @@
|
|||||||
package io.legado.app.ui.book.cache
|
package io.legado.app.ui.book.cache
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
import android.net.Uri
|
|
||||||
import android.util.ArraySet
|
|
||||||
import androidx.documentfile.provider.DocumentFile
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import com.bumptech.glide.Glide
|
|
||||||
import com.bumptech.glide.request.target.CustomTarget
|
|
||||||
import com.bumptech.glide.request.transition.Transition
|
|
||||||
import io.legado.app.R
|
|
||||||
import io.legado.app.base.BaseViewModel
|
import io.legado.app.base.BaseViewModel
|
||||||
import io.legado.app.constant.AppLog
|
|
||||||
import io.legado.app.constant.AppPattern
|
|
||||||
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
|
||||||
import io.legado.app.data.entities.BookChapter
|
|
||||||
import io.legado.app.exception.NoStackTraceException
|
|
||||||
import io.legado.app.help.AppWebDav
|
|
||||||
import io.legado.app.help.book.BookHelp
|
import io.legado.app.help.book.BookHelp
|
||||||
import io.legado.app.help.book.ContentProcessor
|
|
||||||
import io.legado.app.help.book.getExportFileName
|
|
||||||
import io.legado.app.help.book.isLocal
|
import io.legado.app.help.book.isLocal
|
||||||
import io.legado.app.help.config.AppConfig
|
|
||||||
import io.legado.app.help.coroutine.Coroutine
|
import io.legado.app.help.coroutine.Coroutine
|
||||||
import io.legado.app.help.coroutine.OrderCoroutine
|
import io.legado.app.utils.sendValue
|
||||||
import io.legado.app.utils.*
|
import kotlinx.coroutines.ensureActive
|
||||||
import kotlinx.coroutines.*
|
import kotlin.collections.HashSet
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlin.collections.List
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlin.collections.contains
|
||||||
import me.ag2s.epublib.domain.*
|
import kotlin.collections.forEach
|
||||||
import me.ag2s.epublib.domain.Date
|
import kotlin.collections.hashMapOf
|
||||||
import me.ag2s.epublib.epub.EpubWriter
|
import kotlin.collections.hashSetOf
|
||||||
import me.ag2s.epublib.epub.EpubWriterProcessor
|
import kotlin.collections.isNotEmpty
|
||||||
import me.ag2s.epublib.util.ResourceUtil
|
import kotlin.collections.set
|
||||||
import splitties.init.appCtx
|
|
||||||
import java.io.BufferedOutputStream
|
|
||||||
import java.io.ByteArrayOutputStream
|
|
||||||
import java.io.File
|
|
||||||
import java.io.FileOutputStream
|
|
||||||
import java.nio.charset.Charset
|
|
||||||
import java.nio.file.*
|
|
||||||
import java.util.*
|
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
|
||||||
import kotlin.coroutines.coroutineContext
|
|
||||||
|
|
||||||
|
|
||||||
class CacheViewModel(application: Application) : BaseViewModel(application) {
|
class CacheViewModel(application: Application) : BaseViewModel(application) {
|
||||||
val upAdapterLiveData = MutableLiveData<String>()
|
val upAdapterLiveData = MutableLiveData<String>()
|
||||||
val exportProgress = ConcurrentHashMap<String, Int>()
|
|
||||||
val exportMsg = ConcurrentHashMap<String, String>()
|
|
||||||
private val mutex = Mutex()
|
|
||||||
val cacheChapters = hashMapOf<String, HashSet<String>>()
|
|
||||||
private var loadChapterCoroutine: Coroutine<Unit>? = null
|
|
||||||
|
|
||||||
@Volatile
|
private var loadChapterCoroutine: Coroutine<Unit>? = null
|
||||||
private var exportNumber = 0
|
val cacheChapters = hashMapOf<String, HashSet<String>>()
|
||||||
|
|
||||||
fun loadCacheFiles(books: List<Book>) {
|
fun loadCacheFiles(books: List<Book>) {
|
||||||
loadChapterCoroutine?.cancel()
|
loadChapterCoroutine?.cancel()
|
||||||
@@ -80,855 +48,4 @@ class CacheViewModel(application: Application) : BaseViewModel(application) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun exportFileExist(path: String, book: Book): Boolean {
|
|
||||||
val fileName = book.getExportFileName("txt")
|
|
||||||
return if (path.isContentScheme()) {
|
|
||||||
val uri = Uri.parse(path)
|
|
||||||
val doc = DocumentFile.fromTreeUri(context, uri) ?: return false
|
|
||||||
doc.findFile(fileName) ?: return false
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
File(path).exists(fileName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun export(path: String, book: Book) {
|
|
||||||
if (exportProgress.contains(book.bookUrl)) return
|
|
||||||
exportProgress[book.bookUrl] = 0
|
|
||||||
exportMsg.remove(book.bookUrl)
|
|
||||||
upAdapterLiveData.sendValue(book.bookUrl)
|
|
||||||
execute {
|
|
||||||
mutex.withLock {
|
|
||||||
while (exportNumber > 0) {
|
|
||||||
delay(1000)
|
|
||||||
}
|
|
||||||
exportNumber++
|
|
||||||
}
|
|
||||||
if (path.isContentScheme()) {
|
|
||||||
val uri = Uri.parse(path)
|
|
||||||
val doc = DocumentFile.fromTreeUri(context, uri)
|
|
||||||
?: throw NoStackTraceException("获取导出文档失败")
|
|
||||||
export(doc, book)
|
|
||||||
} else {
|
|
||||||
export(File(path).createFolderIfNotExist(), book)
|
|
||||||
}
|
|
||||||
}.onError {
|
|
||||||
exportProgress.remove(book.bookUrl)
|
|
||||||
exportMsg[book.bookUrl] = it.localizedMessage ?: "ERROR"
|
|
||||||
upAdapterLiveData.postValue(book.bookUrl)
|
|
||||||
AppLog.put("导出书籍<${book.name}>出错", it)
|
|
||||||
}.onSuccess {
|
|
||||||
exportProgress.remove(book.bookUrl)
|
|
||||||
exportMsg[book.bookUrl] = context.getString(R.string.export_success)
|
|
||||||
upAdapterLiveData.postValue(book.bookUrl)
|
|
||||||
}.onFinally {
|
|
||||||
exportNumber--
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun export(doc: DocumentFile, book: Book) {
|
|
||||||
val filename = book.getExportFileName("txt")
|
|
||||||
DocumentUtils.delete(doc, filename)
|
|
||||||
val bookDoc = DocumentUtils.createFileIfNotExist(doc, filename)
|
|
||||||
?: throw NoStackTraceException("创建文档失败,请尝试重新设置导出文件夹")
|
|
||||||
context.contentResolver.openOutputStream(bookDoc.uri, "wa")?.use { bookOs ->
|
|
||||||
getAllContents(book) { text, srcList ->
|
|
||||||
bookOs.write(text.toByteArray(Charset.forName(AppConfig.exportCharset)))
|
|
||||||
srcList?.forEach {
|
|
||||||
val vFile = BookHelp.getImage(book, it.third)
|
|
||||||
if (vFile.exists()) {
|
|
||||||
DocumentUtils.createFileIfNotExist(
|
|
||||||
doc,
|
|
||||||
"${it.second}-${MD5Utils.md5Encode16(it.third)}.jpg",
|
|
||||||
subDirs = arrayOf("${book.name}_${book.author}", "images", it.first)
|
|
||||||
)?.writeBytes(context, vFile.readBytes())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (AppConfig.exportToWebDav) {
|
|
||||||
// 导出到webdav
|
|
||||||
AppWebDav.exportWebDav(bookDoc.uri, filename)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun export(file: File, book: Book) {
|
|
||||||
val filename = book.getExportFileName("txt")
|
|
||||||
val bookPath = FileUtils.getPath(file, filename)
|
|
||||||
val bookFile = FileUtils.createFileWithReplace(bookPath)
|
|
||||||
getAllContents(book) { text, srcList ->
|
|
||||||
bookFile.appendText(text, Charset.forName(AppConfig.exportCharset))
|
|
||||||
srcList?.forEach {
|
|
||||||
val vFile = BookHelp.getImage(book, it.third)
|
|
||||||
if (vFile.exists()) {
|
|
||||||
FileUtils.createFileIfNotExist(
|
|
||||||
file,
|
|
||||||
"${book.name}_${book.author}",
|
|
||||||
"images",
|
|
||||||
it.first,
|
|
||||||
"${it.second}-${MD5Utils.md5Encode16(it.third)}.jpg"
|
|
||||||
).writeBytes(vFile.readBytes())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (AppConfig.exportToWebDav) {
|
|
||||||
AppWebDav.exportWebDav(Uri.fromFile(bookFile), filename) // 导出到webdav
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun getAllContents(
|
|
||||||
book: Book,
|
|
||||||
append: (text: String, srcList: ArrayList<Triple<String, Int, String>>?) -> Unit
|
|
||||||
) {
|
|
||||||
val useReplace = AppConfig.exportUseReplace && book.getUseReplaceRule()
|
|
||||||
val contentProcessor = ContentProcessor.get(book.name, book.origin)
|
|
||||||
val qy = "${book.name}\n${
|
|
||||||
context.getString(R.string.author_show, book.getRealAuthor())
|
|
||||||
}\n${
|
|
||||||
context.getString(
|
|
||||||
R.string.intro_show,
|
|
||||||
"\n" + HtmlFormatter.format(book.getDisplayIntro())
|
|
||||||
)
|
|
||||||
}"
|
|
||||||
append(qy, null)
|
|
||||||
if (AppConfig.parallelExportBook) {
|
|
||||||
val oc =
|
|
||||||
OrderCoroutine<Pair<String, ArrayList<Triple<String, Int, String>>?>>(AppConfig.threadCount)
|
|
||||||
appDb.bookChapterDao.getChapterList(book.bookUrl).forEach { chapter ->
|
|
||||||
oc.submit { getExportData(book, chapter, contentProcessor, useReplace) }
|
|
||||||
}
|
|
||||||
oc.collect { index, result ->
|
|
||||||
upAdapterLiveData.postValue(book.bookUrl)
|
|
||||||
exportProgress[book.bookUrl] = index
|
|
||||||
append.invoke(result.first, result.second)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
appDb.bookChapterDao.getChapterList(book.bookUrl).forEachIndexed { index, chapter ->
|
|
||||||
coroutineContext.ensureActive()
|
|
||||||
upAdapterLiveData.postValue(book.bookUrl)
|
|
||||||
exportProgress[book.bookUrl] = index
|
|
||||||
val result = getExportData(book, chapter, contentProcessor, useReplace)
|
|
||||||
append.invoke(result.first, result.second)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun getExportData(
|
|
||||||
book: Book,
|
|
||||||
chapter: BookChapter,
|
|
||||||
contentProcessor: ContentProcessor,
|
|
||||||
useReplace: Boolean
|
|
||||||
): Pair<String, ArrayList<Triple<String, Int, String>>?> {
|
|
||||||
BookHelp.getContent(book, chapter).let { content ->
|
|
||||||
val content1 = contentProcessor
|
|
||||||
.getContent(
|
|
||||||
book,
|
|
||||||
// 不导出vip标识
|
|
||||||
chapter.apply { isVip = false },
|
|
||||||
content ?: if (chapter.isVolume) "" else "null",
|
|
||||||
includeTitle = !AppConfig.exportNoChapterName,
|
|
||||||
useReplace = useReplace,
|
|
||||||
chineseConvert = false,
|
|
||||||
reSegment = false
|
|
||||||
).toString()
|
|
||||||
if (AppConfig.exportPictureFile) {
|
|
||||||
//txt导出图片文件
|
|
||||||
val srcList = arrayListOf<Triple<String, Int, String>>()
|
|
||||||
content?.split("\n")?.forEachIndexed { index, text ->
|
|
||||||
val matcher = AppPattern.imgPattern.matcher(text)
|
|
||||||
while (matcher.find()) {
|
|
||||||
matcher.group(1)?.let {
|
|
||||||
val src = NetworkUtils.getAbsoluteURL(chapter.url, it)
|
|
||||||
srcList.add(Triple(chapter.title, index, src))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Pair("\n\n$content1", srcList)
|
|
||||||
} else {
|
|
||||||
return Pair("\n\n$content1", null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 解析范围字符串
|
|
||||||
*
|
|
||||||
* @param scope 范围字符串
|
|
||||||
* @return 范围
|
|
||||||
*
|
|
||||||
* @since 2023/5/22
|
|
||||||
* @author Discut
|
|
||||||
*/
|
|
||||||
private fun paresScope(scope: String): IntArray {
|
|
||||||
val split = scope.split(",")
|
|
||||||
val result = ArraySet<Int>()
|
|
||||||
for (s in split) {
|
|
||||||
val v = s.split("-")
|
|
||||||
if (v.size != 2) {
|
|
||||||
result.add(s.toInt() - 1)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
val left = v[0].toInt()
|
|
||||||
val right = v[1].toInt()
|
|
||||||
if (left > right) {
|
|
||||||
AppLog.put("Error expression : $s; left > right")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for (i in left..right)
|
|
||||||
result.add(i - 1)
|
|
||||||
}
|
|
||||||
return result.toIntArray()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//////////////////Start EPUB
|
|
||||||
/**
|
|
||||||
* 导出Epub 根据自定义导出范围
|
|
||||||
*
|
|
||||||
* @param path 导出路径
|
|
||||||
* @param book 书籍
|
|
||||||
* @param size 每本Epub包含的章节
|
|
||||||
* @param scope 导出范围
|
|
||||||
* @since 2023/5/22
|
|
||||||
*/
|
|
||||||
fun exportEPUBs(path: String, book: Book, size: Int = 1, scope: String) {
|
|
||||||
if (exportProgress.contains(book.bookUrl)) return
|
|
||||||
CustomExporter(this).let {
|
|
||||||
it.scope = paresScope(scope)
|
|
||||||
it.size = size
|
|
||||||
it.export(path, book)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 导出Epub
|
|
||||||
*/
|
|
||||||
fun exportEPUB(path: String, book: Book) {
|
|
||||||
if (exportProgress.contains(book.bookUrl)) return
|
|
||||||
exportProgress[book.bookUrl] = 0
|
|
||||||
exportMsg.remove(book.bookUrl)
|
|
||||||
upAdapterLiveData.sendValue(book.bookUrl)
|
|
||||||
execute {
|
|
||||||
mutex.withLock {
|
|
||||||
while (exportNumber > 0) {
|
|
||||||
delay(1000)
|
|
||||||
}
|
|
||||||
exportNumber++
|
|
||||||
}
|
|
||||||
if (path.isContentScheme()) {
|
|
||||||
val uri = Uri.parse(path)
|
|
||||||
val doc = DocumentFile.fromTreeUri(context, uri)
|
|
||||||
?: throw NoStackTraceException("获取导出文档失败")
|
|
||||||
exportEpub(doc, book)
|
|
||||||
} else {
|
|
||||||
exportEpub(File(path).createFolderIfNotExist(), book)
|
|
||||||
}
|
|
||||||
}.onError {
|
|
||||||
exportProgress.remove(book.bookUrl)
|
|
||||||
exportMsg[book.bookUrl] = it.localizedMessage ?: "ERROR"
|
|
||||||
upAdapterLiveData.postValue(book.bookUrl)
|
|
||||||
it.printStackTrace()
|
|
||||||
AppLog.put("导出epub书籍<${book.name}>出错\n${it.localizedMessage}", it)
|
|
||||||
}.onSuccess {
|
|
||||||
exportProgress.remove(book.bookUrl)
|
|
||||||
exportMsg[book.bookUrl] = context.getString(R.string.export_success)
|
|
||||||
upAdapterLiveData.postValue(book.bookUrl)
|
|
||||||
}.onFinally {
|
|
||||||
exportNumber--
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun exportEpub(doc: DocumentFile, book: Book) {
|
|
||||||
val filename = book.getExportFileName("epub")
|
|
||||||
DocumentUtils.delete(doc, filename)
|
|
||||||
val epubBook = EpubBook()
|
|
||||||
epubBook.version = "2.0"
|
|
||||||
//set metadata
|
|
||||||
setEpubMetadata(book, epubBook)
|
|
||||||
//set cover
|
|
||||||
setCover(book, epubBook)
|
|
||||||
//set css
|
|
||||||
val contentModel = setAssets(doc, book, epubBook)
|
|
||||||
|
|
||||||
//设置正文
|
|
||||||
setEpubContent(contentModel, book, epubBook)
|
|
||||||
DocumentUtils.createFileIfNotExist(doc, filename)?.let { bookDoc ->
|
|
||||||
context.contentResolver.openOutputStream(bookDoc.uri, "wa")?.use { bookOs ->
|
|
||||||
EpubWriter().write(epubBook, BufferedOutputStream(bookOs))
|
|
||||||
}
|
|
||||||
if (AppConfig.exportToWebDav) {
|
|
||||||
// 导出到webdav
|
|
||||||
AppWebDav.exportWebDav(bookDoc.uri, filename)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private suspend fun exportEpub(file: File, book: Book) {
|
|
||||||
val filename = book.getExportFileName("epub")
|
|
||||||
val epubBook = EpubBook()
|
|
||||||
epubBook.version = "2.0"
|
|
||||||
//set metadata
|
|
||||||
setEpubMetadata(book, epubBook)
|
|
||||||
//set cover
|
|
||||||
setCover(book, epubBook)
|
|
||||||
//set css
|
|
||||||
val contentModel = setAssets(book, epubBook)
|
|
||||||
|
|
||||||
val bookPath = FileUtils.getPath(file, filename)
|
|
||||||
val bookFile = FileUtils.createFileWithReplace(bookPath)
|
|
||||||
//设置正文
|
|
||||||
setEpubContent(contentModel, book, epubBook)
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
EpubWriter().write(epubBook, BufferedOutputStream(FileOutputStream(bookFile)))
|
|
||||||
if (AppConfig.exportToWebDav) {
|
|
||||||
// 导出到webdav
|
|
||||||
AppWebDav.exportWebDav(Uri.fromFile(bookFile), filename)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setAssets(doc: DocumentFile, book: Book, epubBook: EpubBook): String {
|
|
||||||
var contentModel = ""
|
|
||||||
DocumentUtils.getDirDocument(doc, "Asset").let { customPath ->
|
|
||||||
if (customPath == null) {//使用内置模板
|
|
||||||
contentModel = setAssets(book, epubBook)
|
|
||||||
} else {//外部模板
|
|
||||||
customPath.listFiles().forEach { folder ->
|
|
||||||
if (folder.isDirectory && folder.name == "Text") {
|
|
||||||
folder.listFiles().sortedWith { o1, o2 ->
|
|
||||||
val name1 = o1.name ?: ""
|
|
||||||
val name2 = o2.name ?: ""
|
|
||||||
name1.cnCompare(name2)
|
|
||||||
}.forEach { file ->
|
|
||||||
if (file.isFile) {
|
|
||||||
when {
|
|
||||||
//正文模板
|
|
||||||
file.name.equals("chapter.html", true)
|
|
||||||
|| file.name.equals("chapter.xhtml", true) -> {
|
|
||||||
contentModel = file.readText(context)
|
|
||||||
}
|
|
||||||
//封面等其他模板
|
|
||||||
true == file.name?.endsWith("html", true) -> {
|
|
||||||
epubBook.addSection(
|
|
||||||
FileUtils.getNameExcludeExtension(
|
|
||||||
file.name ?: "Cover.html"
|
|
||||||
),
|
|
||||||
ResourceUtil.createPublicResource(
|
|
||||||
book.name,
|
|
||||||
book.getRealAuthor(),
|
|
||||||
book.getDisplayIntro(),
|
|
||||||
book.kind,
|
|
||||||
book.wordCount,
|
|
||||||
file.readText(context),
|
|
||||||
"${folder.name}/${file.name}"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
//其他格式文件当做资源文件
|
|
||||||
folder.listFiles().forEach {
|
|
||||||
if (it.isFile)
|
|
||||||
epubBook.resources.add(
|
|
||||||
Resource(
|
|
||||||
it.readBytes(context),
|
|
||||||
"${folder.name}/${it.name}"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (folder.isDirectory) {
|
|
||||||
//资源文件
|
|
||||||
folder.listFiles().forEach {
|
|
||||||
if (it.isFile)
|
|
||||||
epubBook.resources.add(
|
|
||||||
Resource(
|
|
||||||
it.readBytes(context),
|
|
||||||
"${folder.name}/${it.name}"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {//Asset下面的资源文件
|
|
||||||
epubBook.resources.add(
|
|
||||||
Resource(
|
|
||||||
folder.readBytes(context),
|
|
||||||
"${folder.name}"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return contentModel
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setAssets(book: Book, epubBook: EpubBook): String {
|
|
||||||
epubBook.resources.add(
|
|
||||||
Resource(
|
|
||||||
appCtx.assets.open("epub/fonts.css").readBytes(),
|
|
||||||
"Styles/fonts.css"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
epubBook.resources.add(
|
|
||||||
Resource(
|
|
||||||
appCtx.assets.open("epub/main.css").readBytes(),
|
|
||||||
"Styles/main.css"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
epubBook.resources.add(
|
|
||||||
Resource(
|
|
||||||
appCtx.assets.open("epub/logo.png").readBytes(),
|
|
||||||
"Images/logo.png"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
epubBook.addSection(
|
|
||||||
context.getString(R.string.img_cover),
|
|
||||||
ResourceUtil.createPublicResource(
|
|
||||||
book.name,
|
|
||||||
book.getRealAuthor(),
|
|
||||||
book.getDisplayIntro(),
|
|
||||||
book.kind,
|
|
||||||
book.wordCount,
|
|
||||||
String(appCtx.assets.open("epub/cover.html").readBytes()),
|
|
||||||
"Text/cover.html"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
epubBook.addSection(
|
|
||||||
context.getString(R.string.book_intro),
|
|
||||||
ResourceUtil.createPublicResource(
|
|
||||||
book.name,
|
|
||||||
book.getRealAuthor(),
|
|
||||||
book.getDisplayIntro(),
|
|
||||||
book.kind,
|
|
||||||
book.wordCount,
|
|
||||||
String(appCtx.assets.open("epub/intro.html").readBytes()),
|
|
||||||
"Text/intro.html"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return String(appCtx.assets.open("epub/chapter.html").readBytes())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setCover(book: Book, epubBook: EpubBook) {
|
|
||||||
Glide.with(context)
|
|
||||||
.asBitmap()
|
|
||||||
.load(book.getDisplayCover())
|
|
||||||
.into(object : CustomTarget<Bitmap>() {
|
|
||||||
override fun onResourceReady(
|
|
||||||
resource: Bitmap,
|
|
||||||
transition: Transition<in Bitmap>?
|
|
||||||
) {
|
|
||||||
val stream = ByteArrayOutputStream()
|
|
||||||
resource.compress(Bitmap.CompressFormat.JPEG, 100, stream)
|
|
||||||
val byteArray: ByteArray = stream.toByteArray()
|
|
||||||
stream.close()
|
|
||||||
epubBook.coverImage = Resource(byteArray, "Images/cover.jpg")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onLoadCleared(placeholder: Drawable?) {
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun setEpubContent(
|
|
||||||
contentModel: String,
|
|
||||||
book: Book,
|
|
||||||
epubBook: EpubBook
|
|
||||||
) {
|
|
||||||
//正文
|
|
||||||
val useReplace = AppConfig.exportUseReplace && book.getUseReplaceRule()
|
|
||||||
val contentProcessor = ContentProcessor.get(book.name, book.origin)
|
|
||||||
appDb.bookChapterDao.getChapterList(book.bookUrl).forEachIndexed { index, chapter ->
|
|
||||||
coroutineContext.ensureActive()
|
|
||||||
upAdapterLiveData.postValue(book.bookUrl)
|
|
||||||
exportProgress[book.bookUrl] = index
|
|
||||||
BookHelp.getContent(book, chapter).let { content ->
|
|
||||||
var content1 = fixPic(
|
|
||||||
epubBook,
|
|
||||||
book,
|
|
||||||
content ?: if (chapter.isVolume) "" else "null",
|
|
||||||
chapter
|
|
||||||
)
|
|
||||||
content1 = contentProcessor
|
|
||||||
.getContent(
|
|
||||||
book,
|
|
||||||
chapter,
|
|
||||||
content1,
|
|
||||||
includeTitle = false,
|
|
||||||
useReplace = useReplace,
|
|
||||||
chineseConvert = false,
|
|
||||||
reSegment = false
|
|
||||||
).toString()
|
|
||||||
val title = chapter.run {
|
|
||||||
// 不导出vip标识
|
|
||||||
isVip = false
|
|
||||||
getDisplayTitle(
|
|
||||||
contentProcessor.getTitleReplaceRules(),
|
|
||||||
useReplace = useReplace
|
|
||||||
)
|
|
||||||
}
|
|
||||||
epubBook.addSection(
|
|
||||||
title,
|
|
||||||
ResourceUtil.createChapterResource(
|
|
||||||
title.replace("\uD83D\uDD12", ""),
|
|
||||||
content1,
|
|
||||||
contentModel,
|
|
||||||
"Text/chapter_${index}.html"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun fixPic(
|
|
||||||
epubBook: EpubBook,
|
|
||||||
book: Book,
|
|
||||||
content: String,
|
|
||||||
chapter: BookChapter
|
|
||||||
): String {
|
|
||||||
val data = StringBuilder("")
|
|
||||||
content.split("\n").forEach { text ->
|
|
||||||
var text1 = text
|
|
||||||
val matcher = AppPattern.imgPattern.matcher(text)
|
|
||||||
while (matcher.find()) {
|
|
||||||
matcher.group(1)?.let {
|
|
||||||
val src = NetworkUtils.getAbsoluteURL(chapter.url, it)
|
|
||||||
val originalHref =
|
|
||||||
"${MD5Utils.md5Encode16(src)}.${BookHelp.getImageSuffix(src)}"
|
|
||||||
val href =
|
|
||||||
"Images/${MD5Utils.md5Encode16(src)}.${BookHelp.getImageSuffix(src)}"
|
|
||||||
val vFile = BookHelp.getImage(book, src)
|
|
||||||
val fp = FileResourceProvider(vFile.parent)
|
|
||||||
if (vFile.exists()) {
|
|
||||||
val img = LazyResource(fp, href, originalHref)
|
|
||||||
epubBook.resources.add(img)
|
|
||||||
}
|
|
||||||
text1 = text1.replace(src, "../${href}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
data.append(text1).append("\n")
|
|
||||||
}
|
|
||||||
return data.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setEpubMetadata(book: Book, epubBook: EpubBook) {
|
|
||||||
val metadata = Metadata()
|
|
||||||
metadata.titles.add(book.name)//书籍的名称
|
|
||||||
metadata.authors.add(Author(book.getRealAuthor()))//书籍的作者
|
|
||||||
metadata.language = "zh"//数据的语言
|
|
||||||
metadata.dates.add(Date())//数据的创建日期
|
|
||||||
metadata.publishers.add("Legado")//数据的创建者
|
|
||||||
metadata.descriptions.add(book.getDisplayIntro())//书籍的简介
|
|
||||||
//metadata.subjects.add("")//书籍的主题,在静读天下里面有使用这个分类书籍
|
|
||||||
epubBook.metadata = metadata
|
|
||||||
}
|
|
||||||
|
|
||||||
//////end of EPUB
|
|
||||||
|
|
||||||
//////start of custom exporter
|
|
||||||
/**
|
|
||||||
* 自定义Exporter
|
|
||||||
*
|
|
||||||
* @since 2023/5/23
|
|
||||||
*/
|
|
||||||
class CustomExporter(private val context: CacheViewModel) {
|
|
||||||
var scope: IntArray = IntArray(0)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* epub 文件包含最大章节数
|
|
||||||
*/
|
|
||||||
var size: Int = 1
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 导出Epub
|
|
||||||
*
|
|
||||||
* from [io.legado.app.ui.book.cache.CacheViewModel.exportEPUB]
|
|
||||||
* @param path 导出的路径
|
|
||||||
* @param book 书籍
|
|
||||||
*/
|
|
||||||
fun export(
|
|
||||||
path: String,
|
|
||||||
book: Book
|
|
||||||
) {
|
|
||||||
context.exportProgress[book.bookUrl] = 0
|
|
||||||
context.exportMsg.remove(book.bookUrl)
|
|
||||||
context.upAdapterLiveData.sendValue(book.bookUrl)
|
|
||||||
context.execute {
|
|
||||||
context.mutex.withLock {
|
|
||||||
while (context.exportNumber > 0) {
|
|
||||||
delay(1000)
|
|
||||||
}
|
|
||||||
context.exportNumber++
|
|
||||||
}
|
|
||||||
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"
|
|
||||||
context.upAdapterLiveData.postValue(book.bookUrl)
|
|
||||||
it.printStackTrace()
|
|
||||||
AppLog.put("导出epub书籍<${book.name}>出错\n${it.localizedMessage}", it)
|
|
||||||
}.onSuccess {
|
|
||||||
context.exportProgress.remove(book.bookUrl)
|
|
||||||
context.exportMsg[book.bookUrl] = context.context.getString(R.string.export_success)
|
|
||||||
context.upAdapterLiveData.postValue(book.bookUrl)
|
|
||||||
}.onFinally {
|
|
||||||
context.exportNumber--
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置epub正文
|
|
||||||
*
|
|
||||||
* from [io.legado.app.ui.book.cache.CacheViewModel.setEpubContent]
|
|
||||||
*
|
|
||||||
* @param contentModel 正文模板
|
|
||||||
* @param book 书籍
|
|
||||||
* @param epubBook 分割后的epub
|
|
||||||
* @param epubBookIndex 分割后的epub序号
|
|
||||||
*/
|
|
||||||
private suspend fun setEpubContent(
|
|
||||||
contentModel: String,
|
|
||||||
book: Book,
|
|
||||||
epubBook: EpubBook,
|
|
||||||
epubBookIndex: Int,
|
|
||||||
updateProgress: (chapterList: MutableList<BookChapter>, index: Int) -> Unit
|
|
||||||
) {
|
|
||||||
//正文
|
|
||||||
val useReplace = AppConfig.exportUseReplace && book.getUseReplaceRule()
|
|
||||||
val contentProcessor = ContentProcessor.get(book.name, book.origin)
|
|
||||||
var chapterList: MutableList<BookChapter> = ArrayList()
|
|
||||||
appDb.bookChapterDao.getChapterList(book.bookUrl).forEachIndexed { index, chapter ->
|
|
||||||
if (scope.indexOf(index) >= 0) {
|
|
||||||
chapterList.add(chapter)
|
|
||||||
}
|
|
||||||
if (scope.size == chapterList.size) {
|
|
||||||
return@forEachIndexed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// val totalChapterNum = book.totalChapterNum / scope.size
|
|
||||||
if (chapterList.size == 0) {
|
|
||||||
throw RuntimeException("书籍<${book.name}>(${epubBookIndex + 1})未找到章节信息")
|
|
||||||
}
|
|
||||||
chapterList = chapterList.subList(
|
|
||||||
epubBookIndex * size,
|
|
||||||
if ((epubBookIndex + 1) * size > scope.size) scope.size else (epubBookIndex + 1) * size
|
|
||||||
)
|
|
||||||
chapterList.forEachIndexed { index, chapter ->
|
|
||||||
coroutineContext.ensureActive()
|
|
||||||
updateProgress(chapterList, index)
|
|
||||||
BookHelp.getContent(book, chapter).let { content ->
|
|
||||||
var content1 = context.fixPic(
|
|
||||||
epubBook,
|
|
||||||
book,
|
|
||||||
content ?: if (chapter.isVolume) "" else "null",
|
|
||||||
chapter
|
|
||||||
)
|
|
||||||
content1 = contentProcessor
|
|
||||||
.getContent(
|
|
||||||
book,
|
|
||||||
chapter,
|
|
||||||
content1,
|
|
||||||
includeTitle = false,
|
|
||||||
useReplace = useReplace,
|
|
||||||
chineseConvert = false,
|
|
||||||
reSegment = false
|
|
||||||
).toString()
|
|
||||||
val title = chapter.run {
|
|
||||||
// 不导出vip标识
|
|
||||||
isVip = false
|
|
||||||
getDisplayTitle(
|
|
||||||
contentProcessor.getTitleReplaceRules(),
|
|
||||||
useReplace = useReplace
|
|
||||||
)
|
|
||||||
}
|
|
||||||
epubBook.addSection(
|
|
||||||
title,
|
|
||||||
ResourceUtil.createChapterResource(
|
|
||||||
title.replace("\uD83D\uDD12", ""),
|
|
||||||
content1,
|
|
||||||
contentModel,
|
|
||||||
"Text/chapter_${index}.html"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建多个epub 对象
|
|
||||||
*
|
|
||||||
* 分割epub时,一个书籍需要创建多个epub对象
|
|
||||||
* @param doc 导出文档
|
|
||||||
* @param book 书籍
|
|
||||||
*
|
|
||||||
* @return <内容模板字符串, <epub文件名, epub对象>>
|
|
||||||
*/
|
|
||||||
private fun createEpubs(
|
|
||||||
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)
|
|
||||||
doc?.let {
|
|
||||||
DocumentUtils.delete(it, filename)
|
|
||||||
}
|
|
||||||
val epubBook = EpubBook()
|
|
||||||
epubBook.version = "2.0"
|
|
||||||
//set metadata
|
|
||||||
context.setEpubMetadata(book, epubBook)
|
|
||||||
//set cover
|
|
||||||
context.setCover(book, epubBook)
|
|
||||||
//set css
|
|
||||||
contentModel = doc?.let {
|
|
||||||
context.setAssets(it, book, epubBook)
|
|
||||||
} ?: context.setAssets(book, epubBook)
|
|
||||||
|
|
||||||
// add epubBook
|
|
||||||
result.add(Pair(filename, epubBook))
|
|
||||||
}
|
|
||||||
return Pair(contentModel, result)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 保存文件到 设备
|
|
||||||
*/
|
|
||||||
private suspend fun save2Drive(
|
|
||||||
filename: String,
|
|
||||||
epubBook: EpubBook,
|
|
||||||
doc: DocumentFile,
|
|
||||||
callback: (total: Int, progress: Int) -> Unit
|
|
||||||
) {
|
|
||||||
DocumentUtils.createFileIfNotExist(doc, filename)?.let { bookDoc ->
|
|
||||||
context.context.contentResolver.openOutputStream(bookDoc.uri, "wa")?.use { bookOs ->
|
|
||||||
EpubWriter()
|
|
||||||
.setCallback(object : EpubWriterProcessor.Callback {
|
|
||||||
override fun onProgressing(total: Int, progress: Int) {
|
|
||||||
callback(total, progress)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.write(epubBook, BufferedOutputStream(bookOs))
|
|
||||||
}
|
|
||||||
if (AppConfig.exportToWebDav) {
|
|
||||||
// 导出到webdav
|
|
||||||
AppWebDav.exportWebDav(bookDoc.uri, filename)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 保存文件到 设备
|
|
||||||
*/
|
|
||||||
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()
|
|
||||||
.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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 解析 分割epub后的数量
|
|
||||||
*
|
|
||||||
* @param total 章节总数
|
|
||||||
* @param size 每个epub文件包含多少章节
|
|
||||||
*/
|
|
||||||
private fun paresNumOfEpub(total: Int, size: Int): Int {
|
|
||||||
val i = total % size
|
|
||||||
var result = total / size
|
|
||||||
if (i > 0) {
|
|
||||||
result++
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -6,9 +6,9 @@
|
|||||||
android:viewportHeight="1024">
|
android:viewportHeight="1024">
|
||||||
|
|
||||||
<path
|
<path
|
||||||
android:fillColor="#050505"
|
android:fillColor="#4A4B4A"
|
||||||
android:pathData="M526 780H380.8c-19.2 0-34.8-15.6-34.8-34.8V267.8c0-19.2 15.6-34.8 34.8-34.8h477.4c19.2 0 34.8 15.6 34.8 34.8V413c0 18.8 15.2 34 34 34s34-15.2 34-34V234.1c0-38.2-31-69.1-69.1-69.1H347.1c-38.2 0-69.1 31-69.1 69.1v544.7c0 38.2 31 69.1 69.1 69.1H526c18.8 0 34-15.2 34-34 0-18.7-15.2-33.9-34-33.9z" />
|
android:pathData="M526 780H380.8c-19.2 0-34.8-15.6-34.8-34.8V267.8c0-19.2 15.6-34.8 34.8-34.8h477.4c19.2 0 34.8 15.6 34.8 34.8V413c0 18.8 15.2 34 34 34s34-15.2 34-34V234.1c0-38.2-31-69.1-69.1-69.1H347.1c-38.2 0-69.1 31-69.1 69.1v544.7c0 38.2 31 69.1 69.1 69.1H526c18.8 0 34-15.2 34-34 0-18.7-15.2-33.9-34-33.9z" />
|
||||||
<path
|
<path
|
||||||
android:fillColor="#000000"
|
android:fillColor="#4A4B4A"
|
||||||
android:pathData="M950.9 654.8l-0.1-0.1L817.1 521c-13.3-13.3-34.8-13.3-48.1 0-13.3 13.3-13.3 34.8 0 48.1l75.9 75.9H632c-18.8 0-34 15.2-34 34s15.2 34 34 34h212.7L769 788.7c-13.3 13.3-13.3 34.8 0 48.1 13.3 13.3 34.8 13.3 48.1 0l131.6-131.6c7.5-6.2 12.3-15.6 12.3-26.1 0-9.6-3.9-18.1-10.1-24.3z" />
|
android:pathData="M950.9 654.8l-0.1-0.1L817.1 521c-13.3-13.3-34.8-13.3-48.1 0-13.3 13.3-13.3 34.8 0 48.1l75.9 75.9H632c-18.8 0-34 15.2-34 34s15.2 34 34 34h212.7L769 788.7c-13.3 13.3-13.3 34.8 0 48.1 13.3 13.3 34.8 13.3 48.1 0l131.6-131.6c7.5-6.2 12.3-15.6 12.3-26.1 0-9.6-3.9-18.1-10.1-24.3z" />
|
||||||
</vector>
|
</vector>
|
||||||
Reference in New Issue
Block a user